mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 14:55:05 +00:00
[climate] Store custom mode vectors on Climate entity to eliminate heap allocation (#15206)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
"""Legacy climate component — tests deprecated ClimateTraits setters backward compat.
|
||||
|
||||
Remove this entire directory in 2026.11.0 when the deprecated setters are removed.
|
||||
"""
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Legacy climate platform that uses deprecated ClimateTraits setters."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
legacy_climate_ns = cg.esphome_ns.namespace("legacy_climate_test")
|
||||
LegacyClimate = legacy_climate_ns.class_("LegacyClimate", climate.Climate, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = climate.climate_schema(LegacyClimate).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate.new_climate(config)
|
||||
await cg.register_component(var, config)
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate/climate.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::legacy_climate_test {
|
||||
|
||||
/// Test climate that uses the DEPRECATED ClimateTraits setters for custom modes.
|
||||
/// This validates backward compatibility for external components that haven't migrated.
|
||||
class LegacyClimate : public climate::Climate, public Component {
|
||||
public:
|
||||
void setup() override {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
this->target_temperature = 22.0f;
|
||||
this->current_temperature = 20.0f;
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
protected:
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_COOL});
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(30.0f);
|
||||
traits.set_visual_temperature_step(0.5f);
|
||||
|
||||
// DEPRECATED API: setting custom modes directly on ClimateTraits.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
traits.set_supported_custom_fan_modes({"Turbo", "Silent", "Auto"});
|
||||
traits.set_supported_custom_presets({"Eco Mode", "Night Mode"});
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
return traits;
|
||||
}
|
||||
|
||||
void control(const climate::ClimateCall &call) override {
|
||||
if (call.get_mode().has_value()) {
|
||||
this->mode = *call.get_mode();
|
||||
}
|
||||
if (call.get_target_temperature().has_value()) {
|
||||
this->target_temperature = *call.get_target_temperature();
|
||||
}
|
||||
if (call.has_custom_fan_mode()) {
|
||||
this->set_custom_fan_mode_(call.get_custom_fan_mode());
|
||||
}
|
||||
if (call.has_custom_preset()) {
|
||||
this->set_custom_preset_(call.get_custom_preset());
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::legacy_climate_test
|
||||
18
tests/integration/fixtures/legacy_climate_compat.yaml
Normal file
18
tests/integration/fixtures/legacy_climate_compat.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: legacy-climate-compat
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
components: [legacy_climate_component]
|
||||
|
||||
climate:
|
||||
- platform: legacy_climate_component
|
||||
name: "Legacy Climate"
|
||||
id: legacy_climate
|
||||
96
tests/integration/test_legacy_climate_compat.py
Normal file
96
tests/integration/test_legacy_climate_compat.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Integration test for backward compatibility of deprecated ClimateTraits setters.
|
||||
|
||||
Verifies that external components using the old traits.set_supported_custom_fan_modes()
|
||||
and traits.set_supported_custom_presets() API still work correctly during the
|
||||
deprecation period.
|
||||
|
||||
Remove this entire test file and the legacy_climate_component external component
|
||||
in 2026.11.0 when the deprecated ClimateTraits setters are removed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import ClimateInfo
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_climate_compat(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that deprecated ClimateTraits custom mode setters still work end-to-end."""
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
|
||||
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
|
||||
assert len(climate_infos) == 1, (
|
||||
f"Expected 1 climate entity, got {len(climate_infos)}"
|
||||
)
|
||||
|
||||
test_climate = climate_infos[0]
|
||||
|
||||
# Verify custom fan modes set via deprecated ClimateTraits setter are exposed
|
||||
assert set(test_climate.supported_custom_fan_modes) == {
|
||||
"Turbo",
|
||||
"Silent",
|
||||
"Auto",
|
||||
}, (
|
||||
f"Expected custom fan modes {{Turbo, Silent, Auto}}, "
|
||||
f"got {test_climate.supported_custom_fan_modes}"
|
||||
)
|
||||
|
||||
# Verify custom presets set via deprecated ClimateTraits setter are exposed
|
||||
assert set(test_climate.supported_custom_presets) == {
|
||||
"Eco Mode",
|
||||
"Night Mode",
|
||||
}, (
|
||||
f"Expected custom presets {{Eco Mode, Night Mode}}, "
|
||||
f"got {test_climate.supported_custom_presets}"
|
||||
)
|
||||
|
||||
# Set up state tracking with InitialStateHelper
|
||||
turbo_future: asyncio.Future[aioesphomeapi.ClimateState] = loop.create_future()
|
||||
|
||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||
if (
|
||||
isinstance(state, aioesphomeapi.ClimateState)
|
||||
and state.custom_fan_mode == "Turbo"
|
||||
and not turbo_future.done()
|
||||
):
|
||||
turbo_future.set_result(state)
|
||||
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Verify we can set a custom fan mode via API (tests find_custom_fan_mode_ compat path)
|
||||
client.climate_command(test_climate.key, custom_fan_mode="Turbo")
|
||||
|
||||
try:
|
||||
turbo_state = await asyncio.wait_for(turbo_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Custom fan mode 'Turbo' not received within 5 seconds")
|
||||
|
||||
assert turbo_state.custom_fan_mode == "Turbo"
|
||||
Reference in New Issue
Block a user