Add humidity support to climate (#5732)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Stefan Rado 2023-12-13 02:23:02 +01:00 committed by GitHub
parent a72725f4b4
commit 6c7a133faa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 359 additions and 7 deletions

View file

@ -859,6 +859,10 @@ message ListEntitiesClimateResponse {
string icon = 19; string icon = 19;
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
float visual_current_temperature_step = 21; float visual_current_temperature_step = 21;
bool supports_current_humidity = 22;
bool supports_target_humidity = 23;
float visual_min_humidity = 24;
float visual_max_humidity = 25;
} }
message ClimateStateResponse { message ClimateStateResponse {
option (id) = 47; option (id) = 47;
@ -879,6 +883,8 @@ message ClimateStateResponse {
string custom_fan_mode = 11; string custom_fan_mode = 11;
ClimatePreset preset = 12; ClimatePreset preset = 12;
string custom_preset = 13; string custom_preset = 13;
float current_humidity = 14;
float target_humidity = 15;
} }
message ClimateCommandRequest { message ClimateCommandRequest {
option (id) = 48; option (id) = 48;
@ -907,6 +913,8 @@ message ClimateCommandRequest {
ClimatePreset preset = 19; ClimatePreset preset = 19;
bool has_custom_preset = 20; bool has_custom_preset = 20;
string custom_preset = 21; string custom_preset = 21;
bool has_target_humidity = 22;
float target_humidity = 23;
} }
// ==================== NUMBER ==================== // ==================== NUMBER ====================

View file

@ -560,6 +560,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
resp.custom_preset = climate->custom_preset.value(); resp.custom_preset = climate->custom_preset.value();
if (traits.get_supports_swing_modes()) if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
if (traits.get_supports_current_humidity())
resp.current_humidity = climate->current_humidity;
if (traits.get_supports_target_humidity())
resp.target_humidity = climate->target_humidity;
return this->send_climate_state_response(resp); return this->send_climate_state_response(resp);
} }
bool APIConnection::send_climate_info(climate::Climate *climate) { bool APIConnection::send_climate_info(climate::Climate *climate) {
@ -576,7 +580,9 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category()); msg.entity_category = static_cast<enums::EntityCategory>(climate->get_entity_category());
msg.supports_current_temperature = traits.get_supports_current_temperature(); msg.supports_current_temperature = traits.get_supports_current_temperature();
msg.supports_current_humidity = traits.get_supports_current_humidity();
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
msg.supports_target_humidity = traits.get_supports_target_humidity();
for (auto mode : traits.get_supported_modes()) for (auto mode : traits.get_supported_modes())
msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode)); msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
@ -585,6 +591,8 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY);
msg.supports_action = traits.get_supports_action(); msg.supports_action = traits.get_supports_action();
@ -615,6 +623,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
call.set_target_temperature_low(msg.target_temperature_low); call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_target_temperature_high) if (msg.has_target_temperature_high)
call.set_target_temperature_high(msg.target_temperature_high); call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_target_humidity)
call.set_target_humidity(msg.target_humidity);
if (msg.has_fan_mode) if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode) if (msg.has_custom_fan_mode)

View file

