Compare commits

...
7 changed files with 75 additions and 14 deletions
+4
View File
@@ -185,6 +185,10 @@ _PRESET_MODES_SCHEMA = cv.All(
)
# speed_count_ is a uint16_t in FanTraits
validate_speed_count = cv.int_range(min=1, max=65535)
def validate_preset_modes(value):
# Check against defined schema
value = _PRESET_MODES_SCHEMA(value)
+15 -11
View File
@@ -17,24 +17,24 @@ class FanTraits {
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
: flags_{oscillation, speed, direction, 0}, speed_count_(speed_count) {}
/// Return if this fan supports oscillation.
bool supports_oscillation() const { return this->oscillation_; }
bool supports_oscillation() const { return this->flags_.oscillation; }
/// Set whether this fan supports oscillation.
void set_oscillation(bool oscillation) { this->oscillation_ = oscillation; }
void set_oscillation(bool oscillation) { this->flags_.oscillation = oscillation; }
/// Return if this fan supports speed modes.
bool supports_speed() const { return this->speed_; }
bool supports_speed() const { return this->flags_.speed; }
/// Set whether this fan supports speed levels.
void set_speed(bool speed) { this->speed_ = speed; }
void set_speed(bool speed) { this->flags_.speed = speed; }
/// Return how many speed levels the fan has
int supported_speed_count() const { return this->speed_count_; }
/// Set how many speed levels this fan has.
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
/// Return if this fan supports changing direction
bool supports_direction() const { return this->direction_; }
bool supports_direction() const { return this->flags_.direction; }
/// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; }
void set_direction(bool direction) { this->flags_.direction = direction; }
/// Empty when the fan has no preset modes. Set them on the Fan entity.
const FanPresetModes &supported_preset_modes() const;
@@ -60,10 +60,14 @@ class FanTraits {
/// Set the preset modes pointer (only Fan::wire_preset_modes_() should call this).
void set_supported_preset_modes_(const FanPresetModes *preset_modes) { this->preset_modes_ = preset_modes; }
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
int speed_count_{};
// Bit-packed flags to save memory (1 byte instead of 3)
struct FanTraitFlags {
uint8_t oscillation : 1;
uint8_t speed : 1;
uint8_t direction : 1;
uint8_t reserved : 5;
} flags_{};
uint16_t speed_count_{}; // validators cap configs at 65535
const FanPresetModes *preset_modes_{nullptr}; // owned by the Fan entity
};
static_assert(std::is_trivially_copyable_v<FanTraits>, "FanTraits is returned by value from get_traits()");
+1 -1
View File
@@ -33,7 +33,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_SPEED_COUNT, default=100): fan.validate_speed_count,
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
+1 -1
View File
@@ -26,7 +26,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SPEED): cv.invalid(
"Configuring individual speeds is deprecated."
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_SPEED_COUNT, default=100): fan.validate_speed_count,
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)
+1 -1
View File
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
{
cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean,
cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean,
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1),
cv.Optional(CONF_SPEED_COUNT): fan.validate_speed_count,
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)
@@ -0,0 +1,38 @@
"""speed_count must fit the uint16_t FanTraits stores it in."""
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.hbridge.fan import CONFIG_SCHEMA as HBRIDGE_SCHEMA
from esphome.components.speed.fan import CONFIG_SCHEMA as SPEED_SCHEMA
from esphome.components.template.fan import CONFIG_SCHEMA as TEMPLATE_SCHEMA
from esphome.const import CONF_SPEED_COUNT
SCHEMAS: dict[str, tuple[Any, dict[str, Any]]] = {
"speed": (SPEED_SCHEMA, {"name": "Fan", "output": "fan_out"}),
"hbridge": (HBRIDGE_SCHEMA, {"name": "Fan", "pin_a": "out_a", "pin_b": "out_b"}),
"template": (TEMPLATE_SCHEMA, {"name": "Fan"}),
}
@pytest.mark.parametrize("platform", SCHEMAS)
@pytest.mark.parametrize("speed_count", [1, 65535])
def test_speed_count_limits_accepted(platform: str, speed_count: int) -> None:
schema, base = SCHEMAS[platform]
config = schema({**base, CONF_SPEED_COUNT: speed_count})
assert config[CONF_SPEED_COUNT] == speed_count
@pytest.mark.parametrize("platform", SCHEMAS)
@pytest.mark.parametrize("speed_count", [0, 65536])
def test_speed_count_out_of_range_rejected(platform: str, speed_count: int) -> None:
schema, base = SCHEMAS[platform]
with pytest.raises(cv.Invalid, match="speed_count|at least|at most"):
schema({**base, CONF_SPEED_COUNT: speed_count})
def test_template_fan_without_speed_count() -> None:
schema, base = SCHEMAS["template"]
assert CONF_SPEED_COUNT not in schema(base)
+15
View File
@@ -57,4 +57,19 @@ TEST(FanPresetModesTest, TraitsWithoutAFanHaveNoPresets) {
EXPECT_FALSE(traits.supports_preset_modes());
}
TEST(FanTraitsTest, SpeedCountKeepsSixteenBits) {
FanTraits traits(true, true, false, 65535);
EXPECT_EQ(traits.supported_speed_count(), 65535);
EXPECT_TRUE(traits.supports_oscillation());
EXPECT_TRUE(traits.supports_speed());
EXPECT_FALSE(traits.supports_direction());
traits.set_supported_speed_count(1);
traits.set_direction(true);
traits.set_oscillation(false);
EXPECT_EQ(traits.supported_speed_count(), 1);
EXPECT_TRUE(traits.supports_direction());
EXPECT_FALSE(traits.supports_oscillation());
EXPECT_TRUE(traits.supports_speed());
}
} // namespace esphome::fan::testing