mirror of
https://github.com/esphome/esphome.git
synced 2025-07-28 06:06:33 +00:00
[fan] fix initial FanCall to properly set speed (#8277)
This commit is contained in:
parent
f82ac34784
commit
59f69ac5ca
@ -41,39 +41,48 @@ void FanCall::perform() {
|
||||
void FanCall::validate_() {
|
||||
auto traits = this->parent_.get_traits();
|
||||
|
||||
if (this->speed_.has_value())
|
||||
if (this->speed_.has_value()) {
|
||||
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
|
||||
|
||||
if (this->binary_state_.has_value() && *this->binary_state_) {
|
||||
// when turning on, if neither current nor new speed available, set speed to 100%
|
||||
if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) {
|
||||
this->speed_ = traits.supported_speed_count();
|
||||
}
|
||||
}
|
||||
|
||||
if (this->oscillating_.has_value() && !traits.supports_oscillation()) {
|
||||
ESP_LOGW(TAG, "'%s' - This fan does not support oscillation!", this->parent_.get_name().c_str());
|
||||
this->oscillating_.reset();
|
||||
}
|
||||
|
||||
if (this->speed_.has_value() && !traits.supports_speed()) {
|
||||
ESP_LOGW(TAG, "'%s' - This fan does not support speeds!", this->parent_.get_name().c_str());
|
||||
this->speed_.reset();
|
||||
}
|
||||
|
||||
if (this->direction_.has_value() && !traits.supports_direction()) {
|
||||
ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str());
|
||||
this->direction_.reset();
|
||||
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
|
||||
// "Manually setting a speed must disable any set preset mode"
|
||||
this->preset_mode_.clear();
|
||||
}
|
||||
|
||||
if (!this->preset_mode_.empty()) {
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
|
||||
ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(),
|
||||
this->preset_mode_.c_str());
|
||||
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
|
||||
this->preset_mode_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// when turning on...
|
||||
if (!this->parent_.state && this->binary_state_.has_value() &&
|
||||
*this->binary_state_
|
||||
// ..,and no preset mode will be active...
|
||||
&& this->preset_mode_.empty() &&
|
||||
this->parent_.preset_mode.empty()
|
||||
// ...and neither current nor new speed is available...
|
||||
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
|
||||
// ...set speed to 100%
|
||||
this->speed_ = traits.supported_speed_count();
|
||||
}
|
||||
|
||||
if (this->oscillating_.has_value() && !traits.supports_oscillation()) {
|
||||
ESP_LOGW(TAG, "%s: Oscillation not supported", this->parent_.get_name().c_str());
|
||||
this->oscillating_.reset();
|
||||
}
|
||||
|
||||
if (this->speed_.has_value() && !traits.supports_speed()) {
|
||||
ESP_LOGW(TAG, "%s: Speed control not supported", this->parent_.get_name().c_str());
|
||||
this->speed_.reset();
|
||||
}
|
||||
|
||||
if (this->direction_.has_value() && !traits.supports_direction()) {
|
||||
ESP_LOGW(TAG, "%s: Direction control not supported", this->parent_.get_name().c_str());
|
||||
this->direction_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
FanCall FanRestoreState::to_call(Fan &fan) {
|
||||
|
34
tests/integration/fixtures/host_mode_fan_preset.yaml
Normal file
34
tests/integration/fixtures/host_mode_fan_preset.yaml
Normal file
@ -0,0 +1,34 @@
|
||||
esphome:
|
||||
name: host-test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
|
||||
# Test fan with preset modes and speed settings
|
||||
fan:
|
||||
- platform: template
|
||||
name: "Test Fan with Presets"
|
||||
id: test_fan_presets
|
||||
speed_count: 5
|
||||
preset_modes:
|
||||
- "Eco"
|
||||
- "Sleep"
|
||||
- "Turbo"
|
||||
has_oscillating: true
|
||||
has_direction: true
|
||||
|
||||
- platform: template
|
||||
name: "Test Fan Simple"
|
||||
id: test_fan_simple
|
||||
speed_count: 3
|
||||
has_oscillating: false
|
||||
has_direction: false
|
||||
|
||||
- platform: template
|
||||
name: "Test Fan No Speed"
|
||||
id: test_fan_no_speed
|
||||
has_oscillating: true
|
||||
has_direction: false
|
152
tests/integration/test_host_mode_fan_preset.py
Normal file
152
tests/integration/test_host_mode_fan_preset.py
Normal file
@ -0,0 +1,152 @@
|
||||
"""Integration test for fan preset mode behavior."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import FanInfo, FanState
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_host_mode_fan_preset(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test fan preset mode behavior according to Home Assistant guidelines."""
|
||||
# Write, compile and run the ESPHome device, then connect to API
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
# Get all fan entities
|
||||
entities = await client.list_entities_services()
|
||||
fans: list[FanInfo] = []
|
||||
for entity_list in entities:
|
||||
for entity in entity_list:
|
||||
if isinstance(entity, FanInfo):
|
||||
fans.append(entity)
|
||||
|
||||
# Create a map of fan names to entity info
|
||||
fan_map = {fan.name: fan for fan in fans}
|
||||
|
||||
# Verify we have our test fans
|
||||
assert "Test Fan with Presets" in fan_map
|
||||
assert "Test Fan Simple" in fan_map
|
||||
assert "Test Fan No Speed" in fan_map
|
||||
|
||||
# Get fan with presets
|
||||
fan_presets = fan_map["Test Fan with Presets"]
|
||||
assert fan_presets.supports_speed is True
|
||||
assert fan_presets.supported_speed_count == 5
|
||||
assert fan_presets.supports_oscillation is True
|
||||
assert fan_presets.supports_direction is True
|
||||
assert set(fan_presets.supported_preset_modes) == {"Eco", "Sleep", "Turbo"}
|
||||
|
||||
# Subscribe to states
|
||||
states: dict[int, FanState] = {}
|
||||
state_event = asyncio.Event()
|
||||
|
||||
def on_state(state: FanState) -> None:
|
||||
if isinstance(state, FanState):
|
||||
states[state.key] = state
|
||||
state_event.set()
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Test 1: Turn on fan without speed or preset - should set speed to 100%
|
||||
state_event.clear()
|
||||
client.fan_command(
|
||||
key=fan_presets.key,
|
||||
state=True,
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=2.0)
|
||||
|
||||
fan_state = states[fan_presets.key]
|
||||
assert fan_state.state is True
|
||||
assert fan_state.speed_level == 5 # Should be max speed (100%)
|
||||
assert fan_state.preset_mode == ""
|
||||
|
||||
# Turn off
|
||||
state_event.clear()
|
||||
client.fan_command(
|
||||
key=fan_presets.key,
|
||||
state=False,
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=2.0)
|
||||
|
||||
# Test 2: Turn on fan with preset mode - should NOT set speed to 100%
|
||||
state_event.clear()
|
||||
client.fan_command(
|
||||
key=fan_presets.key,
|
||||
state=True,
|
||||
preset_mode="Eco",
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=2.0)
|
||||
|
||||
fan_state = states[fan_presets.key]
|
||||
assert fan_state.state is True
|
||||
assert fan_state.preset_mode == "Eco"
|
||||
# Speed should be whatever the preset sets, not forced to 100%
|
||||
|
||||
# Test 3: Setting speed should clear preset mode
|
||||
state_event.clear()
|
||||
client.fan_command(
|
||||
key=fan_presets.key,
|
||||
speed_level=3,
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=2.0)
|
||||
|
||||
fan_state = states[fan_presets.key]
|
||||
assert fan_state.state is True
|
||||
assert fan_state.speed_level == 3
|
||||
assert fan_state.preset_mode == "" # Preset mode should be cleared
|
||||
|
||||
# Test 4: Setting preset mode should work when fan is already on
|
||||
state_event.clear()
|
||||
client.fan_command(
|
||||
key=fan_presets.key,
|
||||
preset_mode="Sleep",
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=2.0)
|
||||
|
||||
fan_state = states[fan_presets.key]
|
||||
assert fan_state.state is True
|
||||
assert fan_state.preset_mode == "Sleep"
|
||||
|
||||
# Turn off
|
||||
state_event.clear()
|
||||
client.fan_command(
|
||||
key=fan_presets.key,
|
||||
state=False,
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=2.0)
|
||||
|
||||
# Test 5: Turn on fan with specific speed
|
||||
state_event.clear()
|
||||
client.fan_command(
|
||||
key=fan_presets.key,
|
||||
state=True,
|
||||
speed_level=2,
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=2.0)
|
||||
|
||||
fan_state = states[fan_presets.key]
|
||||
assert fan_state.state is True
|
||||
assert fan_state.speed_level == 2
|
||||
assert fan_state.preset_mode == ""
|
||||
|
||||
# Test 6: Test fan with no speed support
|
||||
fan_no_speed = fan_map["Test Fan No Speed"]
|
||||
assert fan_no_speed.supports_speed is False
|
||||
|
||||
state_event.clear()
|
||||
client.fan_command(
|
||||
key=fan_no_speed.key,
|
||||
state=True,
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=2.0)
|
||||
|
||||
fan_state = states[fan_no_speed.key]
|
||||
assert fan_state.state is True
|
||||
# No speed should be set for fans that don't support speed
|
Loading…
x
Reference in New Issue
Block a user