@ -3611,6 +3611,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v
this->entity_category = value.as_enum<enums::EntityCategory>(); this->entity_category = value.as_enum<enums::EntityCategory>();
return true; return true;
} }
case 22: {
this->supports_current_humidity = value.as_bool();
return true;
}
case 23: {
this->supports_target_humidity = value.as_bool();
return true;
}
default: default:
return false; return false;
} }
@ -3667,6 +3675,14 @@ bool ListEntitiesClimateResponse::decode_32bit(uint32_t field_id, Proto32Bit val
this->visual_current_temperature_step = value.as_float(); this->visual_current_temperature_step = value.as_float();
return true; return true;
} }
case 24: {
this->visual_min_humidity = value.as_float();
return true;
}
case 25: {
this->visual_max_humidity = value.as_float();
return true;
}
default: default:
return false; return false;
} }
@ -3705,6 +3721,10 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(19, this->icon); buffer.encode_string(19, this->icon);
buffer.encode_enum<enums::EntityCategory>(20, this->entity_category); buffer.encode_enum<enums::EntityCategory>(20, this->entity_category);
buffer.encode_float(21, this->visual_current_temperature_step); buffer.encode_float(21, this->visual_current_temperature_step);
buffer.encode_bool(22, this->supports_current_humidity);
buffer.encode_bool(23, this->supports_target_humidity);
buffer.encode_float(24, this->visual_min_humidity);
buffer.encode_float(25, this->visual_max_humidity);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesClimateResponse::dump_to(std::string &out) const { void ListEntitiesClimateResponse::dump_to(std::string &out) const {
@ -3810,7 +3830,24 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%g", this->visual_current_temperature_step); sprintf(buffer, "%g", this->visual_current_temperature_step);
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append("}");
out.append(" supports_current_humidity: ");
out.append(YESNO(this->supports_current_humidity));
out.append("\n");
out.append(" supports_target_humidity: ");
out.append(YESNO(this->supports_target_humidity));
out.append("\n");
out.append(" visual_min_humidity: ");
sprintf(buffer, "%g", this->visual_min_humidity);
out.append(buffer);
out.append("\n");
out.append(" visual_max_humidity: ");
sprintf(buffer, "%g", this->visual_max_humidity);
out.append(buffer);
out.append("\n");
} }
#endif #endif
bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
@ -3879,6 +3916,14 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
this->target_temperature_high = value.as_float(); this->target_temperature_high = value.as_float();
return true; return true;
} }
case 14: {
this->current_humidity = value.as_float();
return true;
}
case 15: {
this->target_humidity = value.as_float();
return true;
}
default: default:
return false; return false;
} }
@ -3897,6 +3942,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(11, this->custom_fan_mode); buffer.encode_string(11, this->custom_fan_mode);
buffer.encode_enum<enums::ClimatePreset>(12, this->preset); buffer.encode_enum<enums::ClimatePreset>(12, this->preset);
buffer.encode_string(13, this->custom_preset); buffer.encode_string(13, this->custom_preset);
buffer.encode_float(14, this->current_humidity);
buffer.encode_float(15, this->target_humidity);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ClimateStateResponse::dump_to(std::string &out) const { void ClimateStateResponse::dump_to(std::string &out) const {
@ -3958,7 +4005,16 @@ void ClimateStateResponse::dump_to(std::string &out) const {
out.append(" custom_preset: "); out.append(" custom_preset: ");
out.append("'").append(this->custom_preset).append("'"); out.append("'").append(this->custom_preset).append("'");
out.append("\n"); out.append("\n");
out.append("}");
out.append(" current_humidity: ");
sprintf(buffer, "%g", this->current_humidity);
out.append(buffer);
out.append("\n");
out.append(" target_humidity: ");
sprintf(buffer, "%g", this->target_humidity);
out.append(buffer);
out.append("\n");
} }
#endif #endif
bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
@ -4023,6 +4079,10 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
this->has_custom_preset = value.as_bool(); this->has_custom_preset = value.as_bool();
return true; return true;
} }
case 22: {
this->has_target_humidity = value.as_bool();
return true;
}
default: default:
return false; return false;
} }
@ -4059,6 +4119,10 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
this->target_temperature_high = value.as_float(); this->target_temperature_high = value.as_float();
return true; return true;
} }
case 23: {
this->target_humidity = value.as_float();
return true;
}
default: default:
return false; return false;
} }
@ -4085,6 +4149,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::ClimatePreset>(19, this->preset); buffer.encode_enum<enums::ClimatePreset>(19, this->preset);
buffer.encode_bool(20, this->has_custom_preset); buffer.encode_bool(20, this->has_custom_preset);
buffer.encode_string(21, this->custom_preset); buffer.encode_string(21, this->custom_preset);
buffer.encode_bool(22, this->has_target_humidity);
buffer.encode_float(23, this->target_humidity);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ClimateCommandRequest::dump_to(std::string &out) const { void ClimateCommandRequest::dump_to(std::string &out) const {
@ -4177,6 +4243,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
out.append(" custom_preset: "); out.append(" custom_preset: ");
out.append("'").append(this->custom_preset).append("'"); out.append("'").append(this->custom_preset).append("'");
out.append("\n"); out.append("\n");
out.append(" has_target_humidity: ");
out.append(YESNO(this->has_target_humidity));
out.append("\n");
out.append(" target_humidity: ");
sprintf(buffer, "%g", this->target_humidity);
out.append(buffer);
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif

View file

@ -985,6 +985,10 @@ class ListEntitiesClimateResponse : public ProtoMessage {
std::string icon{}; std::string icon{};
enums::EntityCategory entity_category{}; enums::EntityCategory entity_category{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false};
bool supports_target_humidity{false};
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
@ -1010,6 +1014,8 @@ class ClimateStateResponse : public ProtoMessage {
std::string custom_fan_mode{}; std::string custom_fan_mode{};
enums::ClimatePreset preset{}; enums::ClimatePreset preset{};
std::string custom_preset{}; std::string custom_preset{};
float current_humidity{0.0f};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
@ -1043,6 +1049,8 @@ class ClimateCommandRequest : public ProtoMessage {
enums::ClimatePreset preset{}; enums::ClimatePreset preset{};
bool has_custom_preset{false}; bool has_custom_preset{false};
std::string custom_preset{}; std::string custom_preset{};
bool has_target_humidity{false};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;

View file

@ -15,6 +15,16 @@ void BangBangClimate::setup() {
this->publish_state(); this->publish_state();
}); });
this->current_temperature = this->sensor_->state; this->current_temperature = this->sensor_->state;
// register for humidity values and get initial state
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->current_humidity = state;
this->publish_state();
});
this->current_humidity = this->humidity_sensor_->state;
}
// restore set points // restore set points
auto restore = this->restore_state_(); auto restore = this->restore_state_();
if (restore.has_value()) { if (restore.has_value()) {
@ -47,6 +57,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) {
climate::ClimateTraits BangBangClimate::traits() { climate::ClimateTraits BangBangClimate::traits() {
auto traits = climate::ClimateTraits(); auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true); traits.set_supports_current_temperature(true);
if (this->humidity_sensor_ != nullptr)
traits.set_supports_current_humidity(true);
traits.set_supported_modes({ traits.set_supported_modes({
climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_OFF,
}); });
@ -171,6 +183,7 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
BangBangClimate::BangBangClimate() BangBangClimate::BangBangClimate()
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }

View file

@ -24,6 +24,7 @@ class BangBangClimate : public climate::Climate, public Component {
void dump_config() override; void dump_config() override;
void set_sensor(sensor::Sensor *sensor); void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
Trigger<> *get_idle_trigger() const; Trigger<> *get_idle_trigger() const;
Trigger<> *get_cool_trigger() const; Trigger<> *get_cool_trigger() const;
void set_supports_cool(bool supports_cool); void set_supports_cool(bool supports_cool);
@ -48,6 +49,9 @@ class BangBangClimate : public climate::Climate, public Component {
/// The sensor used for getting the current temperature /// The sensor used for getting the current temperature
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
/// The sensor used for getting the current humidity
sensor::Sensor *humidity_sensor_{nullptr};
/** The trigger to call when the controller should switch to idle mode. /** The trigger to call when the controller should switch to idle mode.
* *
* In idle mode, the controller is assumed to have both heating and cooling disabled. * In idle mode, the controller is assumed to have both heating and cooling disabled.

View file

@ -8,6 +8,7 @@ from esphome.const import (
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_HIGH,
CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
CONF_HEAT_ACTION, CONF_HEAT_ACTION,
CONF_HUMIDITY_SENSOR,
CONF_ID, CONF_ID,
CONF_IDLE_ACTION, CONF_IDLE_ACTION,
CONF_SENSOR, CONF_SENSOR,
@ -22,6 +23,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(BangBangClimate), cv.GenerateID(): cv.declare_id(BangBangClimate),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True),
@ -47,6 +49,10 @@ async def to_code(config):
sens = await cg.get_variable(config[CONF_SENSOR]) sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))
if CONF_HUMIDITY_SENSOR in config:
sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR])
cg.add(var.set_humidity_sensor(sens))
normal_config = BangBangClimateTargetTempConfig( normal_config = BangBangClimateTargetTempConfig(
config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH],

View file

@ -8,6 +8,7 @@ from esphome.const import (
CONF_AWAY, CONF_AWAY,
CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_COMMAND_TOPIC,
CONF_AWAY_STATE_TOPIC, CONF_AWAY_STATE_TOPIC,
CONF_CURRENT_HUMIDITY_STATE_TOPIC,
CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CURRENT_TEMPERATURE_STATE_TOPIC,
CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_FAN_MODE,
CONF_CUSTOM_PRESET, CONF_CUSTOM_PRESET,
@ -28,6 +29,8 @@ from esphome.const import (
CONF_SWING_MODE, CONF_SWING_MODE,
CONF_SWING_MODE_COMMAND_TOPIC, CONF_SWING_MODE_COMMAND_TOPIC,
CONF_SWING_MODE_STATE_TOPIC, CONF_SWING_MODE_STATE_TOPIC,
CONF_TARGET_HUMIDITY_COMMAND_TOPIC,
CONF_TARGET_HUMIDITY_STATE_TOPIC,
CONF_TARGET_TEMPERATURE, CONF_TARGET_TEMPERATURE,
CONF_TARGET_TEMPERATURE_COMMAND_TOPIC, CONF_TARGET_TEMPERATURE_COMMAND_TOPIC,
CONF_TARGET_TEMPERATURE_STATE_TOPIC, CONF_TARGET_TEMPERATURE_STATE_TOPIC,
@ -106,6 +109,9 @@ CLIMATE_SWING_MODES = {
validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True)
CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_CURRENT_TEMPERATURE = "current_temperature"
CONF_MIN_HUMIDITY = "min_humidity"
CONF_MAX_HUMIDITY = "max_humidity"
CONF_TARGET_HUMIDITY = "target_humidity"
visual_temperature = cv.float_with_unit( visual_temperature = cv.float_with_unit(
"visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?" "visual_temperature", "(°C|° C|°|C|° K|° K|K|°F|° F|F)?"
@ -153,6 +159,8 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA,
cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int,
cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int,
} }
), ),
cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All( cv.Optional(CONF_ACTION_STATE_TOPIC): cv.All(
@ -167,6 +175,9 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All( cv.Optional(CONF_CURRENT_TEMPERATURE_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic cv.requires_component("mqtt"), cv.publish_topic
), ),
cv.Optional(CONF_CURRENT_HUMIDITY_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All( cv.Optional(CONF_FAN_MODE_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic cv.requires_component("mqtt"), cv.publish_topic
), ),
@ -209,6 +220,12 @@ CLIMATE_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).
cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All( cv.Optional(CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic cv.requires_component("mqtt"), cv.publish_topic
), ),
cv.Optional(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_TARGET_HUMIDITY_STATE_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.publish_topic
),
cv.Optional(CONF_ON_CONTROL): automation.validate_automation( cv.Optional(CONF_ON_CONTROL): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ControlTrigger),
@ -238,6 +255,10 @@ async def setup_climate_core_(var, config):
visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE], visual[CONF_TEMPERATURE_STEP][CONF_CURRENT_TEMPERATURE],
) )
) )
if CONF_MIN_HUMIDITY in visual:
cg.add(var.set_visual_min_humidity_override(visual[CONF_MIN_HUMIDITY]))
if CONF_MAX_HUMIDITY in visual:
cg.add(var.set_visual_max_humidity_override(visual[CONF_MAX_HUMIDITY]))
if CONF_MQTT_ID in config: if CONF_MQTT_ID in config:
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
@ -255,6 +276,12 @@ async def setup_climate_core_(var, config):
config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC] config[CONF_CURRENT_TEMPERATURE_STATE_TOPIC]
) )
) )
if CONF_CURRENT_HUMIDITY_STATE_TOPIC in config:
cg.add(
mqtt_.set_custom_current_humidity_state_topic(
config[CONF_CURRENT_HUMIDITY_STATE_TOPIC]
)
)
if CONF_FAN_MODE_COMMAND_TOPIC in config: if CONF_FAN_MODE_COMMAND_TOPIC in config:
cg.add( cg.add(
mqtt_.set_custom_fan_mode_command_topic( mqtt_.set_custom_fan_mode_command_topic(
@ -323,6 +350,18 @@ async def setup_climate_core_(var, config):
config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC] config[CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC]
) )
) )
if CONF_TARGET_HUMIDITY_COMMAND_TOPIC in config:
cg.add(
mqtt_.set_custom_target_humidity_command_topic(
config[CONF_TARGET_HUMIDITY_COMMAND_TOPIC]
)
)
if CONF_TARGET_HUMIDITY_STATE_TOPIC in config:
cg.add(
mqtt_.set_custom_target_humidity_state_topic(
config[CONF_TARGET_HUMIDITY_STATE_TOPIC]
)
)
for conf in config.get(CONF_ON_STATE, []): for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
@ -351,6 +390,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): 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_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int),
cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"),
cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
validate_climate_fan_mode validate_climate_fan_mode
@ -387,6 +427,9 @@ async def climate_control_to_code(config, action_id, template_arg, args):
config[CONF_TARGET_TEMPERATURE_HIGH], args, float config[CONF_TARGET_TEMPERATURE_HIGH], args, float
) )
cg.add(var.set_target_temperature_high(template_)) cg.add(var.set_target_temperature_high(template_))
if CONF_TARGET_HUMIDITY in config:
template_ = await cg.templatable(config[CONF_TARGET_HUMIDITY], args, float)
cg.add(var.set_target_humidity(template_))
if CONF_FAN_MODE in config: if CONF_FAN_MODE in config:
template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
cg.add(var.set_fan_mode(template_)) cg.add(var.set_fan_mode(template_))

View file

@ -14,6 +14,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, target_temperature) TEMPLATABLE_VALUE(float, target_temperature)
TEMPLATABLE_VALUE(float, target_temperature_low) TEMPLATABLE_VALUE(float, target_temperature_low)
TEMPLATABLE_VALUE(float, target_temperature_high) TEMPLATABLE_VALUE(float, target_temperature_high)
TEMPLATABLE_VALUE(float, target_humidity)
TEMPLATABLE_VALUE(bool, away) TEMPLATABLE_VALUE(bool, away)
TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) TEMPLATABLE_VALUE(ClimateFanMode, fan_mode)
TEMPLATABLE_VALUE(std::string, custom_fan_mode) TEMPLATABLE_VALUE(std::string, custom_fan_mode)
@ -27,6 +28,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...));
call.set_target_temperature_low(this->target_temperature_low_.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_target_temperature_high(this->target_temperature_high_.optional_value(x...));
call.set_target_humidity(this->target_humidity_.optional_value(x...));
if (away_.has_value()) { if (away_.has_value()) {
call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME);
} }

View file

@ -45,6 +45,9 @@ void ClimateCall::perform() {
if (this->target_temperature_high_.has_value()) { if (this->target_temperature_high_.has_value()) {
ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_);
} }
if (this->target_humidity_.has_value()) {
ESP_LOGD(TAG, " Target Humidity: %.0f", *this->target_humidity_);
}
this->parent_->control(*this); this->parent_->control(*this);
} }
void ClimateCall::validate_() { void ClimateCall::validate_() {
@ -262,10 +265,16 @@ ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_h
this->target_temperature_high_ = target_temperature_high; this->target_temperature_high_ = target_temperature_high;
return *this; return *this;
} }
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
this->target_humidity_ = target_humidity;
return *this;
}
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; } 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() 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_low() const { return this->target_temperature_low_; }
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; }
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
@ -283,6 +292,10 @@ ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperat
this->target_temperature_ = target_temperature; this->target_temperature_ = target_temperature;
return *this; return *this;
} }
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
this->target_humidity_ = target_humidity;
return *this;
}
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) { ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
this->mode_ = mode; this->mode_ = mode;
return *this; return *this;
@ -343,6 +356,9 @@ void Climate::save_state_() {
} else { } else {
state.target_temperature = this->target_temperature; state.target_temperature = this->target_temperature;
} }
if (traits.get_supports_target_humidity()) {
state.target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && fan_mode.has_value()) { if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
state.uses_custom_fan_mode = false; state.uses_custom_fan_mode = false;
state.fan_mode = this->fan_mode.value(); state.fan_mode = this->fan_mode.value();
@ -408,6 +424,12 @@ void Climate::publish_state() {
} else { } else {
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
} }
if (traits.get_supports_current_humidity()) {
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
}
if (traits.get_supports_target_humidity()) {
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
}
// Send state to frontend // Send state to frontend
this->state_callback_.call(*this); this->state_callback_.call(*this);
@ -427,6 +449,12 @@ ClimateTraits Climate::get_traits() {
traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_); traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_);
traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_); traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_);
} }
if (this->visual_min_humidity_override_.has_value()) {
traits.set_visual_min_humidity(*this->visual_min_humidity_override_);
}
if (this->visual_max_humidity_override_.has_value()) {
traits.set_visual_max_humidity(*this->visual_max_humidity_override_);
}
return traits; return traits;
} }
@ -441,6 +469,12 @@ void Climate::set_visual_temperature_step_override(float target, float current)
this->visual_target_temperature_step_override_ = target; this->visual_target_temperature_step_override_ = target;
this->visual_current_temperature_step_override_ = current; this->visual_current_temperature_step_override_ = current;
} }
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
this->visual_min_humidity_override_ = visual_min_humidity_override;
}
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
this->visual_max_humidity_override_ = visual_max_humidity_override;
}
ClimateCall Climate::make_call() { return ClimateCall(this); } ClimateCall Climate::make_call() { return ClimateCall(this); }
@ -454,6 +488,9 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
} else { } else {
call.set_target_temperature(this->target_temperature); call.set_target_temperature(this->target_temperature);
} }
if (traits.get_supports_target_humidity()) {
call.set_target_humidity(this->target_humidity);
}
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
call.set_fan_mode(this->fan_mode); call.set_fan_mode(this->fan_mode);
} }
@ -474,6 +511,9 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
} else { } else {
climate->target_temperature = this->target_temperature; climate->target_temperature = this->target_temperature;
} }
if (traits.get_supports_target_humidity()) {
climate->target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
climate->fan_mode = this->fan_mode; climate->fan_mode = this->fan_mode;
} }
@ -530,17 +570,25 @@ void Climate::dump_traits_(const char *tag) {
auto traits = this->get_traits(); auto traits = this->get_traits();
ESP_LOGCONFIG(tag, "ClimateTraits:"); ESP_LOGCONFIG(tag, "ClimateTraits:");
ESP_LOGCONFIG(tag, " [x] Visual settings:"); ESP_LOGCONFIG(tag, " [x] Visual settings:");
ESP_LOGCONFIG(tag, " - Min: %.1f", traits.get_visual_min_temperature()); ESP_LOGCONFIG(tag, " - Min temperature: %.1f", traits.get_visual_min_temperature());
ESP_LOGCONFIG(tag, " - Max: %.1f", traits.get_visual_max_temperature()); ESP_LOGCONFIG(tag, " - Max temperature: %.1f", traits.get_visual_max_temperature());
ESP_LOGCONFIG(tag, " - Step:"); ESP_LOGCONFIG(tag, " - Temperature step:");
ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step()); ESP_LOGCONFIG(tag, " Target: %.1f", traits.get_visual_target_temperature_step());
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step()); ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
ESP_LOGCONFIG(tag, " - Min humidity: %.0f", traits.get_visual_min_humidity());
ESP_LOGCONFIG(tag, " - Max humidity: %.0f", traits.get_visual_max_humidity());
if (traits.get_supports_current_temperature()) { if (traits.get_supports_current_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature"); ESP_LOGCONFIG(tag, " [x] Supports current temperature");
} }
if (traits.get_supports_current_humidity()) {
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
}
if (traits.get_supports_two_point_target_temperature()) { if (traits.get_supports_two_point_target_temperature()) {
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature"); ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
} }
if (traits.get_supports_target_humidity()) {
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
}
if (traits.get_supports_action()) { if (traits.get_supports_action()) {
ESP_LOGCONFIG(tag, " [x] Supports action"); ESP_LOGCONFIG(tag, " [x] Supports action");
} }

View file

@ -64,6 +64,10 @@ class ClimateCall {
* For climate devices with two point target temperature control * For climate devices with two point target temperature control
*/ */
ClimateCall &set_target_temperature_high(optional<float> target_temperature_high); ClimateCall &set_target_temperature_high(optional<float> target_temperature_high);
/// Set the target humidity of the climate device.
ClimateCall &set_target_humidity(float target_humidity);
/// Set the target humidity of the climate device.
ClimateCall &set_target_humidity(optional<float> target_humidity);
/// Set the fan mode of the climate device. /// Set the fan mode of the climate device.
ClimateCall &set_fan_mode(ClimateFanMode fan_mode); ClimateCall &set_fan_mode(ClimateFanMode fan_mode);
/// Set the fan mode of the climate device. /// Set the fan mode of the climate device.
@ -93,6 +97,7 @@ class ClimateCall {
const optional<float> &get_target_temperature() const; const optional<float> &get_target_temperature() const;
const optional<float> &get_target_temperature_low() const; const optional<float> &get_target_temperature_low() const;
const optional<float> &get_target_temperature_high() const; const optional<float> &get_target_temperature_high() const;
const optional<float> &get_target_humidity() const;
const optional<ClimateFanMode> &get_fan_mode() const; const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const; const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<std::string> &get_custom_fan_mode() const; const optional<std::string> &get_custom_fan_mode() const;
@ -107,6 +112,7 @@ class ClimateCall {
optional<float> target_temperature_; optional<float> target_temperature_;
optional<float> target_temperature_low_; optional<float> target_temperature_low_;
optional<float> target_temperature_high_; optional<float> target_temperature_high_;
optional<float> target_humidity_;
optional<ClimateFanMode> fan_mode_; optional<ClimateFanMode> fan_mode_;
optional<ClimateSwingMode> swing_mode_; optional<ClimateSwingMode> swing_mode_;
optional<std::string> custom_fan_mode_; optional<std::string> custom_fan_mode_;
@ -136,6 +142,7 @@ struct ClimateDeviceRestoreState {
float target_temperature_high; float target_temperature_high;
}; };
}; };
float target_humidity;
/// Convert this struct to a climate call that can be performed. /// Convert this struct to a climate call that can be performed.
ClimateCall to_call(Climate *climate); ClimateCall to_call(Climate *climate);
@ -164,11 +171,16 @@ class Climate : public EntityBase {
/// The active mode of the climate device. /// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF}; ClimateMode mode{CLIMATE_MODE_OFF};
/// The active state of the climate device. /// The active state of the climate device.
ClimateAction action{CLIMATE_ACTION_OFF}; ClimateAction action{CLIMATE_ACTION_OFF};
/// The current temperature of the climate device, as reported from the integration. /// The current temperature of the climate device, as reported from the integration.
float current_temperature{NAN}; float current_temperature{NAN};
/// The current humidity of the climate device, as reported from the integration.
float current_humidity{NAN};
union { union {
/// The target temperature of the climate device. /// The target temperature of the climate device.
float target_temperature; float target_temperature;
@ -180,6 +192,9 @@ class Climate : public EntityBase {
}; };
}; };
/// The target humidity of the climate device.
float target_humidity;
/// The active fan mode of the climate device. /// The active fan mode of the climate device.
optional<ClimateFanMode> fan_mode; optional<ClimateFanMode> fan_mode;
@ -233,6 +248,8 @@ class Climate : public EntityBase {
void set_visual_min_temperature_override(float visual_min_temperature_override); 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_max_temperature_override(float visual_max_temperature_override);
void set_visual_temperature_step_override(float target, float current); void set_visual_temperature_step_override(float target, float current);
void set_visual_min_humidity_override(float visual_min_humidity_override);
void set_visual_max_humidity_override(float visual_max_humidity_override);
protected: protected:
friend ClimateCall; friend ClimateCall;
@ -282,6 +299,8 @@ class Climate : public EntityBase {
optional<float> visual_max_temperature_override_{}; optional<float> visual_max_temperature_override_{};
optional<float> visual_target_temperature_step_override_{}; optional<float> visual_target_temperature_step_override_{};
optional<float> visual_current_temperature_step_override_{}; optional<float> visual_current_temperature_step_override_{};
optional<float> visual_min_humidity_override_{};
optional<float> visual_max_humidity_override_{};
}; };
} // namespace climate } // namespace climate

View file

@ -44,10 +44,18 @@ class ClimateTraits {
void set_supports_current_temperature(bool supports_current_temperature) { void set_supports_current_temperature(bool supports_current_temperature) {
supports_current_temperature_ = supports_current_temperature; supports_current_temperature_ = supports_current_temperature;
} }
bool get_supports_current_humidity() const { return supports_current_humidity_; }
void set_supports_current_humidity(bool supports_current_humidity) {
supports_current_humidity_ = supports_current_humidity;
}
bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; }
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
supports_two_point_target_temperature_ = supports_two_point_target_temperature; supports_two_point_target_temperature_ = supports_two_point_target_temperature;
} }
bool get_supports_target_humidity() const { return supports_target_humidity_; }
void set_supports_target_humidity(bool supports_target_humidity) {
supports_target_humidity_ = supports_target_humidity;
}
void set_supported_modes(std::set<ClimateMode> modes) { supported_modes_ = std::move(modes); } void set_supported_modes(std::set<ClimateMode> modes) { supported_modes_ = std::move(modes); }
void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20") ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
@ -153,6 +161,11 @@ class ClimateTraits {
int8_t get_target_temperature_accuracy_decimals() const; int8_t get_target_temperature_accuracy_decimals() const;
int8_t get_current_temperature_accuracy_decimals() const; int8_t get_current_temperature_accuracy_decimals() const;
float get_visual_min_humidity() const { return visual_min_humidity_; }
void set_visual_min_humidity(float visual_min_humidity) { visual_min_humidity_ = visual_min_humidity; }
float get_visual_max_humidity() const { return visual_max_humidity_; }
void set_visual_max_humidity(float visual_max_humidity) { visual_max_humidity_ = visual_max_humidity; }
protected: protected:
void set_mode_support_(climate::ClimateMode mode, bool supported) { void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) { if (supported) {
@ -177,7 +190,9 @@ class ClimateTraits {
} }
bool supports_current_temperature_{false}; bool supports_current_temperature_{false};
bool supports_current_humidity_{false};
bool supports_two_point_target_temperature_{false}; bool supports_two_point_target_temperature_{false};
bool supports_target_humidity_{false};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
bool supports_action_{false}; bool supports_action_{false};
std::set<climate::ClimateFanMode> supported_fan_modes_; std::set<climate::ClimateFanMode> supported_fan_modes_;
@ -190,6 +205,8 @@ class ClimateTraits {
float visual_max_temperature_{30}; float visual_max_temperature_{30};
float visual_target_temperature_step_{0.1}; float visual_target_temperature_step_{0.1};
float visual_current_temperature_step_{0.1}; float visual_current_temperature_step_{0.1};
float visual_min_humidity_{30};
float visual_max_humidity_{99};
}; };
} // namespace climate } // namespace climate

