Compare commits

..
32 changed files with 115 additions and 288 deletions

No files matched your search

+2 -2
View File
@@ -443,14 +443,14 @@ file does, and it is the authority when they disagree. The most useful starting
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.
`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
switches = switch.sub_switches(config, parent=hub)
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
```
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_();
@@ -6,6 +6,8 @@ from esphome.const import (
)
from esphome.types import ConfigType
AUTO_LOAD = ["factory_reset"]
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton,
device_class=DEVICE_CLASS_RESTART,
@@ -1,4 +1,4 @@
#include "factory_reset_button.h"
#include "factory_reset_device.h"
#include "esphome/core/defines.h"
@@ -11,7 +11,7 @@
namespace esphome::factory_reset {
ESPHOME_LOG_TAG(TAG, "factory_reset.button");
ESPHOME_LOG_TAG(TAG, "factory_reset");
static void reset_and_reboot() {
global_preferences->reset();
@@ -2,7 +2,7 @@
namespace esphome::factory_reset {
/// Run by the factory reset button.
/// Run by the factory reset button and switch.
void factory_reset_device();
} // namespace esphome::factory_reset
@@ -1,23 +1,21 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT
from esphome.types import ConfigType
from .. import factory_reset_ns
FactoryResetSwitch = factory_reset_ns.class_(
"FactoryResetSwitch", switch.Switch, cg.Component
)
AUTO_LOAD = ["factory_reset"]
CONFIG_SCHEMA = switch.switch_schema(
FactoryResetSwitch,
switch.ApplySwitch,
block_inverted=True,
icon=ICON_RESTART_ALERT,
entity_category=ENTITY_CATEGORY_CONFIG,
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await switch.new_switch(config)
await cg.register_component(var, config)
await switch.sub_switches(config).create(
config,
None,
apply="factory_reset::factory_reset_device()",
publish="momentary",
)
@@ -1,41 +0,0 @@
#include "factory_reset_switch.h"
#include "esphome/core/defines.h"
#ifdef USE_OPENTHREAD
#include "esphome/components/openthread/openthread.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::factory_reset {
ESPHOME_LOG_TAG(TAG, "factory_reset.switch");
void FactoryResetSwitch::dump_config() { LOG_SWITCH("", "Factory Reset Switch", this); }
void FactoryResetSwitch::write_state(bool state) {
// Acknowledge
this->publish_state(false);
if (state) {
ESP_LOGI(TAG, "Resetting");
// Let MQTT settle a bit
delay(100); // NOLINT
#ifdef USE_OPENTHREAD
openthread::global_openthread_component->on_factory_reset(FactoryResetSwitch::factory_reset_callback);
#else
global_preferences->reset();
App.safe_reboot();
#endif
}
}
#ifdef USE_OPENTHREAD
void FactoryResetSwitch::factory_reset_callback() {
global_preferences->reset();
App.safe_reboot();
}
#endif
} // namespace esphome::factory_reset
@@ -1,20 +0,0 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
namespace esphome::factory_reset {
class FactoryResetSwitch final : public switch_::Switch, public Component {
public:
void dump_config() override;
#ifdef USE_OPENTHREAD
static void factory_reset_callback();
#endif
protected:
void write_state(bool state) override;
};
} // namespace esphome::factory_reset
@@ -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);
@@ -2,6 +2,8 @@ from esphome.components import button
from esphome.const import DEVICE_CLASS_RESTART, ENTITY_CATEGORY_CONFIG, ICON_RESTART
from esphome.types import ConfigType
AUTO_LOAD = ["restart"]
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton,
icon=ICON_RESTART,
@@ -1,11 +1,11 @@
#include "restart_button.h"
#include "restart.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::restart {
ESPHOME_LOG_TAG(TAG, "restart.button");
ESPHOME_LOG_TAG(TAG, "restart");
void restart_device() {
ESP_LOGI(TAG, "Restarting device");
@@ -2,7 +2,7 @@
namespace esphome::restart {
/// Run by the restart button.
/// Run by the restart button and switch.
void restart_device();
} // namespace esphome::restart
@@ -1,20 +1,18 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART
from esphome.types import ConfigType
restart_ns = cg.esphome_ns.namespace("restart")
RestartSwitch = restart_ns.class_("RestartSwitch", switch.Switch, cg.Component)
AUTO_LOAD = ["restart"]
CONFIG_SCHEMA = switch.switch_schema(
RestartSwitch,
switch.ApplySwitch,
icon=ICON_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
block_inverted=True,
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await switch.new_switch(config)
await cg.register_component(var, config)
await switch.sub_switches(config).create(
config, None, apply="restart::restart_device()", publish="momentary"
)
@@ -1,23 +0,0 @@
#include "restart_switch.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::restart {
ESPHOME_LOG_TAG(TAG, "restart");
void RestartSwitch::write_state(bool state) {
// Acknowledge
this->publish_state(false);
if (state) {
ESP_LOGI(TAG, "Restarting device");
// Let MQTT settle a bit
delay(100); // NOLINT
App.safe_reboot();
}
}
void RestartSwitch::dump_config() { LOG_SWITCH("", "Restart Switch", this); }
} // namespace esphome::restart
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/switch/switch.h"
namespace esphome::restart {
class RestartSwitch final : public switch_::Switch, public Component {
public:
// User provided, not "= default": `new(p) RestartSwitch()` would zero-fill .bss that is already zero.
RestartSwitch() {}
void dump_config() override;
protected:
void write_state(bool state) override;
};
} // namespace esphome::restart
+1 -1
View File
@@ -25,7 +25,7 @@ class SafeModeComponent final : public Component {
/// Set to true if the next startup will enter safe mode
void set_safe_mode_pending(const bool &pending);
bool get_safe_mode_pending();
/// Set safe mode pending and reboot; run by the safe mode button.
/// Set safe mode pending and reboot; run by the safe mode button and switch.
void restart_in_safe_mode();
void dump_config() override;
+11 -18
View File
@@ -1,29 +1,22 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_SAFE_MODE, ENTITY_CATEGORY_CONFIG, ICON_RESTART_ALERT
from esphome.types import ConfigType
from .. import SafeModeComponent, safe_mode_ns
from .. import SafeModeComponent
DEPENDENCIES = ["safe_mode"]
SafeModeSwitch = safe_mode_ns.class_("SafeModeSwitch", switch.Switch, cg.Component)
CONFIG_SCHEMA = (
switch.switch_schema(
SafeModeSwitch,
block_inverted=True,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
)
.extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = switch.switch_schema(
switch.ApplySwitch,
block_inverted=True,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
).extend({cv.GenerateID(CONF_SAFE_MODE): cv.use_id(SafeModeComponent)})
async def to_code(config: ConfigType) -> None:
var = await switch.new_switch(config)
await cg.register_component(var, config)
await cg.add_variable(var.set_safe_mode, config[CONF_SAFE_MODE])
switches = switch.sub_switches(config, parent=config[CONF_SAFE_MODE])
await switches.create(
config, None, apply="restart_in_safe_mode()", publish="momentary"
)
@@ -1,26 +0,0 @@
#include "safe_mode_switch.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::safe_mode {
ESPHOME_LOG_TAG(TAG, "safe_mode.switch");
void SafeModeSwitch::write_state(bool state) {
// Acknowledge
this->publish_state(false);
if (state) {
ESP_LOGI(TAG, "Restarting in safe mode");
this->safe_mode_component_->set_safe_mode_pending(true);
// Let MQTT settle a bit
delay(100); // NOLINT
App.safe_reboot();
}
}
void SafeModeSwitch::dump_config() { LOG_SWITCH("", "Safe Mode Switch", this); }
} // namespace esphome::safe_mode
@@ -1,19 +0,0 @@
#pragma once
#include "esphome/components/safe_mode/safe_mode.h"
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
namespace esphome::safe_mode {
class SafeModeSwitch final : public switch_::Switch, public Component {
public:
void dump_config() override;
void set_safe_mode(SafeModeComponent *safe_mode_component) { this->safe_mode_component_ = safe_mode_component; }
protected:
SafeModeComponent *safe_mode_component_;
void write_state(bool state) override;
};
} // namespace esphome::safe_mode
@@ -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)))
@@ -2,6 +2,8 @@ from esphome.components import button
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_POWER
from esphome.types import ConfigType
AUTO_LOAD = ["shutdown"]
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG, icon=ICON_POWER
)
@@ -1,4 +1,4 @@
#include "shutdown_button.h"
#include "shutdown.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
@@ -12,7 +12,7 @@
namespace esphome::shutdown {
ESPHOME_LOG_TAG(TAG, "shutdown.button");
ESPHOME_LOG_TAG(TAG, "shutdown");
void shutdown_device() {
ESP_LOGI(TAG, "Shutting down");
@@ -2,7 +2,7 @@
namespace esphome::shutdown {
/// Run by the shutdown button.
/// Run by the shutdown button and switch.
void shutdown_device();
} // namespace esphome::shutdown
@@ -1,20 +1,18 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_POWER
from esphome.types import ConfigType
shutdown_ns = cg.esphome_ns.namespace("shutdown")
ShutdownSwitch = shutdown_ns.class_("ShutdownSwitch", switch.Switch, cg.Component)
AUTO_LOAD = ["shutdown"]
CONFIG_SCHEMA = switch.switch_schema(
ShutdownSwitch,
switch.ApplySwitch,
icon=ICON_POWER,
entity_category=ENTITY_CATEGORY_CONFIG,
block_inverted=True,
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await switch.new_switch(config)
await cg.register_component(var, config)
await switch.sub_switches(config).create(
config, None, apply="shutdown::shutdown_device()", publish="momentary"
)
@@ -1,36 +0,0 @@
#include "shutdown_switch.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#ifdef USE_ESP32
#include <esp_sleep.h>
#endif
#ifdef USE_ESP8266
#include <Esp.h>
#endif
namespace esphome::shutdown {
ESPHOME_LOG_TAG(TAG, "shutdown.switch");
void ShutdownSwitch::dump_config() { LOG_SWITCH("", "Shutdown Switch", this); }
void ShutdownSwitch::write_state(bool state) {
// Acknowledge
this->publish_state(false);
if (state) {
ESP_LOGI(TAG, "Shutting down");
delay(100); // NOLINT
App.run_safe_shutdown_hooks();
#ifdef USE_ESP8266
ESP.deepSleep(0); // NOLINT(readability-static-accessed-through-instance)
#endif
#ifdef USE_ESP32
esp_deep_sleep_start();
#endif
}
}
} // namespace esphome::shutdown
@@ -1,16 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/switch/switch.h"
namespace esphome::shutdown {
class ShutdownSwitch final : public switch_::Switch, public Component {
public:
void dump_config() override;
protected:
void write_state(bool state) override;
};
} // namespace esphome::shutdown
+1
View File
@@ -303,6 +303,7 @@ APPLY_ENTITY = ApplyEntity(
ApplySwitch,
((SwitchPtr, "entity"), (cg.bool_, "state")),
"entity->publish_state(state);",
momentary=("entity->publish_state(false);", "state"),
)
-5
View File
@@ -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);
-2
View File
@@ -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.
+20 -7
View File
@@ -494,7 +494,7 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
config[_KEY_ICON_IDX] = icon_idx
ApplyPublish = Literal["before", "after"] | None
ApplyPublish = Literal["before", "after", "momentary"] | None
@dataclass(frozen=True, slots=True)
@@ -502,12 +502,14 @@ class ApplyEntity:
"""A domain's ``Apply*`` class, its generated function's parameters and its publish statement.
``locals`` are (name, declaration) pairs, declared only when the body uses the name.
``momentary`` is (acknowledge statement, condition) for ``publish="momentary"``.
"""
cls: MockObjClass
params: tuple[tuple[SafeExpType, str], ...]
publish: str | None = None
locals: tuple[tuple[str, str], ...] = ()
momentary: tuple[str, str] | None = None
_APPLY_DOMAIN = "entity_apply"
@@ -549,7 +551,9 @@ 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`` ("before"/"after") placing the domain's publish. ``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.
"""
entity_id: ID = conf[CONF_ID]
is_apply_class = self.apply_entity is not None and str(entity_id.type) == str(
@@ -570,8 +574,12 @@ 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"):
raise ValueError(f'{entity_id}: publish= must be "before" or "after"')
if publish not in (None, "before", "after", "momentary"):
raise ValueError(
f'{entity_id}: publish= must be "before", "after" or "momentary"'
)
if publish == "momentary" and self.apply_entity.momentary is None:
raise ValueError(f'{entity_id}: publish="momentary" is not supported here')
if apply is None:
var = await self.new_entity(conf, *args, **kwargs)
@@ -592,11 +600,16 @@ class SubEntities:
if isinstance(parent, ID):
parent = await get_variable(parent)
apply_entity = self.apply_entity
body = [f"{apply};" if parent is None else f"{parent_ref(parent)}->{apply};"]
call = f"{apply};" if parent is None else f"{parent_ref(parent)}->{apply};"
if publish == "before":
body.insert(0, apply_entity.publish)
body = [apply_entity.publish, call]
elif publish == "after":
body.append(apply_entity.publish)
body = [call, apply_entity.publish]
elif publish == "momentary":
acknowledge, condition = apply_entity.momentary
body = [acknowledge, f"if ({condition}) {{ {call} }}"]
else:
body = [call]
for name, decl in apply_entity.locals:
# Declared right before its first use, so calls keep their order.
uses = (i for i, line in enumerate(body) if re.search(rf"\b{name}\b", line))
-12
View File
@@ -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
@@ -1479,6 +1479,7 @@ _SWITCH_APPLY = entity_helpers.ApplyEntity(
_APPLY_SWITCH_CLS,
((RawExpression("switch_::Switch *"), "entity"), (RawExpression("bool"), "state")),
"entity->publish_state(state);",
momentary=("entity->publish_state(false);", "state"),
)
_BUTTON_APPLY = entity_helpers.ApplyEntity(
cg.esphome_ns.namespace("button").class_("ApplyButton"),
@@ -1670,6 +1671,12 @@ async def test_sub_entities_apply_parent_id(
{"apply": "x()", "publish": "Before"},
"must be",
),
(
_SELECT_APPLY,
_apply_conf("k_id", _SELECT_APPLY.cls),
{"apply": "x()", "publish": "momentary"},
"not supported",
),
],
)
async def test_sub_entities_apply_errors(
@@ -1689,6 +1696,35 @@ async def test_sub_entities_apply_errors(
assert apply_functions == []
@pytest.mark.asyncio
@pytest.mark.parametrize(
("parent", "apply", "body"),
[
(
_HUB,
"restart()",
"entity->publish_state(false);\nif (state) { ::hub->restart(); }",
),
(
None,
"restart::restart_device()",
"entity->publish_state(false);\nif (state) { restart::restart_device(); }",
),
],
)
async def test_sub_entities_apply_momentary(
setup_test_environment: list[str],
apply_functions: list[tuple[str, str, str]],
parent: MockObj | None,
apply: str,
body: str,
) -> None:
"""A momentary switch publishes off first and acts only when turned on."""
sub = _apply_sub([], {"r": _apply_conf("r_sw")}, parent=parent)
await sub("r", None, apply=apply, publish="momentary")
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],