mirror of
https://github.com/esphome/esphome.git
synced 2026-10-27 16:32:11 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de42809df3 | ||
|
|
c04cdea1a0 | ||
|
|
f2d1aca8cc | ||
|
|
a52336ee74 | ||
|
|
772be625b5 | ||
|
|
46d8c3ec02 | ||
|
|
fb88e6fc40 | ||
|
|
4eafe90cab | ||
|
|
d5bd1d598a |
No files matched your search
@@ -443,7 +443,8 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
- **Hub forwarding entities need no C++ class:** a button, switch, number or select that only calls its hub
|
||||
uses `button.ApplyButton` (`switch.ApplySwitch`, `number.ApplyNumber`, `select.ApplySelect`) in its schema
|
||||
and `apply=` on the `sub_*` call. The body is a call on the hub with `entity` and `state`, `value` or
|
||||
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state.
|
||||
`index`/`option` in scope; on a switch, number or select `publish="before"` or `"after"` publishes the state,
|
||||
and `publish="on_success"` publishes only when the hub call returns true (a write the device can refuse).
|
||||
`publish="momentary"` is only for legacy action switches (restart, factory reset): it publishes off and runs
|
||||
the body only when turned on. Never use it for a new entity; an action is a button.
|
||||
```python
|
||||
|
||||
@@ -4,29 +4,25 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_POWER_MODE, ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..audio_dac import CONF_PCM5122, PCM5122, pcm5122_ns
|
||||
|
||||
PCM5122PowerSwitch = pcm5122_ns.class_("PCM5122PowerSwitch", switch.Switch)
|
||||
|
||||
pcm5122_power_switch_mode = pcm5122_ns.enum("PCM5122PowerSwitchMode")
|
||||
PCM5122_POWER_SWITCH_MODE_ENUM = pcm5122_power_switch_mode.options(
|
||||
"standby", "powerdown", prefix="PCM5122_POWER_SWITCH_MODE_"
|
||||
)
|
||||
from ..audio_dac import CONF_PCM5122, PCM5122
|
||||
|
||||
CONFIG_SCHEMA = switch.switch_schema(
|
||||
PCM5122PowerSwitch,
|
||||
switch.ApplySwitch,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_PCM5122): cv.use_id(PCM5122),
|
||||
cv.Optional(CONF_POWER_MODE, default="powerdown"): cv.enum(
|
||||
PCM5122_POWER_SWITCH_MODE_ENUM, lower=True
|
||||
cv.Optional(CONF_POWER_MODE, default="powerdown"): cv.one_of(
|
||||
"standby", "powerdown", lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_parented(var, config[CONF_PCM5122])
|
||||
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
|
||||
hub = await cg.get_variable(config[CONF_PCM5122])
|
||||
setter = "set_standby" if config[CONF_POWER_MODE] == "standby" else "set_powerdown"
|
||||
# The switch shows the new state only once the DAC accepted it
|
||||
await switch.sub_switches(config, parent=hub).create(
|
||||
config, None, apply=f"{setter}(state)", publish="on_success"
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "power_switch.h"
|
||||
|
||||
namespace esphome::pcm5122 {
|
||||
|
||||
void PCM5122PowerSwitch::write_state(bool state) {
|
||||
bool ok = (this->mode_ == PCM5122_POWER_SWITCH_MODE_STANDBY) ? this->parent_->set_standby(state)
|
||||
: this->parent_->set_powerdown(state);
|
||||
if (ok)
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
#include "../pcm5122.h"
|
||||
|
||||
namespace esphome::pcm5122 {
|
||||
|
||||
enum PCM5122PowerSwitchMode : uint8_t {
|
||||
PCM5122_POWER_SWITCH_MODE_STANDBY,
|
||||
PCM5122_POWER_SWITCH_MODE_POWERDOWN,
|
||||
};
|
||||
|
||||
class PCM5122PowerSwitch final : public switch_::Switch, public Parented<PCM5122> {
|
||||
public:
|
||||
void set_power_mode(PCM5122PowerSwitchMode mode) { this->mode_ = mode; }
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
|
||||
PCM5122PowerSwitchMode mode_{PCM5122_POWER_SWITCH_MODE_POWERDOWN};
|
||||
};
|
||||
|
||||
} // namespace esphome::pcm5122
|
||||
@@ -5,7 +5,7 @@ from importlib import import_module
|
||||
from importlib.util import find_spec
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, get_args
|
||||
|
||||
from esphome.automation import apply_static_function, parent_ref
|
||||
import esphome.codegen as cg
|
||||
@@ -503,7 +503,8 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
|
||||
config[_KEY_ICON_IDX] = icon_idx
|
||||
|
||||
|
||||
ApplyPublish = Literal["before", "after", "momentary"] | None
|
||||
ApplyPublishMode = Literal["before", "after", "momentary", "on_success"]
|
||||
ApplyPublish = ApplyPublishMode | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -577,7 +578,8 @@ class SubEntities:
|
||||
|
||||
With ``apply`` the entity is the domain's ``Apply*`` class and its control runs
|
||||
``parent->apply``, or ``apply`` as a namespaced free statement when no parent is bound, with
|
||||
``publish`` ("before"/"after") placing the domain's publish. ``publish="momentary"`` instead
|
||||
``publish`` ("before"/"after") placing the domain's publish; ``"on_success"`` publishes only when
|
||||
``apply`` returns true. ``publish="momentary"`` instead
|
||||
acknowledges first (a switch publishes off) and runs ``apply`` only when turned on. It is only for
|
||||
legacy action switches; a new action entity is a button.
|
||||
"""
|
||||
@@ -600,10 +602,8 @@ class SubEntities:
|
||||
)
|
||||
if publish is not None and (apply is None or self.apply_entity.publish is None):
|
||||
raise ValueError(f"{entity_id}: publish= needs apply= on a stateful domain")
|
||||
if publish not in (None, "before", "after", "momentary"):
|
||||
raise ValueError(
|
||||
f'{entity_id}: publish= must be "before", "after" or "momentary"'
|
||||
)
|
||||
if publish is not None and publish not in (modes := get_args(ApplyPublishMode)):
|
||||
raise ValueError(f"{entity_id}: publish= must be one of {', '.join(modes)}")
|
||||
if publish == "momentary" and self.apply_entity.momentary is None:
|
||||
raise ValueError(f'{entity_id}: publish="momentary" is not supported here')
|
||||
|
||||
@@ -626,11 +626,14 @@ class SubEntities:
|
||||
if isinstance(parent, ID):
|
||||
parent = await get_variable(parent)
|
||||
apply_entity = self.apply_entity
|
||||
call = f"{apply};" if parent is None else f"{parent_ref(parent)}->{apply};"
|
||||
expr = apply if parent is None else f"{parent_ref(parent)}->{apply}"
|
||||
call = f"{expr};"
|
||||
if publish == "before":
|
||||
body = [apply_entity.publish, call]
|
||||
elif publish == "after":
|
||||
body = [call, apply_entity.publish]
|
||||
elif publish == "on_success":
|
||||
body = [f"if ({expr}) {{ {apply_entity.publish} }}"]
|
||||
elif publish == "momentary":
|
||||
acknowledge, condition = apply_entity.momentary
|
||||
body = [acknowledge, f"if ({condition}) {{ {call} }}"]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The DAC is an audio_dac platform on I2C; the tests drive its power switch.
|
||||
manifest.dependencies = manifest.dependencies + ["audio_dac", "i2c", "switch"]
|
||||
@@ -33,3 +33,7 @@ switch:
|
||||
pcm5122: pcm5122_dac
|
||||
name: PCM5122 Power Down
|
||||
power_mode: powerdown
|
||||
- platform: pcm5122
|
||||
pcm5122: pcm5122_dac
|
||||
name: PCM5122 Standby
|
||||
power_mode: standby
|
||||
@@ -0,0 +1,102 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "esphome/components/pcm5122/pcm5122.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::pcm5122::testing {
|
||||
|
||||
static constexpr uint8_t ADDRESS = 0x4D;
|
||||
|
||||
// The bodies codegen emits for power_mode: standby and power_mode: powerdown.
|
||||
static PCM5122 *dac = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static void standby(switch_::Switch *entity, bool state) {
|
||||
if (dac->set_standby(state))
|
||||
entity->publish_state(state);
|
||||
}
|
||||
static void powerdown(switch_::Switch *entity, bool state) {
|
||||
if (dac->set_powerdown(state))
|
||||
entity->publish_state(state);
|
||||
}
|
||||
|
||||
// Page 0 registers written as register/value pairs; failing writes are not acknowledged.
|
||||
class RegisterBus final : public i2c::I2CBus {
|
||||
public:
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t * /*out*/,
|
||||
size_t /*out_size*/) override {
|
||||
EXPECT_EQ(address, ADDRESS);
|
||||
if (this->fail)
|
||||
return i2c::ERROR_NOT_ACKNOWLEDGED;
|
||||
if (size == 2)
|
||||
this->registers[data[0]] = data[1];
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 256> registers{};
|
||||
bool fail{false};
|
||||
};
|
||||
|
||||
class PCM5122PowerSwitchTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->dac_.set_i2c_bus(&this->bus_);
|
||||
this->dac_.set_i2c_address(ADDRESS);
|
||||
dac = &this->dac_;
|
||||
this->standby_.add_on_state_callback([this](bool) { ++this->publishes_; });
|
||||
this->powerdown_.add_on_state_callback([this](bool) { ++this->publishes_; });
|
||||
}
|
||||
|
||||
uint8_t power_control_() const { return this->bus_.registers[PCM5122_REG_POWER_CONTROL]; }
|
||||
|
||||
RegisterBus bus_;
|
||||
PCM5122 dac_;
|
||||
switch_::ApplySwitch<standby> standby_;
|
||||
switch_::ApplySwitch<powerdown> powerdown_;
|
||||
unsigned publishes_{0};
|
||||
};
|
||||
|
||||
TEST_F(PCM5122PowerSwitchTest, StandbySwitchRequestsStandbyAndPublishes) {
|
||||
this->standby_.turn_on();
|
||||
EXPECT_EQ(this->power_control_(), PCM5122_POWER_CONTROL_RQST);
|
||||
EXPECT_TRUE(this->standby_.state);
|
||||
EXPECT_EQ(this->publishes_, 1u);
|
||||
|
||||
this->standby_.turn_off();
|
||||
EXPECT_EQ(this->power_control_(), 0);
|
||||
EXPECT_FALSE(this->standby_.state);
|
||||
EXPECT_EQ(this->publishes_, 2u);
|
||||
}
|
||||
|
||||
TEST_F(PCM5122PowerSwitchTest, PowerdownSwitchRequestsPowerdownAndPublishes) {
|
||||
this->powerdown_.turn_on();
|
||||
EXPECT_EQ(this->power_control_(), PCM5122_POWER_CONTROL_RQPD);
|
||||
EXPECT_TRUE(this->powerdown_.state);
|
||||
EXPECT_EQ(this->publishes_, 1u);
|
||||
|
||||
this->powerdown_.turn_off();
|
||||
EXPECT_EQ(this->power_control_(), 0);
|
||||
EXPECT_FALSE(this->powerdown_.state);
|
||||
EXPECT_EQ(this->publishes_, 2u);
|
||||
}
|
||||
|
||||
TEST_F(PCM5122PowerSwitchTest, FailedWriteLeavesTheSwitchUnpublished) {
|
||||
this->bus_.fail = true;
|
||||
this->standby_.turn_on();
|
||||
this->powerdown_.turn_on();
|
||||
EXPECT_EQ(this->publishes_, 0u);
|
||||
EXPECT_FALSE(this->standby_.has_state());
|
||||
EXPECT_FALSE(this->powerdown_.has_state());
|
||||
}
|
||||
|
||||
TEST_F(PCM5122PowerSwitchTest, FailedWriteDoesNotLeakIntoTheNextRequest) {
|
||||
this->bus_.fail = true;
|
||||
this->standby_.turn_on();
|
||||
this->bus_.fail = false;
|
||||
this->powerdown_.turn_on();
|
||||
EXPECT_EQ(this->power_control_(), PCM5122_POWER_CONTROL_RQPD);
|
||||
EXPECT_FALSE(this->standby_.has_state());
|
||||
EXPECT_TRUE(this->powerdown_.state);
|
||||
}
|
||||
|
||||
} // namespace esphome::pcm5122::testing
|
||||
@@ -0,0 +1,6 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# Builds the power switch platform against the DAC.
|
||||
manifest.dependencies = manifest.dependencies + ["pcm5122"]
|
||||
@@ -1711,6 +1711,14 @@ _SELECT_APPLY = entity_helpers.ApplyEntity(
|
||||
"after",
|
||||
"::hub->set_mode(index, options_);\nentity->publish_state(index);",
|
||||
),
|
||||
(
|
||||
"set_mode(option)",
|
||||
"on_success",
|
||||
(
|
||||
"const char *option = entity->option_at(index);\n"
|
||||
"if (::hub->set_mode(option)) { entity->publish_state(index); }"
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_sub_entities_apply_locals(
|
||||
@@ -1883,6 +1891,35 @@ async def test_sub_entities_apply_momentary(
|
||||
assert apply_functions[0][2] == body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("parent", "apply", "body"),
|
||||
[
|
||||
(
|
||||
_HUB,
|
||||
"write_level(state)",
|
||||
"if (::hub->write_level(state)) { entity->publish_state(state); }",
|
||||
),
|
||||
(
|
||||
None,
|
||||
"chip::write_level(state)",
|
||||
"if (chip::write_level(state)) { entity->publish_state(state); }",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_sub_entities_apply_on_success(
|
||||
setup_test_environment: list[str],
|
||||
apply_functions: list[tuple[str, str, str]],
|
||||
parent: MockObj | None,
|
||||
apply: str,
|
||||
body: str,
|
||||
) -> None:
|
||||
"""publish="on_success" publishes only when the hub call returns true."""
|
||||
sub = _apply_sub([], {"k": _apply_conf("k_sw")}, parent=parent)
|
||||
await sub("k", None, apply=apply, publish="on_success")
|
||||
assert apply_functions[0][2] == body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sub_entities_apply_without_parent_needs_namespaced_call(
|
||||
setup_test_environment: list[str],
|
||||
|
||||
Reference in new issue
Block a user