View file

@ -17,9 +17,12 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
auto traits = this->device_->get_traits(); auto traits = this->device_->get_traits();
// current_temperature_topic // current_temperature_topic
if (traits.get_supports_current_temperature()) { if (traits.get_supports_current_temperature()) {
// current_temperature_topic
root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic(); root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic();
} }
// current_humidity_topic
if (traits.get_supports_current_humidity()) {
root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic();
}
// mode_command_topic // mode_command_topic
root[MQTT_MODE_COMMAND_TOPIC] = this->get_mode_command_topic(); root[MQTT_MODE_COMMAND_TOPIC] = this->get_mode_command_topic();
// mode_state_topic // mode_state_topic
@ -57,6 +60,13 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic(); root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic();
} }
if (traits.get_supports_target_humidity()) {
// target_humidity_command_topic
root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic();
// target_humidity_state_topic
root[MQTT_TARGET_HUMIDITY_STATE_TOPIC] = this->get_target_humidity_state_topic();
}
// min_temp // min_temp
root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature(); root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature();
// max_temp // max_temp
@ -66,6 +76,11 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// temperature units are always coerced to Celsius internally // temperature units are always coerced to Celsius internally
root[MQTT_TEMPERATURE_UNIT] = "C"; root[MQTT_TEMPERATURE_UNIT] = "C";
// min_humidity
root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity();
// max_humidity
root[MQTT_MAX_HUMIDITY] = traits.get_visual_max_humidity();
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
// preset_mode_command_topic // preset_mode_command_topic
root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic(); root[MQTT_PRESET_MODE_COMMAND_TOPIC] = this->get_preset_command_topic();
@ -192,6 +207,20 @@ void MQTTClimateComponent::setup() {
}); });
} }
if (traits.get_supports_target_humidity()) {
this->subscribe(this->get_target_humidity_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
return;
}
auto call = this->device_->make_call();
call.set_target_humidity(*val);
call.perform();
});
}
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) { this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) {
auto call = this->device_->make_call(); auto call = this->device_->make_call();
@ -273,6 +302,17 @@ bool MQTTClimateComponent::publish_state_() {
success = false; success = false;
} }
if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) {
std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0);
if (!this->publish(this->get_current_humidity_state_topic(), payload))
success = false;
}
if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) {
std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0);
if (!this->publish(this->get_target_humidity_state_topic(), payload))
success = false;
}
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
std::string payload; std::string payload;
if (this->device_->preset.has_value()) { if (this->device_->preset.has_value()) {

View file

@ -20,6 +20,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent {
void setup() override; void setup() override;
MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state) MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state)
MQTT_COMPONENT_CUSTOM_TOPIC(current_humidity, state)
MQTT_COMPONENT_CUSTOM_TOPIC(mode, state) MQTT_COMPONENT_CUSTOM_TOPIC(mode, state)
MQTT_COMPONENT_CUSTOM_TOPIC(mode, command) MQTT_COMPONENT_CUSTOM_TOPIC(mode, command)
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature, state) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature, state)
@ -28,6 +29,8 @@ class MQTTClimateComponent : public mqtt::MQTTComponent {
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_low, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_low, command)
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, state) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, state)
MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, command) MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, command)
MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, state)
MQTT_COMPONENT_CUSTOM_TOPIC(target_humidity, command)
MQTT_COMPONENT_CUSTOM_TOPIC(away, state) MQTT_COMPONENT_CUSTOM_TOPIC(away, state)
MQTT_COMPONENT_CUSTOM_TOPIC(away, command) MQTT_COMPONENT_CUSTOM_TOPIC(away, command)
MQTT_COMPONENT_CUSTOM_TOPIC(action, state) MQTT_COMPONENT_CUSTOM_TOPIC(action, state)

