Compare commits

...
6 changed files with 191 additions and 2 deletions
+38 -1
View File
@@ -13,7 +13,9 @@ from esphome.const import (
CONF_MIN_POWER,
CONF_POWER_SUPPLY,
)
from esphome.core import CORE
from esphome.core import CORE, ID
from esphome.core.entity_helpers import ApplyEntity, SubEntities
from esphome.cpp_generator import MockObj, SafeExpType
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
@@ -54,6 +56,8 @@ BinaryOutput = output_ns.class_("BinaryOutput")
BinaryOutputPtr = BinaryOutput.operator("ptr")
FloatOutput = output_ns.class_("FloatOutput", BinaryOutput)
FloatOutputPtr = FloatOutput.operator("ptr")
ApplyBinaryOutput = output_ns.class_("ApplyBinaryOutput", BinaryOutput)
ApplyFloatOutput = output_ns.class_("ApplyFloatOutput", FloatOutput)
async def setup_output_platform_(obj, config):
@@ -84,6 +88,39 @@ async def register_output(var, config):
await setup_output_platform_(var, config)
async def new_output(config: ConfigType, *args: SafeExpType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_output(var, config)
return var
BINARY_APPLY_ENTITY = ApplyEntity(
ApplyBinaryOutput,
((BinaryOutputPtr, "entity"), (cg.uint16, "index"), (cg.bool_, "state")),
)
FLOAT_APPLY_ENTITY = ApplyEntity(
ApplyFloatOutput,
((FloatOutputPtr, "entity"), (cg.uint16, "index"), (cg.float_, "state")),
)
def sub_binary_outputs(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Return a SubEntities bound to new_output; ``apply`` can use ``entity``, ``index`` and ``state``.
``index`` is the constructor argument (default 0), so a hub's channels share one class.
"""
return SubEntities(new_output, config, parent, BINARY_APPLY_ENTITY)
def sub_float_outputs(
config: ConfigType, *, parent: MockObj | ID | None = None
) -> SubEntities:
"""Like sub_binary_outputs; ``apply`` gets the scaled level as ``state``."""
return SubEntities(new_output, config, parent, FLOAT_APPLY_ENTITY)
BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(BinaryOutput),
+12
View File
@@ -68,4 +68,16 @@ class BinaryOutput {
#endif
};
/// Calls the codegen-generated Fn(this, index, state) on a state write; the hub is baked into Fn.
/// The index (a hub's channel) is data, so all channels of a hub share one instantiation.
template<auto Fn> class ApplyBinaryOutput final : public BinaryOutput {
public:
explicit ApplyBinaryOutput(uint16_t index = 0) : index_(index) {}
protected:
void write_state(bool state) override { Fn(this, this->index_, state); }
uint16_t index_; // Fits in the base's tail padding
};
} // namespace esphome::output
+12
View File
@@ -130,4 +130,16 @@ class FloatOutput : public BinaryOutput {
#endif
};
/// Calls the codegen-generated Fn(this, index, state) with the scaled, inverted level; the hub is baked into Fn.
/// The index (a hub's channel) is data, so all channels of a hub share one instantiation.
template<auto Fn> class ApplyFloatOutput final : public FloatOutput {
public:
explicit ApplyFloatOutput(uint16_t index = 0) : index_(index) {}
protected:
void write_state(float state) override { Fn(this, this->index_, state); }
uint16_t index_; // Fits in the base's tail padding
};
} // namespace esphome::output
+11
View File
@@ -0,0 +1,11 @@
import esphome.codegen as cg
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# Builds the min/max power scaling so the Apply output tests cover it
async def to_code_testing(config: ConfigType) -> None:
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
manifest.to_code = to_code_testing
@@ -0,0 +1,81 @@
#include <gtest/gtest.h>
#include <vector>
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
namespace esphome::output::testing {
static void *last_output = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static uint16_t last_index = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static std::vector<bool> bool_writes; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static std::vector<float> levels; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static void on_binary_write(BinaryOutput *output, uint16_t index, bool state) {
last_output = output;
last_index = index;
bool_writes.push_back(state);
}
static void on_float_write(FloatOutput *output, uint16_t index, float state) {
last_output = output;
last_index = index;
levels.push_back(state);
}
// The index sits in the base's tail padding; only the power supply requester leaves none.
#ifndef USE_POWER_SUPPLY
static_assert(sizeof(ApplyBinaryOutput<on_binary_write>) == sizeof(BinaryOutput));
static_assert(sizeof(ApplyFloatOutput<on_float_write>) == sizeof(FloatOutput));
#endif
static void reset() {
last_output = nullptr;
last_index = 0;
bool_writes.clear();
levels.clear();
}
TEST(ApplyBinaryOutput, WritesReachTheFunctionWithIndexAndInversion) {
reset();
ApplyBinaryOutput<on_binary_write> out(7);
out.turn_on();
out.turn_off();
out.set_inverted(true);
out.turn_on();
EXPECT_EQ(bool_writes, (std::vector<bool>{true, false, false}));
EXPECT_EQ(last_output, &out);
EXPECT_EQ(last_index, 7);
}
TEST(ApplyFloatOutput, WritesReachTheFunctionWithIndexInversionAndClamp) {
reset();
ApplyFloatOutput<on_float_write> out(3);
out.set_level(0.25f);
out.turn_on();
out.set_inverted(true);
out.set_level(0.25f);
out.set_level(2.0f);
EXPECT_EQ(levels, (std::vector<float>{0.25f, 1.0f, 0.75f, 0.0f}));
EXPECT_EQ(last_output, &out);
EXPECT_EQ(last_index, 3);
}
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
TEST(ApplyFloatOutput, PowerScalingIsAppliedBeforeTheFunction) {
reset();
ApplyFloatOutput<on_float_write> out;
out.set_min_power(0.2f);
out.set_max_power(0.8f);
out.set_zero_means_zero(true);
out.set_level(0.5f);
out.turn_off();
ASSERT_EQ(levels.size(), 2u);
EXPECT_FLOAT_EQ(levels[0], 0.5f);
EXPECT_EQ(levels[1], 0.0f);
EXPECT_EQ(last_index, 0);
}
#endif
} // namespace esphome::output::testing
+37 -1
View File
@@ -9,7 +9,7 @@ import pytest
from esphome import final_validate as fv
import esphome.codegen as cg
from esphome.components import button, number, select, switch
from esphome.components import button, number, output, select, switch
from esphome.config import Config
from esphome.config_validation import Invalid
from esphome.const import (
@@ -1466,6 +1466,30 @@ async def test_sub_entities_apply_shares_equal_functions(
]
@pytest.mark.asyncio
async def test_sub_outputs_share_one_function_per_hub(
setup_test_environment: list[str], apply_functions: list[tuple[str, str, str]]
) -> None:
"""A hub's channels pass their channel as the index, so they share one function."""
calls: list[tuple[Any, ...]] = []
cls = output.ApplyFloatOutput
config = {"a": _apply_conf("ch0", cls), "b": _apply_conf("ch5", cls)}
sub = _apply_sub(calls, config, output.FLOAT_APPLY_ENTITY)
await sub("a", None, 0, apply="write_channel(index, state)")
await sub("b", None, 5, apply="write_channel(index, state)")
assert calls == [
("ch0", "<esphome__ch0__fn>", "0"),
("ch5", "<esphome__ch0__fn>", "5"),
]
assert apply_functions == [
(
"esphome__ch0__fn",
"output::FloatOutput * entity, uint16_t index, float state",
"::hub->write_channel(index, state);",
)
]
@pytest.mark.asyncio
async def test_sub_entities_apply_parent_id(
monkeypatch: pytest.MonkeyPatch,
@@ -1567,6 +1591,18 @@ async def test_sub_entities_apply_needs_parent(
"select::Select * entity, size_t index",
"entity->publish_state(index);",
),
(
output.sub_binary_outputs,
output.ApplyBinaryOutput,
"output::BinaryOutput * entity, uint16_t index, bool state",
None,
),
(
output.sub_float_outputs,
output.ApplyFloatOutput,
"output::FloatOutput * entity, uint16_t index, float state",
None,
),
],
)
def test_domain_apply_entities(