mirror of
https://github.com/esphome/esphome.git
synced 2025-11-10 11:55:52 +00:00
Compare commits
10 Commits
integratio
...
climate_st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d43d23a4c7 | ||
|
|
7fef54ba79 | ||
|
|
68caee3226 | ||
|
|
9687ba3c36 | ||
|
|
e2b3dec9d9 | ||
|
|
ca6e8e0cc5 | ||
|
|
c87b53a666 | ||
|
|
e1b6f84348 | ||
|
|
0433a84202 | ||
|
|
0d8ce7fdc9 |
@@ -9,6 +9,8 @@ from esphome.const import (
|
|||||||
CONF_COOL_DEADBAND,
|
CONF_COOL_DEADBAND,
|
||||||
CONF_COOL_MODE,
|
CONF_COOL_MODE,
|
||||||
CONF_COOL_OVERRUN,
|
CONF_COOL_OVERRUN,
|
||||||
|
CONF_CUSTOM_FAN_MODES,
|
||||||
|
CONF_CUSTOM_PRESETS,
|
||||||
CONF_DEFAULT_MODE,
|
CONF_DEFAULT_MODE,
|
||||||
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH,
|
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH,
|
||||||
CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
|
CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
|
||||||
@@ -658,6 +660,8 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA),
|
cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA),
|
||||||
|
cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(cv.string_strict),
|
||||||
|
cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(cv.string_strict),
|
||||||
cv.Optional(CONF_ON_BOOT_RESTORE_FROM): validate_on_boot_restore_from,
|
cv.Optional(CONF_ON_BOOT_RESTORE_FROM): validate_on_boot_restore_from,
|
||||||
cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation(
|
cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation(
|
||||||
single=True
|
single=True
|
||||||
@@ -1008,3 +1012,22 @@ async def to_code(config):
|
|||||||
await automation.build_automation(
|
await automation.build_automation(
|
||||||
var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE]
|
var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Collect custom preset names from preset map (non-standard) and custom_presets list
|
||||||
|
custom_preset_names = [
|
||||||
|
preset_config[CONF_NAME]
|
||||||
|
for preset_config in config.get(CONF_PRESET, [])
|
||||||
|
if preset_config[CONF_NAME].upper() not in climate.CLIMATE_PRESETS
|
||||||
|
]
|
||||||
|
custom_preset_names.extend(config.get(CONF_CUSTOM_PRESETS, []))
|
||||||
|
if custom_preset_names:
|
||||||
|
cg.add(var.set_custom_presets(custom_preset_names))
|
||||||
|
|
||||||
|
# Collect custom fan modes (filter out standard enum fan modes)
|
||||||
|
custom_fan_modes = [
|
||||||
|
mode
|
||||||
|
for mode in config.get(CONF_CUSTOM_FAN_MODES, [])
|
||||||
|
if mode.upper() not in climate.CLIMATE_FAN_MODES
|
||||||
|
]
|
||||||
|
if custom_fan_modes:
|
||||||
|
cg.add(var.set_custom_fan_modes(custom_fan_modes))
|
||||||
|
|||||||
@@ -321,8 +321,12 @@ climate::ClimateTraits ThermostatClimate::traits() {
|
|||||||
for (auto &it : this->preset_config_) {
|
for (auto &it : this->preset_config_) {
|
||||||
traits.add_supported_preset(it.first);
|
traits.add_supported_preset(it.first);
|
||||||
}
|
}
|
||||||
for (auto &it : this->custom_preset_config_) {
|
// Custom presets and fan modes are set directly from Python (includes both map entries and additional lists)
|
||||||
traits.add_supported_custom_preset(it.first);
|
if (!this->additional_custom_presets_.empty()) {
|
||||||
|
traits.set_supported_custom_presets(this->additional_custom_presets_);
|
||||||
|
}
|
||||||
|
if (!this->additional_custom_fan_modes_.empty()) {
|
||||||
|
traits.set_supported_custom_fan_modes(this->additional_custom_fan_modes_);
|
||||||
}
|
}
|
||||||
return traits;
|
return traits;
|
||||||
}
|
}
|
||||||
@@ -1248,6 +1252,14 @@ void ThermostatClimate::set_custom_preset_config(const std::string &name,
|
|||||||
this->custom_preset_config_[name] = config;
|
this->custom_preset_config_[name] = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ThermostatClimate::set_custom_fan_modes(std::initializer_list<const char *> custom_fan_modes) {
|
||||||
|
this->additional_custom_fan_modes_ = custom_fan_modes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThermostatClimate::set_custom_presets(std::initializer_list<const char *> custom_presets) {
|
||||||
|
this->additional_custom_presets_ = custom_presets;
|
||||||
|
}
|
||||||
|
|
||||||
ThermostatClimate::ThermostatClimate()
|
ThermostatClimate::ThermostatClimate()
|
||||||
: cool_action_trigger_(new Trigger<>()),
|
: cool_action_trigger_(new Trigger<>()),
|
||||||
supplemental_cool_action_trigger_(new Trigger<>()),
|
supplemental_cool_action_trigger_(new Trigger<>()),
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ class ThermostatClimate : public climate::Climate, public Component {
|
|||||||
|
|
||||||
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
|
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
|
||||||
void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config);
|
void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config);
|
||||||
|
void set_custom_fan_modes(std::initializer_list<const char *> custom_fan_modes);
|
||||||
|
void set_custom_presets(std::initializer_list<const char *> custom_presets);
|
||||||
|
|
||||||
Trigger<> *get_cool_action_trigger() const;
|
Trigger<> *get_cool_action_trigger() const;
|
||||||
Trigger<> *get_supplemental_cool_action_trigger() const;
|
Trigger<> *get_supplemental_cool_action_trigger() const;
|
||||||
@@ -537,6 +539,10 @@ class ThermostatClimate : public climate::Climate, public Component {
|
|||||||
std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{};
|
std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{};
|
||||||
/// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
|
/// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
|
||||||
std::map<std::string, ThermostatClimateTargetTempConfig> custom_preset_config_{};
|
std::map<std::string, ThermostatClimateTargetTempConfig> custom_preset_config_{};
|
||||||
|
/// Additional custom fan modes to expose (beyond those with actions)
|
||||||
|
std::vector<const char *> additional_custom_fan_modes_{};
|
||||||
|
/// Additional custom presets to expose (beyond those in custom_preset_config_)
|
||||||
|
std::vector<const char *> additional_custom_presets_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace thermostat
|
} // namespace thermostat
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ climate:
|
|||||||
- name: Away
|
- name: Away
|
||||||
default_target_temperature_low: 16°C
|
default_target_temperature_low: 16°C
|
||||||
default_target_temperature_high: 20°C
|
default_target_temperature_high: 20°C
|
||||||
|
custom_fan_modes:
|
||||||
|
- "Custom Fan 1"
|
||||||
|
- "Custom Fan 2"
|
||||||
|
custom_presets:
|
||||||
|
- "Custom Preset 1"
|
||||||
|
- "Custom Preset 2"
|
||||||
idle_action:
|
idle_action:
|
||||||
- logger.log: idle_action
|
- logger.log: idle_action
|
||||||
cool_action:
|
cool_action:
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
esphome:
|
||||||
|
name: climate-custom-modes-test
|
||||||
|
host:
|
||||||
|
api:
|
||||||
|
logger:
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: template
|
||||||
|
id: thermostat_sensor
|
||||||
|
lambda: "return 22.0;"
|
||||||
|
|
||||||
|
climate:
|
||||||
|
- platform: thermostat
|
||||||
|
id: test_thermostat
|
||||||
|
name: Test Thermostat Custom Modes
|
||||||
|
sensor: thermostat_sensor
|
||||||
|
preset:
|
||||||
|
- name: Away
|
||||||
|
default_target_temperature_low: 16°C
|
||||||
|
default_target_temperature_high: 20°C
|
||||||
|
custom_fan_modes:
|
||||||
|
- "Turbo"
|
||||||
|
- "Silent"
|
||||||
|
- "Sleep Mode"
|
||||||
|
custom_presets:
|
||||||
|
- "Eco Plus"
|
||||||
|
- "Comfort"
|
||||||
|
- "Vacation Mode"
|
||||||
|
idle_action:
|
||||||
|
- logger.log: idle_action
|
||||||
|
cool_action:
|
||||||
|
- logger.log: cool_action
|
||||||
|
heat_action:
|
||||||
|
- logger.log: heat_action
|
||||||
|
min_cooling_off_time: 10s
|
||||||
|
min_cooling_run_time: 10s
|
||||||
|
min_heating_off_time: 10s
|
||||||
|
min_heating_run_time: 10s
|
||||||
|
min_idle_time: 10s
|
||||||
51
tests/integration/test_climate_custom_modes.py
Normal file
51
tests/integration/test_climate_custom_modes.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""Integration test for climate custom fan modes and presets."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from aioesphomeapi import ClimateInfo, ClimatePreset
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_climate_custom_fan_modes_and_presets(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test that custom fan modes and presets are properly exposed via API."""
|
||||||
|
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||||
|
# Get entities and services
|
||||||
|
entities, services = await client.list_entities_services()
|
||||||
|
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
|
||||||
|
assert len(climate_infos) == 1, "Expected exactly 1 climate entity"
|
||||||
|
|
||||||
|
test_climate = climate_infos[0]
|
||||||
|
|
||||||
|
# Verify custom fan modes are exposed
|
||||||
|
custom_fan_modes = test_climate.supported_custom_fan_modes
|
||||||
|
assert len(custom_fan_modes) == 3, (
|
||||||
|
f"Expected 3 custom fan modes, got {len(custom_fan_modes)}"
|
||||||
|
)
|
||||||
|
assert "Turbo" in custom_fan_modes, "Expected 'Turbo' in custom fan modes"
|
||||||
|
assert "Silent" in custom_fan_modes, "Expected 'Silent' in custom fan modes"
|
||||||
|
assert "Sleep Mode" in custom_fan_modes, (
|
||||||
|
"Expected 'Sleep Mode' in custom fan modes"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify enum presets are exposed (from preset: config map)
|
||||||
|
assert ClimatePreset.AWAY in test_climate.supported_presets, (
|
||||||
|
"Expected AWAY in enum presets"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify custom string presets are exposed (from custom_presets: config list)
|
||||||
|
custom_presets = test_climate.supported_custom_presets
|
||||||
|
assert len(custom_presets) == 3, (
|
||||||
|
f"Expected 3 custom presets, got {len(custom_presets)}: {custom_presets}"
|
||||||
|
)
|
||||||
|
assert "Eco Plus" in custom_presets, "Expected 'Eco Plus' in custom presets"
|
||||||
|
assert "Comfort" in custom_presets, "Expected 'Comfort' in custom presets"
|
||||||
|
assert "Vacation Mode" in custom_presets, (
|
||||||
|
"Expected 'Vacation Mode' in custom presets"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user