View file

@ -51,6 +51,8 @@ constexpr const char *const MQTT_CODE_ARM_REQUIRED = "cod_arm_req";
constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "cod_dis_req"; constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "cod_dis_req";
constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t";
constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl";
constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "curr_hum_t";
constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "curr_hum_tpl";
constexpr const char *const MQTT_DEVICE = "dev"; constexpr const char *const MQTT_DEVICE = "dev";
constexpr const char *const MQTT_DEVICE_CLASS = "dev_cla"; constexpr const char *const MQTT_DEVICE_CLASS = "dev_cla";
constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t"; constexpr const char *const MQTT_DOCKED_TOPIC = "dock_t";
@ -305,6 +307,8 @@ constexpr const char *const MQTT_CODE_ARM_REQUIRED = "code_arm_required";
constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "code_disarm_required"; constexpr const char *const MQTT_CODE_DISARM_REQUIRED = "code_disarm_required";
constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic";
constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template";
constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic";
constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template";
constexpr const char *const MQTT_DEVICE = "device"; constexpr const char *const MQTT_DEVICE = "device";
constexpr const char *const MQTT_DEVICE_CLASS = "device_class"; constexpr const char *const MQTT_DEVICE_CLASS = "device_class";
constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic"; constexpr const char *const MQTT_DOCKED_TOPIC = "docked_topic";

