mirror of
https://github.com/esphome/esphome.git
synced 2026-10-25 07:25:35 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c7e9348c1 | ||
|
|
f0c3c35800 | ||
|
|
ee99bab08b | ||
|
|
6c1a7642f3 | ||
|
|
1ce0db00b3 | ||
|
|
a50a30414e |
No files matched your search
@@ -449,8 +449,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
```
|
||||
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
|
||||
to still drive a state when the restore mode is disabled.
|
||||
A hub that applies its switches' restored states itself reads `sw->get_initial_write_state()`, which
|
||||
already applies `inverted`.
|
||||
|
||||
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
|
||||
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
|
||||
|
||||
@@ -173,8 +173,8 @@ void DFRobotSEN0609Component::setup() {
|
||||
switch_::Switch *sw = this->switches_[i];
|
||||
if (sw == nullptr)
|
||||
continue;
|
||||
// A disabled restore mode leaves the switch on, which an inverted switch writes as off
|
||||
this->publish_switch_(static_cast<SwitchSetting>(i), sw->get_initial_write_state().value_or(!sw->is_inverted()));
|
||||
const bool on = sw->get_initial_state_with_restore_mode().value_or(true);
|
||||
this->publish_switch_(static_cast<SwitchSetting>(i), on != sw->is_inverted());
|
||||
}
|
||||
#endif
|
||||
this->queue_configuration_();
|
||||
|
||||
@@ -155,9 +155,12 @@ void HitachiClimate::setup() {
|
||||
set_swing_v_(swing_vertical(restore->swing_mode));
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
// The restored value is the switch state, which an inverted switch flips relative to the frame bit
|
||||
if (this->mildew_proof_switch_ != nullptr) {
|
||||
if (auto initial = this->mildew_proof_switch_->get_initial_write_state())
|
||||
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET, *initial);
|
||||
if (auto initial = this->mildew_proof_switch_->get_initial_state_with_restore_mode()) {
|
||||
assign_bits(this->remote_state_[HITACHI_AC344_MILDEWPROOF_BYTE], 1 << HITACHI_AC344_MILDEWPROOF_OFFSET,
|
||||
*initial != this->mildew_proof_switch_->is_inverted());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
this->setup_(restore);
|
||||
|
||||
@@ -15,7 +15,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
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -57,6 +59,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):
|
||||
@@ -87,6 +91,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),
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -25,7 +25,7 @@ void SeeedMultiChannelRelay::setup() {
|
||||
switch_::Switch *sw = this->switches_[channel];
|
||||
if (sw == nullptr)
|
||||
continue;
|
||||
const bool on = sw->get_initial_write_state().value_or(false);
|
||||
const bool on = sw->get_initial_state_with_restore_mode().value_or(sw->is_inverted()) != sw->is_inverted();
|
||||
assign_bits(value, 1 << channel, on);
|
||||
}
|
||||
if (!this->status_comm_result(this->write_byte(CMD_CHANNEL_CTRL, value)))
|
||||
|
||||
@@ -60,11 +60,6 @@ optional<bool> Switch::get_initial_state_with_restore_mode() {
|
||||
}
|
||||
return initial_state;
|
||||
}
|
||||
optional<bool> Switch::get_initial_write_state() {
|
||||
if (auto initial = this->get_initial_state_with_restore_mode())
|
||||
return *initial != this->inverted_;
|
||||
return {};
|
||||
}
|
||||
void Switch::restore() {
|
||||
if (auto initial = this->get_initial_state_with_restore_mode())
|
||||
this->control(*initial);
|
||||
|
||||
@@ -114,8 +114,6 @@ class Switch : public EntityBase {
|
||||
* state.
|
||||
*/
|
||||
optional<bool> get_initial_state_with_restore_mode();
|
||||
/// get_initial_state_with_restore_mode() with inverted applied: the level write_state() takes.
|
||||
optional<bool> get_initial_write_state();
|
||||
|
||||
/// Drive the switch to the state its restore mode yields, through control(), so inversion and
|
||||
/// write_state() apply; a disabled restore mode drives \p when_disabled, or nothing without one.
|
||||
|
||||
@@ -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
|
||||
@@ -46,16 +46,4 @@ TEST(SwitchRestore, DefaultAppliesOnlyWhenTheModeIsDisabled) {
|
||||
EXPECT_EQ(on.writes, std::vector<bool>{true});
|
||||
}
|
||||
|
||||
TEST(SwitchRestore, InitialWriteStateAppliesInversion) {
|
||||
RecordingSwitch on, inverted, disabled;
|
||||
on.set_restore_mode(SWITCH_ALWAYS_ON);
|
||||
inverted.set_restore_mode(SWITCH_ALWAYS_ON);
|
||||
inverted.set_inverted(true);
|
||||
disabled.set_restore_mode(SWITCH_RESTORE_DISABLED);
|
||||
disabled.set_inverted(true);
|
||||
EXPECT_EQ(on.get_initial_write_state(), optional<bool>(true));
|
||||
EXPECT_EQ(inverted.get_initial_write_state(), optional<bool>(false));
|
||||
EXPECT_FALSE(disabled.get_initial_write_state().has_value());
|
||||
}
|
||||
|
||||
} // namespace esphome::switch_::testing
|
||||
@@ -11,7 +11,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
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import Invalid
|
||||
@@ -1620,6 +1620,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,
|
||||
@@ -1795,6 +1819,18 @@ async def test_sub_entities_apply_without_parent_shares_equal_functions(
|
||||
"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(
|
||||
|
||||
Reference in new issue
Block a user