mirror of
https://github.com/esphome/esphome.git
synced 2026-09-27 06:50:22 +00:00
[climate_ir] Only advertise HEAT_COOL when heat and cool are both supported (#19483)
This commit is contained in:
@@ -14,6 +14,8 @@ from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SUPPORTS_HEAT_COOL = "supports_heat_cool"
|
||||
|
||||
DEPENDENCIES = ["remote_transmitter"]
|
||||
AUTO_LOAD = ["sensor", "remote_base"]
|
||||
CODEOWNERS = ["@glmnet"]
|
||||
@@ -37,6 +39,7 @@ def climate_ir_schema(
|
||||
{
|
||||
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
|
||||
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_SUPPORTS_HEAT_COOL): cv.boolean,
|
||||
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
|
||||
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
|
||||
}
|
||||
@@ -61,8 +64,13 @@ def climate_ir_with_receiver_schema(
|
||||
async def register_climate_ir(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
await remote_base.register_transmittable(var, config)
|
||||
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
|
||||
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
|
||||
supports_cool = config[CONF_SUPPORTS_COOL]
|
||||
supports_heat = config[CONF_SUPPORTS_HEAT]
|
||||
cg.add(var.set_supports_cool(supports_cool))
|
||||
cg.add(var.set_supports_heat(supports_heat))
|
||||
# The header default is true, so only the false case needs a call.
|
||||
if not config.get(CONF_SUPPORTS_HEAT_COOL, supports_cool and supports_heat):
|
||||
cg.add(var.set_supports_heat_cool(False))
|
||||
if remote_base.CONF_RECEIVER_ID in config:
|
||||
await remote_base.register_listener(var, config)
|
||||
if sensor_id := config.get(CONF_SENSOR):
|
||||
|
||||
@@ -13,11 +13,13 @@ climate::ClimateTraits ClimateIR::traits() {
|
||||
if (this->humidity_sensor_ != nullptr) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
|
||||
}
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF});
|
||||
if (this->supports_cool_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
|
||||
if (this->supports_heat_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
|
||||
if (this->supports_heat_cool_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
|
||||
if (this->supports_dry_)
|
||||
traits.add_supported_mode(climate::CLIMATE_MODE_DRY);
|
||||
if (this->supports_fan_only_)
|
||||
@@ -94,9 +96,10 @@ void ClimateIR::dump_config() {
|
||||
" Min. Temperature: %.1f°C\n"
|
||||
" Max. Temperature: %.1f°C\n"
|
||||
" Supports HEAT: %s\n"
|
||||
" Supports COOL: %s",
|
||||
" Supports COOL: %s\n"
|
||||
" Supports HEAT_COOL: %s",
|
||||
this->minimum_temperature_, this->maximum_temperature_, YESNO(this->supports_heat_),
|
||||
YESNO(this->supports_cool_));
|
||||
YESNO(this->supports_cool_), YESNO(this->supports_heat_cool_));
|
||||
}
|
||||
|
||||
} // namespace esphome::climate_ir
|
||||
|
||||
@@ -41,6 +41,7 @@ class ClimateIR : public Component,
|
||||
void dump_config() override;
|
||||
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
void set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; }
|
||||
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
|
||||
|
||||
@@ -60,6 +61,8 @@ class ClimateIR : public Component,
|
||||
|
||||
bool supports_cool_{true};
|
||||
bool supports_heat_{true};
|
||||
// Default (supports_cool && supports_heat) is resolved during code generation.
|
||||
bool supports_heat_cool_{true};
|
||||
bool supports_dry_{false};
|
||||
bool supports_fan_only_{false};
|
||||
climate::ClimateFanModeMask fan_modes_{};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_heat: false
|
||||
@@ -0,0 +1,16 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_heat: false
|
||||
supports_heat_cool: true
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_heat_cool: false
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_cool: false
|
||||
@@ -0,0 +1,16 @@
|
||||
esphome:
|
||||
name: climateir-heatcool
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
remote_transmitter:
|
||||
pin: GPIO5
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
id: test_coolix
|
||||
name: Coolix
|
||||
supports_heat: false
|
||||
supports_cool: false
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Tests for the supports_heat_cool default resolved in climate_ir code generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _emitted_value(main_cpp: str) -> str | None:
|
||||
"""Return the argument of the generated set_supports_heat_cool() call, or None if absent."""
|
||||
match = re.search(r"set_supports_heat_cool\((true|false)\)", main_cpp)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "expected"),
|
||||
[
|
||||
("heat_and_cool.yaml", None),
|
||||
("cool_only.yaml", "false"),
|
||||
("heat_only.yaml", "false"),
|
||||
("neither.yaml", "false"),
|
||||
],
|
||||
)
|
||||
def test_default_requires_heat_and_cool(
|
||||
config: str,
|
||||
expected: str | None,
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without the key, HEAT_COOL follows supports_heat and supports_cool."""
|
||||
main_cpp = generate_main(component_config_path(config))
|
||||
assert _emitted_value(main_cpp) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "expected"),
|
||||
[
|
||||
("cool_only_override_on.yaml", None),
|
||||
("heat_and_cool_override_off.yaml", "false"),
|
||||
],
|
||||
)
|
||||
def test_explicit_key_overrides_default(
|
||||
config: str,
|
||||
expected: str | None,
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A cool-only unit can still offer HEAT_COOL, and a heat+cool unit can drop it."""
|
||||
main_cpp = generate_main(component_config_path(config))
|
||||
assert _emitted_value(main_cpp) == expected
|
||||
@@ -0,0 +1,6 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# ClimateIR derives from climate::Climate without declaring it as a dependency.
|
||||
manifest.dependencies = manifest.dependencies + ["climate"]
|
||||
@@ -0,0 +1,56 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome::climate_ir::testing {
|
||||
|
||||
class TestClimateIR : public ClimateIR {
|
||||
public:
|
||||
TestClimateIR() : ClimateIR(16.0f, 30.0f) {}
|
||||
|
||||
using ClimateIR::traits;
|
||||
|
||||
protected:
|
||||
void transmit_state() override {}
|
||||
};
|
||||
|
||||
// The HEAT_COOL default is covered in tests/component_tests/climate_ir.
|
||||
|
||||
TEST(ClimateIRTest, HeatCoolAdvertisedWhenSupported) {
|
||||
TestClimateIR climate;
|
||||
climate.set_supports_heat(true);
|
||||
climate.set_supports_cool(true);
|
||||
climate.set_supports_heat_cool(true);
|
||||
EXPECT_TRUE(climate.traits().supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
|
||||
}
|
||||
|
||||
TEST(ClimateIRTest, HeatCoolNotAdvertisedWhenUnsupported) {
|
||||
TestClimateIR climate;
|
||||
climate.set_supports_heat(true);
|
||||
climate.set_supports_cool(true);
|
||||
climate.set_supports_heat_cool(false);
|
||||
EXPECT_FALSE(climate.traits().supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
|
||||
}
|
||||
|
||||
TEST(ClimateIRTest, HeatCoolAdvertisedForCoolOnlyDeviceThatSupportsIt) {
|
||||
TestClimateIR climate;
|
||||
climate.set_supports_heat(false);
|
||||
climate.set_supports_cool(true);
|
||||
climate.set_supports_heat_cool(true);
|
||||
auto traits = climate.traits();
|
||||
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
|
||||
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_HEAT));
|
||||
}
|
||||
|
||||
TEST(ClimateIRTest, HeatAndCoolModesFollowTheirOwnFlags) {
|
||||
TestClimateIR climate;
|
||||
climate.set_supports_heat(false);
|
||||
climate.set_supports_cool(true);
|
||||
climate.set_supports_heat_cool(false);
|
||||
auto traits = climate.traits();
|
||||
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_COOL));
|
||||
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_HEAT));
|
||||
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_HEAT_COOL));
|
||||
EXPECT_TRUE(traits.supports_mode(climate::CLIMATE_MODE_OFF));
|
||||
}
|
||||
|
||||
} // namespace esphome::climate_ir::testing
|
||||
@@ -0,0 +1,10 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
climate:
|
||||
- platform: gree
|
||||
name: GREE
|
||||
transmitter_id: xmitr
|
||||
model: YAN
|
||||
supports_heat: false
|
||||
supports_heat_cool: true
|
||||
@@ -0,0 +1,9 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
climate:
|
||||
- platform: gree
|
||||
name: GREE
|
||||
transmitter_id: xmitr
|
||||
model: YAN
|
||||
supports_cool: false
|
||||
@@ -0,0 +1,9 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
climate:
|
||||
- platform: gree
|
||||
name: GREE
|
||||
transmitter_id: xmitr
|
||||
model: YAN
|
||||
supports_heat_cool: false
|
||||
@@ -0,0 +1,9 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
climate:
|
||||
- platform: gree
|
||||
name: GREE
|
||||
transmitter_id: xmitr
|
||||
model: YAN
|
||||
supports_heat: false
|
||||
Reference in New Issue
Block a user