View file

@ -2,7 +2,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.components import climate, sensor, output from esphome.components import climate, sensor, output
from esphome.const import CONF_ID, CONF_SENSOR from esphome.const import CONF_HUMIDITY_SENSOR, CONF_ID, CONF_SENSOR
pid_ns = cg.esphome_ns.namespace("pid") pid_ns = cg.esphome_ns.namespace("pid")
PIDClimate = pid_ns.class_("PIDClimate", climate.Climate, cg.Component) PIDClimate = pid_ns.class_("PIDClimate", climate.Climate, cg.Component)
@ -45,6 +45,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(PIDClimate), cv.GenerateID(): cv.declare_id(PIDClimate),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature,
cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput),
cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput),
@ -86,6 +87,10 @@ async def to_code(config):
sens = await cg.get_variable(config[CONF_SENSOR]) sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))
if CONF_HUMIDITY_SENSOR in config:
sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR])
cg.add(var.set_humidity_sensor(sens))
if CONF_COOL_OUTPUT in config: if CONF_COOL_OUTPUT in config:
out = await cg.get_variable(config[CONF_COOL_OUTPUT]) out = await cg.get_variable(config[CONF_COOL_OUTPUT])
cg.add(var.set_cool_output(out)) cg.add(var.set_cool_output(out))

View file

@ -14,6 +14,16 @@ void PIDClimate::setup() {
this->update_pid_(); this->update_pid_();
}); });
this->current_temperature = this->sensor_->state; this->current_temperature = this->sensor_->state;
// register for humidity values and get initial state
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->current_humidity = state;
this->publish_state();
});
this->current_humidity = this->humidity_sensor_->state;
}
// restore set points // restore set points
auto restore = this->restore_state_(); auto restore = this->restore_state_();
if (restore.has_value()) { if (restore.has_value()) {
@ -47,6 +57,9 @@ climate::ClimateTraits PIDClimate::traits() {
traits.set_supports_current_temperature(true); traits.set_supports_current_temperature(true);
traits.set_supports_two_point_target_temperature(false); traits.set_supports_two_point_target_temperature(false);
if (this->humidity_sensor_ != nullptr)
traits.set_supports_current_humidity(true);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
if (supports_cool_()) if (supports_cool_())
traits.add_supported_mode(climate::CLIMATE_MODE_COOL); traits.add_supported_mode(climate::CLIMATE_MODE_COOL);

View file

@ -19,6 +19,7 @@ class PIDClimate : public climate::Climate, public Component {
void dump_config() override; void dump_config() override;
void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; } void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; }
void set_humidity_sensor(sensor::Sensor *sensor) { humidity_sensor_ = sensor; }
void set_cool_output(output::FloatOutput *cool_output) { cool_output_ = cool_output; } void set_cool_output(output::FloatOutput *cool_output) { cool_output_ = cool_output; }
void set_heat_output(output::FloatOutput *heat_output) { heat_output_ = heat_output; } void set_heat_output(output::FloatOutput *heat_output) { heat_output_ = heat_output; }
void set_kp(float kp) { controller_.kp_ = kp; } void set_kp(float kp) { controller_.kp_ = kp; }
@ -85,6 +86,8 @@ class PIDClimate : public climate::Climate, public Component {
/// The sensor used for getting the current temperature /// The sensor used for getting the current temperature
sensor::Sensor *sensor_; sensor::Sensor *sensor_;
/// The sensor used for getting the current humidity
sensor::Sensor *humidity_sensor_{nullptr};
output::FloatOutput *cool_output_{nullptr}; output::FloatOutput *cool_output_{nullptr};
output::FloatOutput *heat_output_{nullptr}; output::FloatOutput *heat_output_{nullptr};
PIDController controller_; PIDController controller_;

View file

@ -35,6 +35,7 @@ from esphome.const import (
CONF_HEAT_DEADBAND, CONF_HEAT_DEADBAND,
CONF_HEAT_MODE, CONF_HEAT_MODE,
CONF_HEAT_OVERRUN, CONF_HEAT_OVERRUN,
CONF_HUMIDITY_SENSOR,
CONF_ID, CONF_ID,
CONF_IDLE_ACTION, CONF_IDLE_ACTION,
CONF_MAX_COOLING_RUN_TIME, CONF_MAX_COOLING_RUN_TIME,
@ -519,6 +520,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(ThermostatClimate), cv.GenerateID(): cv.declare_id(ThermostatClimate),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True),
cv.Optional( cv.Optional(
@ -658,6 +660,10 @@ async def to_code(config):
) )
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))
if CONF_HUMIDITY_SENSOR in config:
sens = await cg.get_variable(config[CONF_HUMIDITY_SENSOR])
cg.add(var.set_humidity_sensor(sens))
cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND])) cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND]))
cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN])) cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN]))
cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND]))

View file

@ -27,6 +27,15 @@ void ThermostatClimate::setup() {
}); });
this->current_temperature = this->sensor_->state; this->current_temperature = this->sensor_->state;
// register for humidity values and get initial state
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->current_humidity = state;
this->publish_state();
});
this->current_humidity = this->humidity_sensor_->state;
}
auto use_default_preset = true; auto use_default_preset = true;
if (this->on_boot_restore_from_ == thermostat::OnBootRestoreFrom::MEMORY) { if (this->on_boot_restore_from_ == thermostat::OnBootRestoreFrom::MEMORY) {
@ -217,6 +226,9 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
climate::ClimateTraits ThermostatClimate::traits() { climate::ClimateTraits ThermostatClimate::traits() {
auto traits = climate::ClimateTraits(); auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true); traits.set_supports_current_temperature(true);
if (this->humidity_sensor_ != nullptr)
traits.set_supports_current_humidity(true);
if (supports_auto_) if (supports_auto_)
traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); traits.add_supported_mode(climate::CLIMATE_MODE_AUTO);
if (supports_heat_cool_) if (supports_heat_cool_)
@ -1169,6 +1181,9 @@ void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) {
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
} }
void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) {
this->humidity_sensor_ = humidity_sensor;
}
void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; }
void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) {
this->supports_heat_cool_ = supports_heat_cool; this->supports_heat_cool_ = supports_heat_cool;

View file

@ -81,6 +81,7 @@ class ThermostatClimate : public climate::Climate, public Component {
void set_heating_minimum_run_time_in_sec(uint32_t time); void set_heating_minimum_run_time_in_sec(uint32_t time);
void set_idle_minimum_time_in_sec(uint32_t time); void set_idle_minimum_time_in_sec(uint32_t time);
void set_sensor(sensor::Sensor *sensor); void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
void set_use_startup_delay(bool use_startup_delay); void set_use_startup_delay(bool use_startup_delay);
void set_supports_auto(bool supports_auto); void set_supports_auto(bool supports_auto);
void set_supports_heat_cool(bool supports_heat_cool); void set_supports_heat_cool(bool supports_heat_cool);
@ -238,6 +239,8 @@ class ThermostatClimate : public climate::Climate, public Component {
/// The sensor used for getting the current temperature /// The sensor used for getting the current temperature
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
/// The sensor used for getting the current humidity
sensor::Sensor *humidity_sensor_{nullptr};
/// Whether the controller supports auto/cooling/drying/fanning/heating. /// Whether the controller supports auto/cooling/drying/fanning/heating.
/// ///

View file

@ -157,6 +157,7 @@ CONF_CS_PIN = "cs_pin"
CONF_CSS_INCLUDE = "css_include" CONF_CSS_INCLUDE = "css_include"
CONF_CSS_URL = "css_url" CONF_CSS_URL = "css_url"
CONF_CURRENT = "current" CONF_CURRENT = "current"
CONF_CURRENT_HUMIDITY_STATE_TOPIC = "current_humidity_state_topic"
CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_OPERATION = "current_operation"
CONF_CURRENT_RESISTOR = "current_resistor" CONF_CURRENT_RESISTOR = "current_resistor"
CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic"
@ -324,6 +325,7 @@ CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference"
CONF_HOUR = "hour" CONF_HOUR = "hour"
CONF_HOURS = "hours" CONF_HOURS = "hours"
CONF_HUMIDITY = "humidity" CONF_HUMIDITY = "humidity"
CONF_HUMIDITY_SENSOR = "humidity_sensor"
CONF_HYSTERESIS = "hysteresis" CONF_HYSTERESIS = "hysteresis"
CONF_I2C = "i2c" CONF_I2C = "i2c"
CONF_I2C_ID = "i2c_id" CONF_I2C_ID = "i2c_id"
@ -758,6 +760,8 @@ CONF_SYNC = "sync"
CONF_TABLET = "tablet" CONF_TABLET = "tablet"
CONF_TAG = "tag" CONF_TAG = "tag"
CONF_TARGET = "target" CONF_TARGET = "target"
CONF_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic"
CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic"
CONF_TARGET_TEMPERATURE = "target_temperature" CONF_TARGET_TEMPERATURE = "target_temperature"
CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action" CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action"
CONF_TARGET_TEMPERATURE_COMMAND_TOPIC = "target_temperature_command_topic" CONF_TARGET_TEMPERATURE_COMMAND_TOPIC = "target_temperature_command_topic"

View file

@ -899,6 +899,7 @@ climate:
- platform: bang_bang - platform: bang_bang
name: Bang Bang Climate name: Bang Bang Climate
sensor: ha_hello_world sensor: ha_hello_world
humidity_sensor: ha_hello_world
default_target_temperature_low: 18°C default_target_temperature_low: 18°C
default_target_temperature_high: 24°C default_target_temperature_high: 24°C
idle_action: idle_action:
@ -913,6 +914,7 @@ climate:
- platform: thermostat - platform: thermostat
name: Thermostat Climate name: Thermostat Climate
sensor: ha_hello_world sensor: ha_hello_world
humidity_sensor: ha_hello_world
preset: preset:
- name: Default Preset - name: Default Preset
default_target_temperature_low: 18°C default_target_temperature_low: 18°C
@ -1000,6 +1002,7 @@ climate:
id: pid_climate id: pid_climate
name: PID Climate Controller name: PID Climate Controller
sensor: ha_hello_world sensor: ha_hello_world
humidity_sensor: ha_hello_world
default_target_temperature: 21°C default_target_temperature: 21°C
heat_output: my_slow_pwm heat_output: my_slow_pwm
control_parameters: control_parameters: