Compare commits

..
Author SHA1 Message Date
J. Nick Koston cf1cccfa67 Merge remote-tracking branch 'origin/apply-switch-momentary' into apply-switch-momentary-migrate
# Conflicts:
#	esphome/components/shutdown/switch/shutdown_switch.cpp
2026-10-25 07:56:26 -10:00
J. Nick Koston 9371116e63 Merge remote-tracking branch 'origin/dev' into apply-switch-momentary 2026-10-25 07:54:21 -10:00
J. Nick Koston 3131a8bf15 Merge branch 'apply-switch-momentary' into apply-switch-momentary-migrate 2026-10-23 19:05:16 -10:00
J. Nick Koston 055ea07380 Merge branch 'dev' into apply-switch-momentary 2026-10-23 19:05:13 -10:00
J. Nick Koston 70d3e26975 Merge branch 'apply-switch-momentary' into apply-switch-momentary-migrate 2026-10-23 12:35:39 -10:00
J. Nick Koston af3c41004a [core] Brace the momentary guard so it covers the whole apply statement 2026-10-23 12:35:31 -10:00
J. Nick Koston a590b8f735 [safe_mode] Name the switch as a caller of restart_in_safe_mode 2026-10-23 12:21:02 -10:00
J. Nick Koston eeccf7c68e Merge branch 'apply-switch-momentary' into apply-switch-momentary-migrate 2026-10-23 12:15:59 -10:00
J. Nick Koston 03c44ffe4c [core] Say publish="momentary" is only for legacy action switches 2026-10-23 12:15:52 -10:00
J. Nick Koston af8f919456 Merge branch 'apply-switch-momentary' into apply-switch-momentary-migrate 2026-10-23 12:11:08 -10:00
J. Nick Koston a04efc2f72 [core] Build the Apply body from the call once per publish mode 2026-10-23 12:11:08 -10:00
J. Nick Koston e431ab8347 [restart][shutdown][factory_reset][safe_mode] Use momentary Apply switches 2026-10-23 12:08:49 -10:00
J. Nick Koston f0a090bc86 [core] Add publish="momentary" for Apply switches that trigger an action 2026-10-23 11:58:54 -10:00
64 changed files with 548 additions and 1625 deletions

No files matched your search

+2
View File
@@ -443,6 +443,8 @@ 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")
+2 -16
View File
@@ -31,25 +31,11 @@ static inline bool IRAM_ATTR is_code_addr(uint32_t addr) {
}
#if CONFIG_IDF_TARGET_ARCH_RISCV
// Bounds of this image's flash-mapped code, from the ESP-IDF linker script.
extern "C" char _instruction_reserved_start;
extern "C" char _instruction_reserved_end;
// Check that [start, end) lies in this image's flash-mapped code or in IRAM.
// The IROM range is larger than what the MMU maps, and reading an unmapped
// address faults.
static inline bool is_readable_code(uint32_t start, uint32_t end) {
auto irom_start = reinterpret_cast<uint32_t>(&_instruction_reserved_start);
auto irom_end = reinterpret_cast<uint32_t>(&_instruction_reserved_end);
return (start >= irom_start && end <= irom_end) || (start >= SOC_IRAM_LOW && end <= SOC_IRAM_HIGH);
}
// Check if a code address is a real return address by verifying the preceding
// instruction is a JAL or JALR with rd=ra (x1). Called at log time (not during
// panic) so flash cache is available. The address may come from an earlier
// build, so check that the bytes before it are mapped before reading them.
// panic) so flash cache is available and both IRAM and IROM are safely readable.
static inline bool is_return_addr(uint32_t addr) {
if (!is_code_addr(addr) || !is_readable_code(addr - 4, addr))
if (!is_code_addr(addr) || addr < 4)
return false;
// A return address on the stack points to the instruction after a call.
// Check for 4-byte JAL/JALR call instruction before this address.
@@ -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
+1 -5
View File
@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_MODEL, CONF_USE_FAHRENHEIT
from esphome.const import CONF_MODEL
from esphome.types import ConfigType
from . import gree_ns
@@ -19,7 +19,6 @@ MODELS = Model.options(
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
{
cv.Required(CONF_MODEL): cv.enum(MODELS, lower=True),
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}
)
@@ -27,8 +26,5 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
async def to_code(config: ConfigType) -> None:
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_model(config[CONF_MODEL]))
if config[CONF_USE_FAHRENHEIT]:
cg.add_define("USE_GREE_FAHRENHEIT")
cg.add(var.set_use_fahrenheit(True))
if config[CONF_MODEL] == "yaw1f":
cg.add_define("USE_GREE_YAW1F")
+9 -30
View File
@@ -1,6 +1,4 @@
#include "gree.h"
#include <cmath>
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/helpers.h"
@@ -36,21 +34,6 @@ void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
this->transmit_state();
}
#ifdef USE_GREE_FAHRENHEIT
uint8_t fahrenheit_half_steps(float target_celsius) {
if (!std::isfinite(target_celsius)) {
return 0;
}
// Whole degrees shown at k = 0 and at the top of the range
static constexpr int MIN_F = static_cast<int>(celsius_to_fahrenheit(GREE_TEMP_MIN));
static constexpr int MAX_F = static_cast<int>(celsius_to_fahrenheit(GREE_TEMP_MAX));
const int desired_f = static_cast<int>(roundf(clamp<float>(celsius_to_fahrenheit(target_celsius), MIN_F, MAX_F)));
// The display at k is (10 * 32 + 18 * GREE_TEMP_MIN + 9 * k) / 10; pick the smallest k that reaches desired_f
const int tenths_above_min = 10 * desired_f - (10 * 32 + 18 * GREE_TEMP_MIN);
return static_cast<uint8_t>((tenths_above_min + 8) / 9);
}
#endif
void GreeClimate::transmit_state() {
uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00};
const Model model = this->model_;
@@ -58,12 +41,7 @@ void GreeClimate::transmit_state() {
const bool yag_layout = model == GREE_YAG || model == GREE_YAW1F;
remote_state[0] = this->fan_speed_() | this->operation_mode_();
#ifdef USE_GREE_FAHRENHEIT
const uint8_t half_steps = this->use_fahrenheit_ ? fahrenheit_half_steps(this->target_temperature) : 0;
remote_state[1] = this->use_fahrenheit_ ? half_steps >> 1 : this->temperature_();
#else
remote_state[1] = this->temperature_();
#endif
if (model == GREE_YAN) {
remote_state[3] = 0x50; // bits 4..7 always 0101
@@ -129,15 +107,16 @@ void GreeClimate::transmit_state() {
remote_state[0] |= GREE_PRESET_SLEEP_BIT;
}
#ifdef USE_GREE_FAHRENHEIT
if (this->use_fahrenheit_) {
remote_state[3] |= GREE_FAHRENHEIT_BIT;
if (half_steps & 1)
remote_state[3] |= GREE_TEMP_HALF_DEGREE_BIT;
// Calculate the checksum
if (model == GREE_YAN) {
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
} else {
remote_state[7] =
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
0x0F)
<< 4);
}
#endif
remote_state[7] = frame_checksum(remote_state);
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
-27
View File
@@ -1,7 +1,6 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
#include "esphome/core/defines.h"
namespace esphome::gree {
@@ -91,26 +90,6 @@ static constexpr uint8_t GREE_MODE_BIT_LIGHT = 0x20;
static constexpr uint8_t GREE_MODE_BIT_HEALTH = 0x40;
static constexpr uint8_t GREE_MODE_BIT_XFAN = 0x80;
// Byte 3 Fahrenheit bits
static constexpr uint8_t GREE_FAHRENHEIT_BIT = 0x08;
static constexpr uint8_t GREE_TEMP_HALF_DEGREE_BIT = 0x04; // Half a degree Celsius added to the temperature
/// Checksum of a state frame: the low nibbles of bytes 0-3 plus the high nibbles of bytes 4-6 plus 0x0A, in the high
/// nibble.
inline uint8_t frame_checksum(const uint8_t *remote_state) {
return (
(((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
0x0F)
<< 4);
}
#ifdef USE_GREE_FAHRENHEIT
/// Return k = 2 * temp_field + half_degree_bit (0..28) for a Celsius target. The unit displays
/// trunc(celsius_to_fahrenheit(GREE_TEMP_MIN + k / 2)), so k is the smallest value that shows the target in F.
uint8_t fahrenheit_half_steps(float target_celsius);
#endif
// Model codes
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG, GREE_YAW1F };
@@ -124,9 +103,6 @@ class GreeClimate final : public climate_ir::ClimateIR {
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
void set_model(Model model);
#ifdef USE_GREE_FAHRENHEIT
void set_use_fahrenheit(bool use_fahrenheit) { this->use_fahrenheit_ = use_fahrenheit; }
#endif
void set_mode_bit(uint8_t bit_mask, bool enabled);
protected:
@@ -142,9 +118,6 @@ class GreeClimate final : public climate_ir::ClimateIR {
Model model_{};
uint8_t mode_bits_{GREE_MODE_BIT_LIGHT}; // The display light is on until a switch says otherwise
#ifdef USE_GREE_FAHRENHEIT
bool use_fahrenheit_{false};
#endif
};
} // namespace esphome::gree
+11 -14
View File
@@ -69,9 +69,8 @@ PackedBits = modbus.modbus_ns.class_("PackedBits")
# The exception code passed to on_error handlers.
ExceptionCode = modbus.modbus_ns.enum("ExceptionCode")
# Lambda arguments: the shared handlers and send's on_response get the address of their own request first,
# then the request/response PDUs (function code + data). The spans are only valid for the duration of the handler.
_ADDRESS_ARG = (cg.uint8, "address")
# Lambda argument types for the reply handlers: the device address the send targeted, and the
# request/response PDUs (function code + data). The spans are only valid for the duration of the handler.
_PDU_SPAN = cg.std_span.template(cg.uint8.operator("const"))
# The pdu lambda's return type: a stack-allocated StaticVector capped at the Modbus PDU limit
@@ -136,11 +135,11 @@ _ACTION_BASE_SCHEMA = cv.Schema(
cv.GenerateID(modbus.CONF_MODBUS_ID): cv.use_id(modbus.ModbusClient),
cv.Required(CONF_ADDRESS): cv.templatable(cv.hex_uint8_t),
# Optional handlers. on_sent fires when the frame reaches the wire; the reply handlers arrive
# later (fire-and-continue), so all run with the address and request/reply available - not the
# outer automation's variables.
# later (fire-and-continue), so all run with the request/reply available - not the outer
# automation's variables.
cv.Optional(CONF_ON_SENT): _handler_schema(),
cv.Optional(CONF_ON_ERROR): _handler_schema(),
# on_no_response takes either a returning lambda (`!lambda "return <bool>;"`, gets `address`/`request`,
# on_no_response takes either a returning lambda (`!lambda "return <bool>;"`, gets `request`,
# returns true to have the hub retry the frame) OR a `then:` automation of actions; the automation
# form may also carry an optional `retry:` returning lambda to run actions AND decide the retry.
cv.Optional(CONF_ON_NO_RESPONSE): cv.All(
@@ -200,7 +199,7 @@ async def register_client_action(
)
if sent_conf := config.get(CONF_ON_SENT):
await automation.build_automation(
var.get_sent_trigger(), [_ADDRESS_ARG, (_PDU_SPAN, "request")], sent_conf
var.get_sent_trigger(), [(_PDU_SPAN, "request")], sent_conf
)
if response_conf := config.get(CONF_ON_RESPONSE):
await automation.build_automation(
@@ -218,7 +217,7 @@ async def register_client_action(
if error_conf := config.get(CONF_ON_ERROR):
await automation.build_automation(
var.get_error_trigger(),
[_ADDRESS_ARG, (_PDU_SPAN, "request"), (ExceptionCode, "exception_code")],
[(_PDU_SPAN, "request"), (ExceptionCode, "exception_code")],
error_conf,
)
if (no_response_conf := config.get(CONF_ON_NO_RESPONSE)) is not None:
@@ -229,20 +228,18 @@ async def register_client_action(
else:
await automation.build_automation(
var.get_no_response_trigger(),
[_ADDRESS_ARG, (_PDU_SPAN, "request")],
[(_PDU_SPAN, "request")],
no_response_conf,
)
retry_conf = no_response_conf.get(CONF_RETRY)
if retry_conf is not None:
retry_lambda = await cg.process_lambda(
retry_conf, [_ADDRESS_ARG, (_PDU_SPAN, "request")], return_type=cg.bool_
retry_conf, [(_PDU_SPAN, "request")], return_type=cg.bool_
)
cg.add(var.set_retry(retry_lambda))
if not_sent_conf := config.get(CONF_ON_NOT_SENT):
await automation.build_automation(
var.get_not_sent_trigger(),
[_ADDRESS_ARG, (_PDU_SPAN, "request")],
not_sent_conf,
var.get_not_sent_trigger(), [(_PDU_SPAN, "request")], not_sent_conf
)
# Wire any command options the action's schema opted into (e.g. continuous on reads). Pass the
# matching direction so a write action never generates a read option's setter.
@@ -268,7 +265,7 @@ async def modbus_client_send_to_code(config, action_id, template_arg, args):
var,
config,
args,
[_ADDRESS_ARG, (_PDU_SPAN, "request"), (_PDU_SPAN, "response")],
[(_PDU_SPAN, "request"), (_PDU_SPAN, "response")],
)
@@ -13,51 +13,45 @@ namespace esphome::modbus_client {
/// the hub routes every reply (or its lack) straight back to the action that sent it, so there is no
/// central client object and no request matching. The device address is templatable; it is stamped on the
/// device at play() time; the hub routes each reply by device pointer, so a changed address never
/// mis-routes an earlier reply. The handlers wired here and send's on_response get `address` first: the
/// address of their own request, which the hub takes from its frame, so overlapping sends each report their own.
/// mis-routes an earlier reply. (The address is not passed to the reply triggers - under overlapping
/// sends it could misreport, and the handler can recompute the expression it configured.)
template<typename... Ts> class ClientActionBase : public Action<Ts...>, public modbus::ModbusClientDevice {
public:
TEMPLATABLE_VALUE(uint8_t, target_address) // the modbus device address
Trigger<uint8_t, std::span<const uint8_t>> *get_sent_trigger() { return &this->sent_trigger_; }
Trigger<uint8_t, std::span<const uint8_t>, modbus::ExceptionCode> *get_error_trigger() {
return &this->error_trigger_;
}
Trigger<uint8_t, std::span<const uint8_t>> *get_no_response_trigger() { return &this->no_response_trigger_; }
Trigger<uint8_t, std::span<const uint8_t>> *get_not_sent_trigger() { return &this->not_sent_trigger_; }
Trigger<std::span<const uint8_t>> *get_sent_trigger() { return &this->sent_trigger_; }
Trigger<std::span<const uint8_t>, modbus::ExceptionCode> *get_error_trigger() { return &this->error_trigger_; }
Trigger<std::span<const uint8_t>> *get_no_response_trigger() { return &this->no_response_trigger_; }
Trigger<std::span<const uint8_t>> *get_not_sent_trigger() { return &this->not_sent_trigger_; }
/// The retry decision for on_no_response: given the request (address, PDU), return true to have the hub re-queue
/// The retry decision for on_no_response: given the request PDU, return true to have the hub re-queue
/// the frame. Set from the lambda form or a then: automation's nested retry lambda; may coexist with
/// the no_response trigger (actions run, then this decides the retry).
using retry_func_t = bool (*)(uint8_t, std::span<const uint8_t>);
using retry_func_t = bool (*)(std::span<const uint8_t>);
void set_retry(retry_func_t f) { this->retry_func_ = f; }
/// The frame was written to the wire: fires once per transmission, before any reply, and never for a
/// send that ended in on_not_sent. request_pdu is the PDU sent (function code + data).
void on_sent(uint8_t address, std::span<const uint8_t> request_pdu) override {
this->sent_trigger_.trigger(address, request_pdu);
}
void on_sent(std::span<const uint8_t> request_pdu) override { this->sent_trigger_.trigger(request_pdu); }
/// Never reached the wire, from either of two sources. The hub calls this for a request it accepted
/// and then dropped, which happens only when clear_tx_queue_for_address() retires it - a modbus
/// device going offline, say. Everything the hub refuses at the door instead returns false from
/// queue_pdu() with no callback at all, so send_or_resolve_() below turns those into this same
/// callback: a full queue, a duplicate write, or an empty PDU from a rejecting builder.
void on_not_sent(uint8_t address, std::span<const uint8_t> request_pdu) override {
this->not_sent_trigger_.trigger(address, request_pdu);
}
void on_not_sent(std::span<const uint8_t> request_pdu) override { this->not_sent_trigger_.trigger(request_pdu); }
/// A Modbus exception reply. Lives here beside its trigger so every action subclass gets the pairing:
/// register_client_action() wires on_error for all of them, so a derived class must not have to
/// remember the override.
void on_error(uint8_t address, std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override {
this->error_trigger_.trigger(address, request_pdu, exception_code);
void on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override {
this->error_trigger_.trigger(request_pdu, exception_code);
}
/// No reply within send_wait_time. Run the on_no_response actions (empty in the pure-lambda form),
/// then let the retry lambda, if set, decide whether the hub re-queues the frame (true = retry). The
/// two coexist: a then: automation can also carry a retry lambda. No lambda = no retry.
bool on_no_response(uint8_t address, std::span<const uint8_t> request_pdu) override {
this->no_response_trigger_.trigger(address, request_pdu);
bool on_no_response(std::span<const uint8_t> request_pdu) override {
this->no_response_trigger_.trigger(request_pdu);
if (this->retry_func_ != nullptr)
return this->retry_func_(address, request_pdu);
return this->retry_func_(request_pdu);
return false;
}
/// Stamp the templated device address before every play(): subclasses cannot forget it, and the hub
@@ -76,13 +70,13 @@ template<typename... Ts> class ClientActionBase : public Action<Ts...>, public m
/// a PduBuffer parameter would widen each one to the 253-byte maximum just to cross the call.
void send_or_resolve_(std::span<const uint8_t> pdu, modbus::CommandOptions options = {}) {
if (!this->queue_pdu(pdu, options))
this->on_not_sent(this->address_, pdu);
this->on_not_sent(pdu);
}
Trigger<uint8_t, std::span<const uint8_t>> sent_trigger_;
Trigger<uint8_t, std::span<const uint8_t>, modbus::ExceptionCode> error_trigger_;
Trigger<uint8_t, std::span<const uint8_t>> no_response_trigger_;
Trigger<uint8_t, std::span<const uint8_t>> not_sent_trigger_;
Trigger<std::span<const uint8_t>> sent_trigger_;
Trigger<std::span<const uint8_t>, modbus::ExceptionCode> error_trigger_;
Trigger<std::span<const uint8_t>> no_response_trigger_;
Trigger<std::span<const uint8_t>> not_sent_trigger_;
retry_func_t retry_func_{nullptr};
};
@@ -125,7 +119,7 @@ template<typename... Ts> class WriteCommandOptions {
};
/// modbus_client.send: fire a raw PDU (function code + data; the hub adds address and CRC). The reply is
/// delivered raw - on_response(address, request, response) - deliberately bypassing the typed dispatch, so
/// delivered raw - on_response(request, response) - deliberately bypassing the typed dispatch, so
/// non-standard/custom transactions pass through untouched.
/// The PDU is a stack-allocated modbus::helpers::PduBuffer, so a pdu lambda can build one with the
/// modbus::helpers::create_*_pdu() builders and return it directly (smaller builder results convert).
@@ -139,7 +133,7 @@ class ModbusClientSendAction : public ClientActionBase<Ts...>,
public:
TEMPLATABLE_VALUE(modbus::helpers::PduBuffer, pdu)
Trigger<uint8_t, std::span<const uint8_t>, std::span<const uint8_t>> *get_response_trigger() {
Trigger<std::span<const uint8_t>, std::span<const uint8_t>> *get_response_trigger() {
return &this->response_trigger_;
}
@@ -149,13 +143,12 @@ class ModbusClientSendAction : public ClientActionBase<Ts...>,
this->send_or_resolve_(this->pdu_.value(x...), options);
}
void on_response(uint8_t address, std::span<const uint8_t> request_pdu,
std::span<const uint8_t> response_pdu) override {
this->response_trigger_.trigger(address, request_pdu, response_pdu);
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
this->response_trigger_.trigger(request_pdu, response_pdu);
}
protected:
Trigger<uint8_t, std::span<const uint8_t>, std::span<const uint8_t>> response_trigger_;
Trigger<std::span<const uint8_t>, std::span<const uint8_t>> response_trigger_;
};
/// Typed actions: these do NOT override the raw on_response, so the base ModbusClientDevice default runs
+16 -10
View File
@@ -2,18 +2,24 @@ import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import CONF_SOURCE_ID
from esphome.types import ConfigType
from .. import Number
from .. import Number, number_ns
CONFIG_SCHEMA = sensor.sensor_schema(sensor.Sensor).extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(Number),
}
NumberSensor = number_ns.class_("NumberSensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = (
sensor.sensor_schema(NumberSensor)
.extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(Number),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
# Number has the same float state callback as Sensor
sensor.follow_sensor(var, await cg.get_variable(config[CONF_SOURCE_ID]))
async def to_code(config):
source = await cg.get_variable(config[CONF_SOURCE_ID])
var = await sensor.new_sensor(config, source)
await cg.register_component(var, config)
@@ -0,0 +1,16 @@
#include "number_sensor.h"
#include "esphome/core/log.h"
namespace esphome::number {
ESPHOME_LOG_TAG(TAG, "number.sensor");
void NumberSensor::setup() {
this->source_->add_on_state_callback([this](float value) { this->publish_state(value); });
if (this->source_->has_state())
this->publish_state(this->source_->state);
}
void NumberSensor::dump_config() { LOG_SENSOR("", "Number Sensor", this); }
} // namespace esphome::number
@@ -0,0 +1,19 @@
#pragma once
#include "../number.h"
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::number {
class NumberSensor final : public sensor::Sensor, public Component {
public:
explicit NumberSensor(Number *source) : source_(source) {}
void setup() override;
void dump_config() override;
protected:
Number *source_;
};
} // namespace esphome::number
@@ -3004,73 +3004,3 @@ register_protocol_action(
(CONF_CHANNELS, "channels", cg.uint16),
(CONF_COMMAND, "command", cg.uint8),
)
# Cardin S438
(
CardinS438Data,
CardinS438BinarySensor,
CardinS438Trigger,
_,
CardinS438Dumper,
) = declare_protocol("CardinS438")
CARDIN_S438_CHANNELS = {
"A": 0x02,
"B": 0x00,
"C": 0x03,
"D": 0x01,
}
def _validate_cardin_s438_code(code):
"""Accept the DIP switches as a 14-character binary string."""
code = cv.string_strict(code)
if len(code) != 14 or any(bit not in "01" for bit in code):
raise cv.Invalid("code must be exactly 14 binary digits, e.g. '10100110101010'")
return int(code, 2)
CARDIN_S438_SCHEMA = cv.Schema(
{
cv.Required(CONF_CODE): _validate_cardin_s438_code,
cv.Optional(CONF_CHANNEL, default="A"): cv.enum(
CARDIN_S438_CHANNELS, upper=True
),
cv.Optional(CONF_COMMAND_REPEATS, default=3): cv.int_range(min=1, max=255),
}
)
@register_binary_sensor("cardin_s438", CardinS438BinarySensor, CARDIN_S438_SCHEMA)
def cardin_s438_binary_sensor(var: MockObj, config: ConfigType) -> None:
cg.add(
var.set_data(
cg.StructInitializer(
CardinS438Data,
("code", config[CONF_CODE]),
("channel", config[CONF_CHANNEL]),
)
)
)
@register_trigger("cardin_s438", CardinS438Trigger, CardinS438Data)
def cardin_s438_trigger(var: MockObj, config: ConfigType) -> None:
"""The trigger takes no options beyond the automation."""
@register_dumper("cardin_s438", CardinS438Dumper)
def cardin_s438_dumper(var: MockObj, config: ConfigType) -> None:
"""The dumper takes no options."""
register_protocol_action(
"cardin_s438",
"CardinS438",
CARDIN_S438_SCHEMA,
(CONF_CODE, "code", cg.uint16),
(CONF_CHANNEL, "channel", cg.uint8),
(CONF_COMMAND_REPEATS, "repeats", cg.uint8),
)
@@ -1,159 +0,0 @@
#include "cardin_s438_protocol.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::remote_base {
ESPHOME_LOG_TAG(TAG, "remote.cardin_s438");
namespace {
/*
* Cardin S438 wire format
*
* Frame: 6400 us sync mark + 40 data bits (5 bytes, LSB-first).
* 0 = 1460 us space, 750 us mark
* 1 = 750 us space, 1460 us mark
*
* Bytes:
* [0..2] Encoded pin fields and seed
* [2] (seed + marker 0x87), modulo 256
* Carries propagate into bytes 1 and 0.
* [3..4] Sum of bytes [0..2], little-endian
*
* Pin code: 14 bits, MSB-first, mapped to X/Y fields:
* Y7,Y6,X7,X6,Y5,X5,Y4,X4,Y3,X3,Y2,Y1,Y0,X2
* Channels in X bits 1:0: A=10, B=00, C=11, D=01.
*
* Three frames per transmission, with 63650 us LOW gaps.
* Seed chosen randomly; no pattern found in captures.
*/
// Frame format and timing, in microseconds.
constexpr uint8_t MARKER = 0x87;
constexpr uint16_t SYNC_MARK_US = 6400;
constexpr uint32_t FRAME_GAP_US = 63650;
constexpr uint8_t WIRE_SIZE = 5;
// Each bit is a space then a mark, and both lengths carry it
constexpr BitTimings CARDIN_TIMINGS{
.one_space = 750, .zero_space = 1460, .one_mark = 1460, .zero_mark = 750, .space_first = true};
// Maps pincode bits, most-significant first, to a bit of the X or Y field: PIN_Y marks Y, the low bits are the bit
// number. In flash, so it stays out of RAM on ESP8266.
constexpr uint8_t PIN_Y = 0x08;
constexpr uint8_t PINCODE_BITS = 14;
constexpr uint8_t PINCODE_MAP[PINCODE_BITS] PROGMEM = {
PIN_Y | 7, PIN_Y | 6, 7, 6, PIN_Y | 5, 5, PIN_Y | 4, 4, PIN_Y | 3, 3, PIN_Y | 2, PIN_Y | 1, PIN_Y | 0, 2,
};
// Convert the pincode and channel to the two decoded pin fields.
void encode_pin_fields(uint16_t pincode, uint8_t channel, uint8_t &plain0, uint8_t &plain1) {
uint8_t x = channel;
uint8_t y = 0;
for (uint8_t i = 0; i < PINCODE_BITS; ++i) {
if (((pincode >> (PINCODE_BITS - 1 - i)) & 1U) == 0)
continue;
const uint8_t entry = progmem_read_byte(&PINCODE_MAP[i]);
(entry & PIN_Y ? y : x) |= 1U << (entry & 0x07);
}
plain1 = ~y;
plain0 = plain1 - x + 1;
}
// Construct the wire bytes for one seed.
void build_wire(uint8_t wire[WIRE_SIZE], uint8_t seed, uint8_t plain0, uint8_t plain1) {
uint16_t sum = MARKER + seed;
wire[2] = sum;
uint8_t key = seed + (sum >> 8);
sum = plain1 + key;
wire[1] = sum;
key += sum >> 8;
wire[0] = plain0 + key;
// checksum over the first three bytes, little-endian.
const uint16_t checksum = sum16_bytes(wire, 3);
wire[3] = checksum;
wire[4] = checksum >> 8;
}
} // namespace
void CardinS438Protocol::encode(RemoteTransmitData *dst, const CardinS438Data &data) {
uint8_t plain0;
uint8_t plain1;
encode_pin_fields(data.code & 0x3FFF, data.channel & 0x03, plain0, plain1);
for (uint8_t frame = 0; frame < data.repeats; ++frame) {
uint8_t wire[WIRE_SIZE];
uint8_t seed = static_cast<uint8_t>(random_uint32() & 0xFF);
build_wire(wire, seed, plain0, plain1);
dst->mark(SYNC_MARK_US);
dst->item_bytes<CARDIN_TIMINGS>(wire, WIRE_SIZE);
dst->space(FRAME_GAP_US);
}
}
optional<CardinS438Data> CardinS438Protocol::decode(RemoteReceiveData src) {
while (src.is_valid()) {
// Find the frame's long sync mark.
if (!src.expect_mark(SYNC_MARK_US)) {
src.advance();
continue;
}
uint8_t wire[WIRE_SIZE];
if (!src.expect_bytes<CARDIN_TIMINGS>(wire, WIRE_SIZE) ||
encode_value_le<uint16_t>(wire + 3) != sum16_bytes(wire, 3)) {
continue;
}
// Recover the seed and undo the encoder's carry transform.
uint8_t seed = wire[2] - MARKER;
uint8_t key = seed;
key += (seed + MARKER) >> 8;
uint8_t plain1 = wire[1] - key;
key += (plain1 + key) >> 8;
uint8_t plain0 = wire[0] - key;
// Recover the original X and Y fields.
uint8_t x = plain1 - plain0 + 1;
uint8_t y = ~plain1;
// Reconstruct the 14-bit code in transmission order.
uint16_t code = 0;
for (const uint8_t &slot : PINCODE_MAP) {
const uint8_t entry = progmem_read_byte(&slot);
code = (code << 1) | (((entry & PIN_Y ? y : x) >> (entry & 0x07)) & 1U);
}
return CardinS438Data{
.code = code,
.channel = static_cast<uint8_t>(x & 0x03),
.seed = seed,
};
}
return {};
}
void CardinS438Protocol::dump(const CardinS438Data &data) {
// The DIP switches as the config takes them; channels as CARDIN_S438_CHANNELS in __init__.py maps them
char code[PINCODE_BITS + 1];
for (uint8_t i = 0; i < PINCODE_BITS; ++i)
code[i] = (data.code >> (PINCODE_BITS - 1 - i)) & 1U ? '1' : '0';
code[PINCODE_BITS] = '\0';
ESP_LOGI(TAG, "Received Cardin S438: code=%s channel=%c seed=0x%02X", code, "BDAC"[data.channel & 0x03], data.seed);
}
} // namespace esphome::remote_base
@@ -1,25 +0,0 @@
#pragma once
#include "esphome/components/remote_base/remote_base.h"
namespace esphome::remote_base {
struct CardinS438Data {
uint16_t code{0};
uint8_t channel{0};
uint8_t seed{0};
uint8_t repeats{3};
bool operator==(const CardinS438Data &rhs) const { return code == rhs.code && channel == rhs.channel; }
};
class CardinS438Protocol : public RemoteProtocol<CardinS438Data> {
public:
void encode(RemoteTransmitData *dst, const CardinS438Data &data);
optional<CardinS438Data> decode(RemoteReceiveData src);
void dump(const CardinS438Data &data);
};
DECLARE_REMOTE_PROTOCOL(CardinS438)
} // namespace esphome::remote_base
@@ -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
@@ -8,8 +8,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <new>
#ifdef USE_ESP_IDF
#include "esp_task_wdt.h"
#endif
@@ -96,38 +94,7 @@ static int draw_callback(JPEGDRAW *jpeg) {
return 1;
}
JpegDecoder::JpegDecoder(RuntimeImage *image) : ImageDecoder(image, JPEG) {
RAMAllocator<JPEGDEC> allocator;
JPEGDEC *jpeg = allocator.allocate(1);
if (jpeg == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for the JPEG decoder");
return;
}
this->jpeg_ = new (jpeg) JPEGDEC();
}
JpegDecoder::~JpegDecoder() {
if (this->jpeg_ != nullptr) {
this->jpeg_->~JPEGDEC();
RAMAllocator<JPEGDEC> allocator;
allocator.deallocate(this->jpeg_, 1);
}
}
int JpegDecoder::prepare(size_t expected_size) {
// Check before the base prepare() so a failure never leaves an active session
if (this->jpeg_ == nullptr) {
ESP_LOGE(TAG, "JPEG decoder engine not initialized!");
return DECODE_ERROR_OUT_OF_MEMORY;
}
return ImageDecoder::prepare(expected_size);
}
int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
if (this->jpeg_ == nullptr) {
ESP_LOGE(TAG, "JPEG decoder engine not initialized!");
return DECODE_ERROR_OUT_OF_MEMORY;
}
// JPEG decoder requires complete data
// If we know the expected size, wait for it
if (this->expected_size_ > 0 && size < this->expected_size_) {
@@ -138,22 +105,22 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
// If size unknown, try to decode and see if it's valid
// The JPEGDEC library will fail gracefully if data is incomplete
if (!this->jpeg_->openRAM(buffer, size, draw_callback)) {
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_->getLastError());
if (!this->jpeg_.openRAM(buffer, size, draw_callback)) {
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
return DECODE_ERROR_INVALID_TYPE;
}
const int err = this->start_();
if (err != 0)
return err;
if (!this->jpeg_->decode(0, 0, this->scale_option_()))
if (!this->jpeg_.decode(0, 0, this->scale_option_()))
return this->fail_();
this->decoded_bytes_ = size;
this->jpeg_->close();
this->jpeg_.close();
return size;
}
int JpegDecoder::start_() {
auto jpeg_type = this->jpeg_->getJPEGType();
auto jpeg_type = this->jpeg_.getJPEGType();
if (jpeg_type == JPEG_MODE_INVALID) {
ESP_LOGE(TAG, "Unsupported JPEG image");
return DECODE_ERROR_INVALID_TYPE;
@@ -161,12 +128,11 @@ int JpegDecoder::start_() {
ESP_LOGE(TAG, "Progressive JPEG images not supported");
return DECODE_ERROR_UNSUPPORTED_FORMAT;
}
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_->getWidth(), this->jpeg_->getHeight(),
this->jpeg_->getBpp());
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
this->jpeg_->setUserPointer(this);
const int width = this->jpeg_->getWidth();
const int height = this->jpeg_->getHeight();
this->jpeg_.setUserPointer(this);
const int width = this->jpeg_.getWidth();
const int height = this->jpeg_.getHeight();
// The tables for drawing are only built once the decoded size is known, below
if (!this->resize_image_(width, height)) {
return DECODE_ERROR_OUT_OF_MEMORY;
@@ -189,17 +155,17 @@ int JpegDecoder::start_() {
if (this->direct_rgb565_) {
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
this->jpeg_->setPixelType(RGB565_LITTLE_ENDIAN);
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
} else {
this->jpeg_->setPixelType(RGB8888);
this->jpeg_.setPixelType(RGB8888);
}
return 0;
}
int JpegDecoder::fail_() {
const bool read_failed = this->source_ != nullptr && this->source_failed_;
const int error = this->jpeg_->getLastError();
this->jpeg_->close();
const int error = this->jpeg_.getLastError();
this->jpeg_.close();
this->source_ = nullptr;
if (read_failed) {
ESP_LOGE(TAG, "Reading the image failed");
@@ -223,7 +189,7 @@ int JpegDecoder::fail_() {
void JpegDecoder::reset() {
// A decode from a source that was given up part way still has the image open
if (this->source_ != nullptr) {
this->jpeg_->close();
this->jpeg_.close();
this->source_ = nullptr;
}
ImageDecoder::reset();
@@ -252,10 +218,6 @@ int32_t JpegDecoder::source_seek(JPEGFILE *file, int32_t position) {
}
int JpegDecoder::begin_source(DecodeSource *source) {
if (this->jpeg_ == nullptr) {
ESP_LOGE(TAG, "JPEG decoder engine not initialized!");
return DECODE_ERROR_OUT_OF_MEMORY;
}
if (this->expected_size_ == 0 || this->expected_size_ > INT32_MAX) {
ESP_LOGE(TAG, "Image size %zu cannot be read from a source", this->expected_size_);
return DECODE_ERROR_INVALID_TYPE;
@@ -266,19 +228,19 @@ int JpegDecoder::begin_source(DecodeSource *source) {
}
this->source_ = source;
this->source_failed_ = false;
if (!this->jpeg_->open(this, this->expected_size_, source_close, source_read, source_seek, draw_callback)) {
if (!this->jpeg_.open(this, this->expected_size_, source_close, source_read, source_seek, draw_callback)) {
// The caller may recover, for example by reading the whole image first, so it reports the failure
ESP_LOGD(TAG, "Could not open image for decoding: %d", this->jpeg_->getLastError());
ESP_LOGD(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
this->source_ = nullptr;
return this->source_failed_ ? DECODE_ERROR_READ : DECODE_ERROR_INVALID_TYPE;
}
const int err = this->start_();
if (err != 0) {
this->jpeg_->close();
this->jpeg_.close();
this->source_ = nullptr;
return err;
}
if (!this->jpeg_->decodeBegin(0, 0, this->scale_option_()))
if (!this->jpeg_.decodeBegin(0, 0, this->scale_option_()))
return this->fail_();
return 0;
}
@@ -286,7 +248,7 @@ int JpegDecoder::begin_source(DecodeSource *source) {
int HOT JpegDecoder::decode_step() {
if (this->source_ == nullptr)
return DECODE_ERROR_INVALID_TYPE;
const int result = this->jpeg_->decodeRows(1);
const int result = this->jpeg_.decodeRows(1);
// JPEGDEC may decode on with the missing data, so check the read before its result
if (this->source_failed_)
return this->fail_();
@@ -297,7 +259,7 @@ int HOT JpegDecoder::decode_step() {
if (result == 0)
return this->fail_();
this->decoded_bytes_ = this->expected_size_;
this->jpeg_->close();
this->jpeg_.close();
this->source_ = nullptr;
return DECODE_STEP_DONE;
}
@@ -18,10 +18,9 @@ class JpegDecoder : public ImageDecoder {
*
* @param image The RuntimeImage to decode the stream into.
*/
JpegDecoder(RuntimeImage *image);
~JpegDecoder() override;
JpegDecoder(RuntimeImage *image) : ImageDecoder(image, JPEG) {}
~JpegDecoder() override {}
int prepare(size_t expected_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
void reset() override;
@@ -47,8 +46,7 @@ class JpegDecoder : public ImageDecoder {
static int32_t source_seek(JPEGFILE *file, int32_t position);
static void source_close(void *handle) {}
/// About 18 KB of decoder state, allocated like pngle in PngDecoder: PSRAM first, nullptr if out of memory
JPEGDEC *jpeg_{nullptr};
JPEGDEC jpeg_{};
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
bool source_failed_{false};
+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
@@ -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.safe_shutdown(TEARDOWN_TIMEOUT_REBOOT_MS);
#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"),
)
@@ -2,22 +2,29 @@ import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_SOURCE_ID
from esphome.core.entity_helpers import FollowEntity
from esphome.types import ConfigType
from .. import Switch
from .. import Switch, switch_ns
CODEOWNERS = ["@ssieb"]
_FOLLOW_SWITCH = FollowEntity("add_on_state_callback", ((cg.bool_, "x"),), "x")
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(binary_sensor.BinarySensor).extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(Switch),
}
SwitchBinarySensor = switch_ns.class_(
"SwitchBinarySensor", binary_sensor.BinarySensor, cg.Component
)
async def to_code(config: ConfigType) -> None:
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(SwitchBinarySensor)
.extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(Switch),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
_FOLLOW_SWITCH(var, await cg.get_variable(config[CONF_SOURCE_ID]))
await cg.register_component(var, config)
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
@@ -0,0 +1,15 @@
#include "switch_binary_sensor.h"
#include "esphome/core/log.h"
namespace esphome::switch_ {
ESPHOME_LOG_TAG(TAG, "switch.binary_sensor");
void SwitchBinarySensor::setup() {
source_->add_on_state_callback([this](bool value) { this->publish_state(value); });
this->publish_state(source_->state);
}
void SwitchBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Switch Binary Sensor", this); }
} // namespace esphome::switch_
@@ -0,0 +1,19 @@
#pragma once
#include "../switch.h"
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::switch_ {
class SwitchBinarySensor final : public binary_sensor::BinarySensor, public Component {
public:
void set_source(Switch *source) { source_ = source; }
void setup() override;
void dump_config() override;
protected:
Switch *source_;
};
} // namespace esphome::switch_
@@ -4,16 +4,23 @@ import esphome.config_validation as cv
from esphome.const import CONF_SOURCE_ID
from esphome.types import ConfigType
from .. import Text
from .. import Text, text_ns
CONFIG_SCHEMA = text_sensor.text_sensor_schema(text_sensor.TextSensor).extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(Text),
}
TextTextSensor = text_ns.class_("TextTextSensor", text_sensor.TextSensor, cg.Component)
CONFIG_SCHEMA = (
text_sensor.text_sensor_schema(TextTextSensor)
.extend(
{
cv.Required(CONF_SOURCE_ID): cv.use_id(Text),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
var = await text_sensor.new_text_sensor(config)
# Text has the same string state callback as TextSensor
text_sensor.follow_text_sensor(var, await cg.get_variable(config[CONF_SOURCE_ID]))
source = await cg.get_variable(config[CONF_SOURCE_ID])
var = await text_sensor.new_text_sensor(config, source)
await cg.register_component(var, config)
@@ -0,0 +1,16 @@
#include "text_text_sensor.h"
#include "esphome/core/log.h"
namespace esphome::text {
ESPHOME_LOG_TAG(TAG, "text.text_sensor");
void TextTextSensor::setup() {
this->source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); });
if (this->source_->has_state())
this->publish_state(this->source_->state);
}
void TextTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Text Text Sensor", this); }
} // namespace esphome::text
@@ -0,0 +1,19 @@
#pragma once
#include "../text.h"
#include "esphome/core/component.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::text {
class TextTextSensor final : public text_sensor::TextSensor, public Component {
public:
explicit TextTextSensor(Text *source) : source_(source) {}
void setup() override;
void dump_config() override;
protected:
Text *source_;
};
} // namespace esphome::text
-2
View File
@@ -96,7 +96,6 @@
#define USE_GPIO_SWITCH_INTERLOCK
#define USE_GRAPH
#define USE_GRAPHICAL_DISPLAY_MENU
#define USE_GREE_FAHRENHEIT
#define USE_GREE_YAW1F
#define USE_HOERMANN_HCP_IDENTITY
#define USE_HOMEASSISTANT_TIME
@@ -202,7 +201,6 @@
#define USE_REMOTE_PROTOCOL_BRENNENSTUHL
#define USE_REMOTE_PROTOCOL_BYRONSX
#define USE_REMOTE_PROTOCOL_CANALSAT
#define USE_REMOTE_PROTOCOL_CARDIN_S438
#define USE_REMOTE_PROTOCOL_COOLIX
#define USE_REMOTE_PROTOCOL_DISH
#define USE_REMOTE_PROTOCOL_DOOYA
+20 -7
View File
@@ -501,7 +501,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)
@@ -509,12 +509,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"
@@ -573,7 +575,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(
@@ -594,8 +598,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)
@@ -616,11 +624,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))
+7 -7
View File
@@ -483,6 +483,13 @@ uint32_t HOT Scheduler::call(uint32_t now) {
// Not reached timeout yet, done for this call
break;
}
// Don't run on failed components (is_item_failed_ exempts SELF_POINTER delays).
if (this->is_item_failed_(item)) {
LockGuard guard{this->lock_};
this->recycle_item_main_loop_(this->pop_raw_locked_());
continue;
}
// Check if item is marked for removal
// This handles two cases:
// 1. Item was marked for removal after cleanup_() but before we got here
@@ -507,13 +514,6 @@ uint32_t HOT Scheduler::call(uint32_t now) {
}
#endif
// Don't run on failed components (is_item_failed_ exempts SELF_POINTER delays).
if (this->is_item_failed_(item)) {
LockGuard guard{this->lock_};
this->recycle_item_main_loop_(this->pop_raw_locked_());
continue;
}
#ifdef ESPHOME_DEBUG_SCHEDULER
{
SchedulerNameLog name_log;
+9 -21
View File
@@ -326,25 +326,6 @@ def load_test_manifest_overrides(
ManifestOverrideLoader = Callable[[list[str]], None]
def resolve_dependencies_with_overrides(
components: list[str], manifest_override_loader: ManifestOverrideLoader
) -> list[str]:
"""Return the components and all their dependencies, with overrides applied.
Overrides are applied before each dependency search, so dependencies added by
any override_manifest(), including one of a component found only as a
dependency, are included. The loader is called again with components it has
already seen, so it must apply each override only once.
"""
resolved = sorted(components)
while True:
manifest_override_loader(resolved)
expanded = sorted(get_all_dependencies(set(resolved)))
if expanded == resolved:
return resolved
resolved = expanded
def compile_and_get_binary(
config: dict,
components: list[str],
@@ -385,12 +366,19 @@ def compile_and_get_binary(
else:
config.setdefault(key, value)
# Apply manifest overrides before dependency resolution so that any
# dependency additions made by override_manifest() are picked up.
manifest_override_loader(components)
# Obtain possible dependencies BEFORE validate_config, because
# get_all_dependencies calls CORE.reset() which clears build_path.
components_with_dependencies = resolve_dependencies_with_overrides(
components, manifest_override_loader
components_with_dependencies: list[str] = sorted(
get_all_dependencies(set(components))
)
# Apply overrides for any transitively discovered dependencies.
manifest_override_loader(components_with_dependencies)
CORE.config_path = tests_dir / "dummy.yaml"
CORE.dashboard = None
@@ -5,8 +5,6 @@
#include <gtest/gtest.h>
#include <chrono>
#include <memory>
#include <string>
#include <thread>
namespace esphome::core::testing {
@@ -43,40 +41,4 @@ TEST(Scheduler, TimeoutAddedByACancelledCallbackIsVisibleAfterCall) {
scheduler.trim_freelist();
}
// A cancelled timeout of a component that has since failed has to be counted as removed when call()
// reaches it, or the leftover count makes the next few cancellations trigger a full cleanup early.
TEST(Scheduler, CancelledItemOfFailedComponentIsCountedAsRemoved) {
Scheduler scheduler;
SchedulerTestComponent healthy;
SchedulerTestComponent failing;
// The healthy item is due first, so cleanup_() stops at it and the cancelled item is only reached in the run loop
scheduler.set_timeout(&healthy, "first", 1, []() {});
scheduler.set_timeout(&failing, "cancelled", 2, []() {});
scheduler.process_to_add();
scheduler.cancel_timeout(&failing, "cancelled");
failing.mark_failed();
std::this_thread::sleep_for(std::chrono::milliseconds(5));
scheduler.call(millis());
// Four cancelled items behind a live one stay below the full cleanup threshold of five, so their callbacks are
// released only once they reach the top of the heap
auto token = std::make_shared<int>(0);
scheduler.set_timeout(&healthy, "keep", 10000, []() {});
static constexpr const char *NAMES[] = {"c0", "c1", "c2", "c3"};
for (const char *name : NAMES)
scheduler.set_timeout(&healthy, name, 20000, [token]() {});
scheduler.process_to_add();
for (const char *name : NAMES)
scheduler.cancel_timeout(&healthy, name);
ASSERT_EQ(token.use_count(), 5);
scheduler.call(millis());
EXPECT_EQ(token.use_count(), 5);
scheduler.cancel_timeout(&healthy, "keep");
scheduler.call(millis());
EXPECT_EQ(token.use_count(), 1);
scheduler.trim_freelist();
}
} // namespace esphome::core::testing
-2
View File
@@ -8,8 +8,6 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.dependencies = manifest.dependencies + ["climate_ir", "climate"]
async def to_code_testing(config: ConfigType) -> None:
# Fahrenheit support is define-gated; emit it so the tests cover that path.
cg.add_define("USE_GREE_FAHRENHEIT")
# The YAW1F follow-up message is compiled in only for configs that use that model
cg.add_define("USE_GREE_YAW1F")
-7
View File
@@ -4,13 +4,6 @@ climate:
id: my_gree_ac
model: YAN
transmitter_id: xmitr
# YX1FF, the model use_fahrenheit was verified on; it has no mode switches, so they stay on YAN
- platform: gree
name: GREE Fahrenheit
id: my_gree_ac_fahrenheit
model: YX1FF
use_fahrenheit: true
transmitter_id: xmitr
switch:
- platform: gree
-71
View File
@@ -1,10 +1,8 @@
#include <gtest/gtest.h>
#include <array>
#include <cmath>
#include "esphome/components/gree/gree.h"
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/helpers.h"
namespace esphome::gree::testing {
@@ -26,75 +24,6 @@ TEST(GreeClimateTest, HeatCoolOverrideAdvertisedWithoutHeat) {
EXPECT_FALSE(traits.supports_mode(climate::CLIMATE_MODE_HEAT));
}
#ifdef USE_GREE_FAHRENHEIT
// Expectations from frames captured off a real YX1FF remote and the panel of a Blueridge mini-split.
TEST(GreeClimateTest, FahrenheitHalfStepsMatchCapturedRemoteFrames) {
struct Expectation {
float celsius;
uint8_t temp_field;
bool extra_degree;
};
const Expectation expectations[] = {
{18.33f, 2, true}, // 65 F -- captured remote frame b1=02 b3=5C
{18.89f, 3, false}, // 66 F -- captured remote frame b1=03 b3=58
{21.67f, 6, false}, // 71 F
{22.22f, 6, true}, // 72 F
{25.0f, 9, false}, // 77 F
{25.56f, 10, false}, // 78 F
};
for (const auto &expectation : expectations) {
const uint8_t half_steps = fahrenheit_half_steps(expectation.celsius);
EXPECT_EQ(expectation.temp_field, half_steps >> 1) << "celsius " << expectation.celsius;
EXPECT_EQ(expectation.extra_degree, (half_steps & 1) != 0) << "celsius " << expectation.celsius;
}
}
TEST(GreeClimateTest, FahrenheitHalfStepsCoverEveryWholeDegreeInRange) {
// 61..86 F is the range a YX1FF remote offers.
for (int fahrenheit = 61; fahrenheit <= 86; fahrenheit++) {
const uint8_t half_steps = fahrenheit_half_steps(fahrenheit_to_celsius(static_cast<float>(fahrenheit)));
const float encoded = static_cast<float>(GREE_TEMP_MIN) + 0.5f * static_cast<float>(half_steps);
EXPECT_EQ(fahrenheit, static_cast<int>(celsius_to_fahrenheit(encoded))) << "fahrenheit " << fahrenheit;
}
}
TEST(GreeClimateTest, FahrenheitHalfStepsHandleNonFiniteTarget) {
// transmit_state() can run from setup() via restore_state_(), when the target may still be NaN.
EXPECT_EQ(0, fahrenheit_half_steps(NAN));
}
#endif // USE_GREE_FAHRENHEIT
// Byte 7 is zeroed in these captured YX1FF frames because frame_checksum() computes it.
TEST(GreeClimateTest, FrameChecksumMatchesCapturedFrames) {
const uint8_t frame_65f[GREE_STATE_FRAME_SIZE] = {0x09, 0x02, 0x60, 0x5C, 0x00, 0x20, 0x00, 0x00};
EXPECT_EQ(0x30, frame_checksum(frame_65f));
const uint8_t frame_78f[GREE_STATE_FRAME_SIZE] = {0x09, 0x0A, 0x60, 0x58, 0x00, 0x20, 0x00, 0x00};
EXPECT_EQ(0x70, frame_checksum(frame_78f));
}
// The removed yan/yx1ff checksum `(b0 << 4) + (b1 << 4) + 0xC0` equals frame_checksum() for every frame they emit.
TEST(GreeClimateTest, FrameChecksumMatchesTheRemovedFoldedFormula) {
int mismatches = 0;
for (int byte0 = 0; byte0 < 256; byte0++) {
for (int byte1 = 0; byte1 < 256; byte1++) {
// byte 2 low nibble is always 0
static constexpr uint8_t BYTE2_VALUES[] = {0x00, 0x20, 0x60, 0x70};
for (const uint8_t byte2 : BYTE2_VALUES) {
for (uint8_t byte4 = 0; byte4 <= GREE_VDIR_DOWN; byte4++) {
const uint8_t frame[GREE_STATE_FRAME_SIZE] = {
static_cast<uint8_t>(byte0), static_cast<uint8_t>(byte1), byte2, 0x50, byte4, 0x20, 0x00, 0x00};
const uint8_t folded = static_cast<uint8_t>((byte0 << 4) + (byte1 << 4) + 0xC0);
if (frame_checksum(frame) != folded) {
mismatches++;
}
}
}
}
}
EXPECT_EQ(0, mismatches);
}
class CapturingGreeTransmitter final : public remote_base::RemoteTransmitterBase {
public:
CapturingGreeTransmitter() : RemoteTransmitterBase(nullptr) {}
@@ -7,8 +7,6 @@
#include "common.h"
#include "esphome/components/modbus/modbus.h"
#include "esphome/components/modbus_client/modbus_client.h"
#include "esphome/core/base_automation.h"
#include "esphome/core/hal.h"
namespace esphome::modbus::testing {
@@ -2547,116 +2545,4 @@ TEST(ModbusClientHubAddress, BroadcastReportsAddressZero) {
hub.send_next_for_test();
EXPECT_EQ(device.sent_, BROADCAST_ADDRESS);
}
namespace {
// A handler on one modbus_client trigger that records the address argument of every firing.
template<typename... Ts> struct AddressRecorder {
explicit AddressRecorder(Trigger<uint8_t, Ts...> *trigger) : automation(trigger) {
this->automation.add_action(&this->action);
}
std::vector<uint8_t> addresses;
LambdaAction<uint8_t, Ts...> action{[this](uint8_t address, Ts...) { this->addresses.push_back(address); }};
Automation<uint8_t, Ts...> automation;
};
} // namespace
// One action, overlapping sends with a moving address: each handler reports its own request's address, not the
// action's latest stamp. The requests dropped in between cover on_not_sent from the hub.
TEST(ModbusClientHubRequestAddress, ActionHandlersReportTheirOwnRequestsAddress) {
NullUART uart;
NoResponseProbeHub hub;
hub.set_uart_parent(&uart);
hub.setup();
modbus_client::ModbusClientSendAction<> action;
action.set_parent(&hub);
action.set_pdu([]() { return read_pdu(); });
static uint8_t target;
action.set_target_address([]() -> uint8_t { return target; });
AddressRecorder<std::span<const uint8_t>> sent(action.get_sent_trigger());
AddressRecorder<std::span<const uint8_t>, std::span<const uint8_t>> answered(action.get_response_trigger());
AddressRecorder<std::span<const uint8_t>, ExceptionCode> failed(action.get_error_trigger());
AddressRecorder<std::span<const uint8_t>> unanswered(action.get_no_response_trigger());
AddressRecorder<std::span<const uint8_t>> dropped(action.get_not_sent_trigger());
const uint8_t exception_response[] = {0x83, 0x02};
auto drop = [&hub](uint8_t address) {
hub.clear_tx_queue_for_address(address);
hub.sweep_for_test();
};
for (target = 0x02; target <= 0x08; target++)
action.play_complex(); // the stamp ends at 0x08, which no handler below may report
hub.send_next_for_test();
drop(0x05);
hub.receive_frame_for_test(0x02, OK_RESPONSE);
hub.send_next_for_test();
drop(0x06);
hub.receive_frame_for_test(0x03, exception_response);
hub.send_next_for_test();
drop(0x07);
hub.timeout_waiting();
EXPECT_EQ(sent.addresses, (std::vector<uint8_t>{0x02, 0x03, 0x04}));
EXPECT_EQ(answered.addresses, std::vector<uint8_t>{0x02});
EXPECT_EQ(failed.addresses, std::vector<uint8_t>{0x03});
EXPECT_EQ(unanswered.addresses, std::vector<uint8_t>{0x04});
EXPECT_EQ(dropped.addresses, (std::vector<uint8_t>{0x05, 0x06, 0x07}));
}
// A full queue refuses the send at the door: on_not_sent reports the address of the refused request, also
// while the action's address moves from one play to the next.
TEST(ModbusClientHubRequestAddress, FullQueueReportsTheRefusedRequestsAddress) {
NullUART uart;
NoResponseProbeHub hub;
hub.set_uart_parent(&uart);
hub.setup();
modbus_client::ModbusClientSendAction<> action;
action.set_parent(&hub);
action.set_pdu([]() { return read_pdu(); });
static uint8_t target;
action.set_target_address([]() -> uint8_t { return target; });
AddressRecorder<std::span<const uint8_t>> dropped(action.get_not_sent_trigger());
for (target = 1; target <= MODBUS_TX_BUFFER_SIZE; target++)
action.play_complex(); // one entry per address fills the queue
ASSERT_TRUE(dropped.addresses.empty());
target = 200;
action.play_complex();
target = 201;
action.play_complex();
EXPECT_EQ(dropped.addresses, (std::vector<uint8_t>{200, 201}));
}
// The on_no_response handler re-plays the action at 0x03 and the hub refuses it (empty PDU), which reports
// 0x03 to on_not_sent. The retry lambda that runs after the handler still gets 0x02, the timed-out request.
TEST(ModbusClientHubRequestAddress, RetryGetsTheTimedOutRequestsAddress) {
NullUART uart;
NoResponseProbeHub hub;
hub.set_uart_parent(&uart);
hub.setup();
modbus_client::ModbusClientSendAction<> action;
action.set_parent(&hub);
action.set_pdu([]() { return read_pdu(); });
action.set_target_address([]() -> uint8_t { return 0x02; });
static uint8_t retry_address;
action.set_retry([](uint8_t address, std::span<const uint8_t>) {
retry_address = address;
return false;
});
LambdaAction<uint8_t, std::span<const uint8_t>> replay{[&action](uint8_t, std::span<const uint8_t>) {
action.set_target_address([]() -> uint8_t { return 0x03; });
action.set_pdu([]() { return helpers::PduBuffer{}; });
action.play_complex();
}};
Automation<uint8_t, std::span<const uint8_t>> on_no_response(action.get_no_response_trigger());
on_no_response.add_action(&replay);
AddressRecorder<std::span<const uint8_t>> dropped(action.get_not_sent_trigger());
action.play_complex();
hub.send_next_for_test();
hub.timeout_waiting();
EXPECT_EQ(dropped.addresses, std::vector<uint8_t>{0x03});
EXPECT_EQ(retry_address, 0x02);
}
} // namespace esphome::modbus::testing
+8 -9
View File
@@ -1,7 +1,6 @@
# The modbus_client actions are self-contained hub devices: each takes the hub (auto-resolved when there
# is a single modbus client hub) and a templatable device address; no component block is needed. The
# shared handlers (on_sent, on_error, on_no_response, on_not_sent) and send's on_response get `address`
# first: the address of their own request, even when the template has moved on.
# address is not passed back to reply handlers - recompute the configured expression if needed.
# The hub does not bound retries, so a retry lambda must (here: a counter capped at 3), or a dead
# device is retried forever. Reset the counter before the send or on a terminal outcome (on_response)
# so the cap is per transaction, not per device lifetime. Never reset in on_sent: it fires again on
@@ -48,7 +47,7 @@ button:
# on_no_response lambda form: return true to retry. `request` is the timed-out PDU.
on_no_response: !lambda "return !request.empty() && request[0] == 0x03 && id(read_retries)++ < 3;"
# Per-send inline reply handlers (fire-and-continue): they run when this send's outcome is known;
# `address` is the address this send went to.
# the targeted address is not passed back - recompute the configured expression if needed.
# A pdu lambda can hand-assemble bytes or return a modbus::helpers::create_*_pdu() builder result.
- modbus_client.send:
address: 0x01
@@ -59,23 +58,23 @@ button:
pdu: !lambda "return {0x03, 0x00, 0x10, 0x00, 0x01};"
on_sent:
then:
- lambda: 'ESP_LOGI("modbus_client.test", "sent fc 0x%X to %u", request.empty() ? 0 : request[0], address);'
- lambda: 'ESP_LOGI("modbus_client.test", "sent fc 0x%X", request.empty() ? 0 : request[0]);'
on_response:
then:
- lambda: |-
id(combined_retries) = 0;
ESP_LOGI("modbus_client.test", "device %u: got %d bytes", address, (int) response.size());
ESP_LOGI("modbus_client.test", "got %d bytes", (int) response.size());
on_error:
then:
- lambda: 'ESP_LOGW("modbus_client.test", "fc 0x%X exception %d", request.empty() ? 0 : request[0], (int) exception_code);'
# on_no_response combined form: run actions on timeout AND decide the retry via nested retry:.
on_no_response:
then:
- lambda: 'ESP_LOGW("modbus_client.test", "no reply from %u for fc 0x%X", address, request.empty() ? 0 : request[0]);'
retry: !lambda "return address != 0 && !request.empty() && request[0] == 0x03 && id(combined_retries)++ < 3;"
- lambda: 'ESP_LOGW("modbus_client.test", "no reply for fc 0x%X", request.empty() ? 0 : request[0]);'
retry: !lambda "return !request.empty() && request[0] == 0x03 && id(combined_retries)++ < 3;"
on_not_sent:
then:
- lambda: 'ESP_LOGW("modbus_client.test", "not sent to %u: fc 0x%X", address, request.empty() ? 0 : request[0]);'
- lambda: 'ESP_LOGW("modbus_client.test", "not sent fc 0x%X", request.empty() ? 0 : request[0]);'
- platform: template
name: "Typed Actions"
on_press:
@@ -101,7 +100,7 @@ button:
- lambda: 'ESP_LOGI("modbus_client.test", "first=%u n=%u", values[0], (unsigned) values.size());'
on_no_response:
then:
- lambda: 'ESP_LOGW("modbus_client.test", "typed read timeout from %u", address);'
- logger.log: "typed read timeout"
- modbus_client.read_input_registers:
continuous: !lambda "return false;"
address: 0x01
-1
View File
@@ -23,7 +23,6 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
"pioneer_wyt",
"drayton",
"panasonic",
"cardin_s438",
):
request_protocol(protocol)
# receiver_dumpers_test.cpp registers dumpers by hand
@@ -1,186 +0,0 @@
#include <array>
#include <gtest/gtest.h>
#include "esphome/components/remote_base/cardin_s438_protocol.h"
namespace esphome::remote_base::testing {
namespace {
constexpr int32_t SYNC_MARK_US = 6400;
constexpr int32_t SHORT_US = 750;
constexpr int32_t LONG_US = 1460;
constexpr int32_t FRAME_GAP_US = 63650;
constexpr size_t FRAME_TIMINGS_SIZE = 1 + 5 * 8 * 2 + 1;
// Captured frame: FA C3 EC A9 02, seed 0x65.
constexpr std::array<uint8_t, 5> CAPTURED_FRAME = {0xFA, 0xC3, 0xEC, 0xA9, 0x02};
// Reference frame for code 0x11FF, channel A, seed 0x65.
constexpr std::array<uint8_t, 5> REFERENCE_FRAME = {0xC9, 0x05, 0xEC, 0xBA, 0x01};
RawTimings encode(const CardinS438Data &data) {
RemoteTransmitData tx;
CardinS438Protocol().encode(&tx, data);
return tx.get_data();
}
optional<CardinS438Data> decode(const RawTimings &timings) {
return CardinS438Protocol().decode(RemoteReceiveData(timings, 25, TOLERANCE_MODE_PERCENTAGE));
}
RawTimings frame_timings(const std::array<uint8_t, 5> &wire) {
RawTimings timings{SYNC_MARK_US};
for (uint8_t byte : wire) {
for (uint8_t bit = 0; bit < 8; ++bit) {
if ((byte >> bit) & 1U) {
timings.insert(timings.end(), {-SHORT_US, LONG_US});
} else {
timings.insert(timings.end(), {-LONG_US, SHORT_US});
}
}
}
timings.push_back(-FRAME_GAP_US);
return timings;
}
} // namespace
TEST(CardinS438ProtocolTest, DecodesCapturedFrame) {
auto decoded = decode(frame_timings(CAPTURED_FRAME));
ASSERT_TRUE(decoded.has_value());
ASSERT_EQ(decoded->seed, 0x65);
EXPECT_EQ(decoded->code, 0x2E12);
EXPECT_EQ(decoded->channel, 0x02);
}
TEST(CardinS438ProtocolTest, DecodesReferenceFrame) {
auto decoded = decode(frame_timings(REFERENCE_FRAME));
ASSERT_TRUE(decoded.has_value());
EXPECT_EQ(decoded->code, 0x11FF);
EXPECT_EQ(decoded->channel, 0x02);
EXPECT_EQ(decoded->seed, 0x65);
}
TEST(CardinS438ProtocolTest, RoundTrips) {
for (uint16_t code : {0x0000, 0x0001, 0x11FF, 0x2AAA, 0x3FFF}) {
for (uint8_t channel = 0; channel < 4; ++channel) {
CardinS438Data sent{};
sent.code = code;
sent.channel = channel;
sent.repeats = 1;
const RawTimings timings = encode(sent);
ASSERT_EQ(timings.size(), FRAME_TIMINGS_SIZE);
auto decoded = decode(timings);
ASSERT_TRUE(decoded.has_value());
EXPECT_TRUE(*decoded == sent);
}
}
}
TEST(CardinS438ProtocolTest, RejectsBadChecksum) {
auto wire = CAPTURED_FRAME;
wire[3] ^= 1;
EXPECT_FALSE(decode(frame_timings(wire)).has_value());
}
TEST(CardinS438ProtocolTest, SkipsLeadingNoise) {
RawTimings timings{300, -1200, 150, -90};
const RawTimings frame = frame_timings(CAPTURED_FRAME);
timings.insert(timings.end(), frame.begin(), frame.end());
auto decoded = decode(timings);
ASSERT_TRUE(decoded.has_value());
EXPECT_EQ(decoded->code, 0x2E12);
}
TEST(CardinS438ProtocolTest, FindsValidFrameAfterBadChecksum) {
auto bad_wire = CAPTURED_FRAME;
bad_wire[3] ^= 1;
RawTimings timings = frame_timings(bad_wire);
const RawTimings good = frame_timings(CAPTURED_FRAME);
timings.insert(timings.end(), good.begin(), good.end());
EXPECT_TRUE(decode(timings).has_value());
}
TEST(CardinS438ProtocolTest, RejectsBadStart) {
RawTimings timings = frame_timings(CAPTURED_FRAME);
timings[0] = 2000;
EXPECT_FALSE(decode(timings).has_value());
}
TEST(CardinS438ProtocolTest, RejectsTruncatedFrame) {
RawTimings timings = frame_timings(CAPTURED_FRAME);
timings.resize(timings.size() - 6);
EXPECT_FALSE(decode(timings).has_value());
}
TEST(CardinS438ProtocolTest, RejectsBadBitTiming) {
RawTimings timings = frame_timings(CAPTURED_FRAME);
timings[1] = -2000;
EXPECT_FALSE(decode(timings).has_value());
}
TEST(CardinS438ProtocolTest, DecodesSeedCarryBoundaries) {
struct SeedVector {
uint8_t seed;
std::array<uint8_t, 5> wire;
};
// All vectors encode code=0x11FF, channel A.
// 0x78: marker sum is 0xFF; second carry occurs.
// 0x79: marker and second carry both occur.
// 0xFF: marker carry wraps the key to zero.
constexpr SeedVector frames[] = {
{0x78, {0xDC, 0x18, 0xFF, 0xF3, 0x01}},
{0x79, {0xDE, 0x1A, 0x00, 0xF8, 0x00}},
{0xFF, {0x63, 0xA0, 0x86, 0x89, 0x01}},
};
for (const auto &frame : frames) {
const auto decoded = decode(frame_timings(frame.wire));
ASSERT_TRUE(decoded.has_value()) << "seed=0x" << std::hex << unsigned(frame.seed);
EXPECT_EQ(decoded->code, 0x11FF);
EXPECT_EQ(decoded->channel, 0x02);
EXPECT_EQ(decoded->seed, frame.seed);
}
}
TEST(CardinS438ProtocolTest, EncodesRequestedFrameCount) {
for (uint8_t repeats : {1, 3, 5}) {
CardinS438Data sent{};
sent.code = 0x11FF;
sent.channel = 0x02;
sent.repeats = repeats;
const RawTimings timings = encode(sent);
ASSERT_EQ(timings.size(), FRAME_TIMINGS_SIZE * repeats);
// Check each independently seeded frame, without assuming seeds differ.
for (size_t i = 0; i < repeats; ++i) {
const auto first = timings.begin() + i * FRAME_TIMINGS_SIZE;
const RawTimings frame(first, first + FRAME_TIMINGS_SIZE);
const auto decoded = decode(frame);
ASSERT_TRUE(decoded.has_value()) << "frame=" << i;
EXPECT_EQ(decoded->code, sent.code);
EXPECT_EQ(decoded->channel, sent.channel);
}
}
}
} // namespace esphome::remote_base::testing
@@ -207,8 +207,3 @@ on_ira211:
- logger.log:
format: "on_ira211: %u %.1f"
args: ["static_cast<uint8_t>(x.command)", "x.get_temperature()"]
on_cardin_s438:
then:
- logger.log:
format: "on_cardin_s438: %u %u"
args: ["x.code", "x.channel"]
@@ -564,12 +564,6 @@ button:
on_press:
- remote_transmitter.transmit_pd_pioneer:
code: [0x23, 0xCB, 0x26, 0x01, 0x00, 0x24, 0x03, 0x0B, 0x05, 0x00, 0x00, 0x00, 0x80]
- platform: template
name: Cardin S438
on_press:
remote_transmitter.transmit_cardin_s438:
code: "01001001110100"
channel: B
- platform: template
name: Repeat on every protocol
on_press:
@@ -619,12 +613,6 @@ button:
repeat:
times: 2
wait_time: 10ms
- remote_transmitter.transmit_cardin_s438:
code: "01001001110100"
channel: B
repeat:
times: 2
wait_time: 10ms
- remote_transmitter.transmit_coolix:
first: 0xB2BF40
repeat:
@@ -907,10 +895,6 @@ button:
device: !lambda return 0x25;
address: !lambda return 0x01;
command: 0x1C
- remote_transmitter.transmit_cardin_s438:
code: !lambda return 0x1274;
channel: !lambda return 1;
command_repeats: !lambda return 3;
- remote_transmitter.transmit_coolix:
first: !lambda return 0xB2BF40;
second: !lambda return 0xB2BF40;
@@ -1,39 +0,0 @@
#include <gtest/gtest.h>
#include "esphome/components/runtime_image/jpeg_decoder.h"
#include "esphome/components/runtime_image/runtime_image.h"
#ifdef USE_RUNTIME_IMAGE_JPEG
namespace esphome::runtime_image::testing {
// A decoder whose JPEGDEC state is missing, as when its allocation fails. The state is handed back before the base
// destructor runs, so the teardown stays with JpegDecoder.
class StatelessJpegDecoder : public JpegDecoder {
public:
explicit StatelessJpegDecoder(RuntimeImage *image) : JpegDecoder(image), state_(this->jpeg_) {
this->jpeg_ = nullptr;
}
~StatelessJpegDecoder() override { this->jpeg_ = this->state_; }
protected:
JPEGDEC *state_;
};
// The ~18 KB of JPEGDEC state lives in its own allocation, so it can come from PSRAM
TEST(JpegOutOfMemory, DecoderStateIsAllocatedSeparately) { EXPECT_LT(sizeof(JpegDecoder), sizeof(JPEGDEC) / 4); }
TEST(JpegOutOfMemory, MissingStateFailsWithoutStartingASession) {
RuntimeImage img(JPEG, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);
StatelessJpegDecoder decoder(&img);
EXPECT_EQ(decoder.prepare(100), DECODE_ERROR_OUT_OF_MEMORY);
EXPECT_FALSE(decoder.is_active());
uint8_t data[4] = {};
EXPECT_EQ(decoder.decode(data, sizeof(data)), DECODE_ERROR_OUT_OF_MEMORY);
EXPECT_EQ(decoder.begin_source(nullptr), DECODE_ERROR_OUT_OF_MEMORY);
decoder.reset();
}
} // namespace esphome::runtime_image::testing
#endif // USE_RUNTIME_IMAGE_JPEG
@@ -1,56 +0,0 @@
esphome:
name: test-source-entity-follow
host:
api:
batch_delay: 0ms
logger:
switch:
- platform: template
id: source_switch
name: Source Switch
optimistic: true
restore_mode: ALWAYS_ON
# Publishes nothing until commanded
- platform: template
id: quiet_switch
name: Quiet Switch
optimistic: true
restore_mode: DISABLED
number:
- platform: template
id: source_number
name: Source Number
optimistic: true
min_value: 0
max_value: 10
step: 0.5
initial_value: 4
text:
- platform: template
id: source_text
name: Source Text
optimistic: true
mode: text
initial_value: hello
binary_sensor:
- platform: switch
source_id: source_switch
name: Switch State
- platform: switch
source_id: quiet_switch
name: Quiet Switch State
sensor:
- platform: number
source_id: source_number
name: Number Value
text_sensor:
- platform: text
source_id: source_text
name: Text Value
+193 -206
View File
@@ -1,208 +1,195 @@
{
"tests/integration/test_action_concurrent_reentry.py": 15.02,
"tests/integration/test_addressable_light_transition.py": 14.54,
"tests/integration/test_alarm_control_panel_state_transitions.py": 15.84,
"tests/integration/test_api_action_metadata.py": 14.04,
"tests/integration/test_api_action_responses.py": 17.62,
"tests/integration/test_api_action_timeout.py": 15.17,
"tests/integration/test_api_conditional_memory.py": 12.16,
"tests/integration/test_api_cover_toggle.py": 13.64,
"tests/integration/test_api_custom_services.py": 11.96,
"tests/integration/test_api_decode_wire_types.py": 12.88,
"tests/integration/test_api_empty_message_roundtrip.py": 11.72,
"tests/integration/test_api_encode_boundaries.py": 9.99,
"tests/integration/test_api_get_time_response_timezone.py": 12.23,
"tests/integration/test_api_homeassistant.py": 17.98,
"tests/integration/test_api_homeassistant_action_no_subscriber.py": 7.67,
"tests/integration/test_api_homeassistant_binary_sensor_initial_state.py": 13.2,
"tests/integration/test_api_homeassistant_entities.py": 10.65,
"tests/integration/test_api_homeassistant_light.py": 33.13,
"tests/integration/test_api_list_entities_backpressure.py": 10.9,
"tests/integration/test_api_message_size_batching.py": 8.36,
"tests/integration/test_api_outgoing_connection.py": 34.71,
"tests/integration/test_api_reboot_timeout.py": 12.36,
"tests/integration/test_api_string_lambda.py": 9.26,
"tests/integration/test_api_vv_logging.py": 14.12,
"tests/integration/test_api_zero_psk_provisioning.py": 18.15,
"tests/integration/test_areas_and_devices.py": 14.82,
"tests/integration/test_automation_wait_actions.py": 13.62,
"tests/integration/test_automations.py": 22.32,
"tests/integration/test_batch_delay_zero_rapid_transitions.py": 10.22,
"tests/integration/test_binary_sensor_autorepeat_filter.py": 11.78,
"tests/integration/test_binary_sensor_invalidate_state.py": 10.5,
"tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 8.28,
"tests/integration/test_branch_resume_async.py": 13.54,
"tests/integration/test_build_info.py": 8.43,
"tests/integration/test_camera_mock.py": 9.04,
"tests/integration/test_climate_control_action.py": 15.04,
"tests/integration/test_climate_custom_modes.py": 11.12,
"tests/integration/test_continuation_actions.py": 8.87,
"tests/integration/test_copy_entity_state.py": 9.64,
"tests/integration/test_copy_initially_off.py": 10.97,
"tests/integration/test_cover_control_action.py": 13.41,
"tests/integration/test_cover_stop_tilt_cancel.py": 16.62,
"tests/integration/test_crc8_helper.py": 7.35,
"tests/integration/test_device_id_in_state.py": 18.29,
"tests/integration/test_duplicate_entities.py": 12.49,
"tests/integration/test_entity_icon.py": 12.18,
"tests/integration/test_entity_missing_state.py": 9.01,
"tests/integration/test_fan_turn_on_action.py": 12.41,
"tests/integration/test_fnv1_hash_object_id.py": 6.14,
"tests/integration/test_fnv1a_hash.py": 8.0,
"tests/integration/test_gpio_expander_cache.py": 9.51,
"tests/integration/test_host_logger_thread_safety.py": 9.08,
"tests/integration/test_host_mode_basic.py": 10.07,
"tests/integration/test_host_mode_batch_delay.py": 10.04,
"tests/integration/test_host_mode_climate_basic_state.py": 10.36,
"tests/integration/test_host_mode_climate_control.py": 10.68,
"tests/integration/test_host_mode_empty_string_options.py": 9.22,
"tests/integration/test_host_mode_entity_fields.py": 12.65,
"tests/integration/test_host_mode_fan_preset.py": 9.37,
"tests/integration/test_host_mode_many_entities.py": 23.2,
"tests/integration/test_host_mode_many_entities_multiple_connections.py": 14.92,
"tests/integration/test_host_mode_noise_encryption.py": 27.22,
"tests/integration/test_host_mode_reconnect.py": 7.22,
"tests/integration/test_host_mode_sensor.py": 9.89,
"tests/integration/test_host_ota.py": 93.33,
"tests/integration/test_host_preferences.py": 9.34,
"tests/integration/test_host_preferences_suspend_resume.py": 10.46,
"tests/integration/test_host_write_after_peer_close.py": 12.93,
"tests/integration/test_improv_serial_uart.py": 10.68,
"tests/integration/test_in_range_conditions.py": 13.84,
"tests/integration/test_includes_lambda_order.py": 9.07,
"tests/integration/test_ir_rf_transmit_complete.py": 15.53,
"tests/integration/test_large_message_batching.py": 13.44,
"tests/integration/test_legacy_area.py": 8.07,
"tests/integration/test_light_automations.py": 14.26,
"tests/integration/test_light_binary_effect_off_phase.py": 20.03,
"tests/integration/test_light_calls.py": 16.45,
"tests/integration/test_light_constant_brightness.py": 13.09,
"tests/integration/test_light_control_action.py": 13.41,
"tests/integration/test_light_dim_relative_action.py": 10.41,
"tests/integration/test_light_effect_zero_brightness.py": 14.36,
"tests/integration/test_light_initial_state.py": 9.84,
"tests/integration/test_light_repeat_effect.py": 16.56,
"tests/integration/test_light_restore_state.py": 13.07,
"tests/integration/test_light_resume_effect.py": 10.81,
"tests/integration/test_light_toggle_action.py": 10.35,
"tests/integration/test_light_transition_state_publish_interval.py": 43.17,
"tests/integration/test_lock_automations.py": 8.08,
"tests/integration/test_logger_buffered_recursion_guard.py": 11.54,
"tests/integration/test_loop_disable_enable.py": 9.37,
"tests/integration/test_loop_failed_before_setup.py": 6.93,
"tests/integration/test_loop_interval_decoupling.py": 9.68,
"tests/integration/test_loop_interval_default_not_pulled_forward.py": 13.06,
"tests/integration/test_lvgl_color_picker_render.py": 39.23,
"tests/integration/test_lvgl_headless_render.py": 40.05,
"tests/integration/test_micros_to_millis.py": 6.53,
"tests/integration/test_multi_click_trigger.py": 11.46,
"tests/integration/test_multi_device_preferences.py": 11.03,
"tests/integration/test_noise_encryption_key_protection.py": 16.83,
"tests/integration/test_object_id_api_verification.py": 8.86,
"tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 8.93,
"tests/integration/test_object_id_no_friendly_name.py": 11.88,
"tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 24.18,
"tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 20.36,
"tests/integration/test_online_image_bmp.py": 16.14,
"tests/integration/test_online_image_stalled_download.py": 19.52,
"tests/integration/test_oversized_payloads.py": 50.86,
"tests/integration/test_preference_key_stability.py": 14.0,
"tests/integration/test_runtime_stats.py": 13.09,
"tests/integration/test_safe_mode_loop_runs.py": 6.98,
"tests/integration/test_scheduler_background_wake.py": 19.01,
"tests/integration/test_scheduler_blocking_warning.py": 20.57,
"tests/integration/test_scheduler_bulk_cleanup.py": 7.57,
"tests/integration/test_scheduler_defer_cancel.py": 12.57,
"tests/integration/test_scheduler_defer_cancel_regular.py": 8.4,
"tests/integration/test_scheduler_defer_fifo_simple.py": 9.31,
"tests/integration/test_scheduler_defer_stress.py": 10.83,
"tests/integration/test_scheduler_heap_stress.py": 8.74,
"tests/integration/test_scheduler_internal_id_no_collision.py": 13.5,
"tests/integration/test_scheduler_interval_reschedule.py": 8.17,
"tests/integration/test_scheduler_interval_zero_coerced.py": 9.21,
"tests/integration/test_scheduler_loop_phase_timeout.py": 14.95,
"tests/integration/test_scheduler_null_name.py": 13.19,
"tests/integration/test_scheduler_numeric_id_test.py": 9.63,
"tests/integration/test_scheduler_pool.py": 10.74,
"tests/integration/test_scheduler_rapid_cancellation.py": 8.55,
"tests/integration/test_scheduler_recursive_timeout.py": 13.76,
"tests/integration/test_scheduler_removed_item_race.py": 14.2,
"tests/integration/test_scheduler_self_keyed.py": 9.51,
"tests/integration/test_scheduler_simultaneous_callbacks.py": 13.19,
"tests/integration/test_scheduler_string_test.py": 10.9,
"tests/integration/test_script_array_params.py": 12.44,
"tests/integration/test_script_delay_params.py": 12.44,
"tests/integration/test_script_on_stop_and_choose.py": 10.69,
"tests/integration/test_script_queued.py": 11.25,
"tests/integration/test_script_queued_idle_loop.py": 15.05,
"tests/integration/test_script_wait_on_boot.py": 11.97,
"tests/integration/test_sdl_headless_screenshot.py": 14.47,
"tests/integration/test_select_stringref_trigger.py": 13.37,
"tests/integration/test_sensor_filters_delta.py": 13.76,
"tests/integration/test_sensor_filters_rh_correction.py": 9.72,
"tests/integration/test_sensor_filters_ring_buffer.py": 13.07,
"tests/integration/test_sensor_filters_sliding_window.py": 28.7,
"tests/integration/test_sensor_filters_value_list.py": 9.61,
"tests/integration/test_sensor_raw_state.py": 24.98,
"tests/integration/test_sensor_timeout_filter.py": 10.01,
"tests/integration/test_sensor_timeout_throttle_filter.py": 9.86,
"tests/integration/test_set_internal_at_boot.py": 9.58,
"tests/integration/test_snapshot_display.py": 14.37,
"tests/integration/test_socket_ipv4_resolve.py": 11.12,
"tests/integration/test_socket_tcp_client_link.py": 13.42,
"tests/integration/test_socket_wake_gate_tcp.py": 12.61,
"tests/integration/test_socket_wake_gate_udp.py": 8.99,
"tests/integration/test_source_entity_follow.py": 14.73,
"tests/integration/test_status_flags.py": 16.3,
"tests/integration/test_status_momentary.py": 15.28,
"tests/integration/test_strftime_to.py": 10.03,
"tests/integration/test_syslog.py": 17.43,
"tests/integration/test_tcp_uart_bridge.py": 16.25,
"tests/integration/test_tcp_uart_server.py": 6.52,
"tests/integration/test_template_alarm_control_panel_many_sensors.py": 8.11,
"tests/integration/test_template_climate_basic.py": 13.7,
"tests/integration/test_template_climate_custom_modes.py": 6.88,
"tests/integration/test_template_climate_nonoptimistic.py": 12.68,
"tests/integration/test_template_climate_on_control_ordering.py": 12.72,
"tests/integration/test_template_climate_publish_all_fields.py": 10.76,
"tests/integration/test_template_climate_sensor_push.py": 13.52,
"tests/integration/test_template_climate_set_actions.py": 13.41,
"tests/integration/test_template_climate_two_point_temperature.py": 8.4,
"tests/integration/test_template_text_save.py": 12.87,
"tests/integration/test_template_time.py": 13.35,
"tests/integration/test_text_command.py": 11.78,
"tests/integration/test_text_sensor_raw_state.py": 8.44,
"tests/integration/test_uart_mock_bl0906_stray_rx.py": 13.96,
"tests/integration/test_uart_mock_dfrobot_sen0465.py": 18.9,
"tests/integration/test_uart_mock_dfrobot_sen0609.py": 12.01,
"tests/integration/test_uart_mock_duco.py": 37.61,
"tests/integration/test_uart_mock_ld2410.py": 33.42,
"tests/integration/test_uart_mock_ld2410_query_ack.py": 10.85,
"tests/integration/test_uart_mock_ld2410s.py": 49.7,
"tests/integration/test_uart_mock_ld2412.py": 72.78,
"tests/integration/test_uart_mock_ld2420.py": 21.52,
"tests/integration/test_uart_mock_ld2420_config_exit.py": 39.94,
"tests/integration/test_uart_mock_ld2450.py": 11.62,
"tests/integration/test_uart_mock_ld2450_has_target_only.py": 15.91,
"tests/integration/test_uart_mock_ld2450_polygon_zone_restore.py": 9.66,
"tests/integration/test_uart_mock_ld2450_polygon_zone_timeout.py": 14.46,
"tests/integration/test_uart_mock_ld2450_polygon_zones.py": 16.34,
"tests/integration/test_uart_mock_ld2460.py": 13.22,
"tests/integration/test_uart_mock_modbus.py": 249.46,
"tests/integration/test_uart_mock_pylontech.py": 14.81,
"tests/integration/test_uart_mock_ufm01.py": 69.05,
"tests/integration/test_uart_tcp_bridge.py": 7.58,
"tests/integration/test_udp.py": 10.0,
"tests/integration/test_udp_ipv6.py": 11.71,
"tests/integration/test_udp_multicast.py": 12.0,
"tests/integration/test_use_address_runtime.py": 16.14,
"tests/integration/test_valve_control_action.py": 12.37,
"tests/integration/test_varint_five_byte_device_id.py": 9.46,
"tests/integration/test_vnc_client.py": 19.94,
"tests/integration/test_wait_until_mid_loop_timing.py": 8.72,
"tests/integration/test_wait_until_on_boot.py": 4.54,
"tests/integration/test_wait_until_ordering.py": 10.39,
"tests/integration/test_wait_until_reentrant_restart.py": 13.99,
"tests/integration/test_wake_loop_forces_phase_b.py": 10.26,
"tests/integration/test_water_heater_template.py": 20.13
"tests/integration/test_action_concurrent_reentry.py": 14.84,
"tests/integration/test_addressable_light_transition.py": 17.96,
"tests/integration/test_alarm_control_panel_state_transitions.py": 16.42,
"tests/integration/test_api_action_metadata.py": 12.08,
"tests/integration/test_api_action_responses.py": 16.62,
"tests/integration/test_api_action_timeout.py": 17.38,
"tests/integration/test_api_conditional_memory.py": 12.24,
"tests/integration/test_api_custom_services.py": 10.53,
"tests/integration/test_api_decode_wire_types.py": 17.97,
"tests/integration/test_api_empty_message_roundtrip.py": 12.54,
"tests/integration/test_api_encode_boundaries.py": 14.46,
"tests/integration/test_api_get_time_response_timezone.py": 16.43,
"tests/integration/test_api_homeassistant.py": 17.44,
"tests/integration/test_api_homeassistant_action_no_subscriber.py": 11.63,
"tests/integration/test_api_homeassistant_binary_sensor_initial_state.py": 12.61,
"tests/integration/test_api_homeassistant_entities.py": 10.45,
"tests/integration/test_api_list_entities_backpressure.py": 13.7,
"tests/integration/test_api_message_size_batching.py": 9.04,
"tests/integration/test_api_outgoing_connection.py": 26.32,
"tests/integration/test_api_reboot_timeout.py": 11.82,
"tests/integration/test_api_string_lambda.py": 11.94,
"tests/integration/test_api_vv_logging.py": 13.47,
"tests/integration/test_api_zero_psk_provisioning.py": 27.07,
"tests/integration/test_areas_and_devices.py": 9.9,
"tests/integration/test_automation_wait_actions.py": 13.22,
"tests/integration/test_automations.py": 26.92,
"tests/integration/test_batch_delay_zero_rapid_transitions.py": 14.21,
"tests/integration/test_binary_sensor_autorepeat_filter.py": 13.7,
"tests/integration/test_binary_sensor_invalidate_state.py": 13.52,
"tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 12.48,
"tests/integration/test_branch_resume_async.py": 16.18,
"tests/integration/test_build_info.py": 11.15,
"tests/integration/test_camera_mock.py": 13.74,
"tests/integration/test_climate_control_action.py": 14.33,
"tests/integration/test_climate_custom_modes.py": 13.85,
"tests/integration/test_continuation_actions.py": 12.9,
"tests/integration/test_cover_control_action.py": 10.2,
"tests/integration/test_crc8_helper.py": 11.98,
"tests/integration/test_device_id_in_state.py": 25.5,
"tests/integration/test_duplicate_entities.py": 14.91,
"tests/integration/test_entity_icon.py": 15.33,
"tests/integration/test_entity_missing_state.py": 8.06,
"tests/integration/test_fan_turn_on_action.py": 13.45,
"tests/integration/test_fnv1_hash_object_id.py": 10.16,
"tests/integration/test_fnv1a_hash.py": 9.94,
"tests/integration/test_gpio_expander_cache.py": 10.52,
"tests/integration/test_host_logger_thread_safety.py": 11.38,
"tests/integration/test_host_mode_basic.py": 10.96,
"tests/integration/test_host_mode_batch_delay.py": 13.46,
"tests/integration/test_host_mode_climate_basic_state.py": 7.78,
"tests/integration/test_host_mode_climate_control.py": 14.03,
"tests/integration/test_host_mode_empty_string_options.py": 12.45,
"tests/integration/test_host_mode_entity_fields.py": 15.66,
"tests/integration/test_host_mode_fan_preset.py": 9.7,
"tests/integration/test_host_mode_many_entities.py": 23.55,
"tests/integration/test_host_mode_many_entities_multiple_connections.py": 14.87,
"tests/integration/test_host_mode_noise_encryption.py": 29.31,
"tests/integration/test_host_mode_reconnect.py": 7.15,
"tests/integration/test_host_mode_sensor.py": 12.09,
"tests/integration/test_host_ota.py": 307.27,
"tests/integration/test_host_preferences.py": 13.14,
"tests/integration/test_host_preferences_suspend_resume.py": 14.41,
"tests/integration/test_host_write_after_peer_close.py": 13.85,
"tests/integration/test_improv_serial_uart.py": 14.8,
"tests/integration/test_in_range_conditions.py": 13.63,
"tests/integration/test_ir_rf_transmit_complete.py": 17.32,
"tests/integration/test_large_message_batching.py": 15.13,
"tests/integration/test_legacy_area.py": 11.23,
"tests/integration/test_light_automations.py": 8.52,
"tests/integration/test_light_binary_effect_off_phase.py": 20.04,
"tests/integration/test_light_calls.py": 16.91,
"tests/integration/test_light_constant_brightness.py": 10.63,
"tests/integration/test_light_control_action.py": 14.43,
"tests/integration/test_light_dim_relative_action.py": 16.59,
"tests/integration/test_light_effect_zero_brightness.py": 17.36,
"tests/integration/test_light_initial_state.py": 13.38,
"tests/integration/test_light_repeat_effect.py": 13.04,
"tests/integration/test_light_restore_state.py": 14.38,
"tests/integration/test_light_resume_effect.py": 17.23,
"tests/integration/test_light_toggle_action.py": 15.35,
"tests/integration/test_light_transition_state_publish_interval.py": 44.06,
"tests/integration/test_lock_automations.py": 13.01,
"tests/integration/test_logger_buffered_recursion_guard.py": 8.93,
"tests/integration/test_loop_disable_enable.py": 12.38,
"tests/integration/test_loop_interval_decoupling.py": 15.54,
"tests/integration/test_loop_interval_default_not_pulled_forward.py": 13.16,
"tests/integration/test_lvgl_color_picker_render.py": 53.51,
"tests/integration/test_lvgl_headless_render.py": 58.96,
"tests/integration/test_micros_to_millis.py": 2.84,
"tests/integration/test_multi_click_trigger.py": 8.88,
"tests/integration/test_multi_device_preferences.py": 14.72,
"tests/integration/test_noise_encryption_key_protection.py": 21.32,
"tests/integration/test_object_id_api_verification.py": 12.83,
"tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 7.81,
"tests/integration/test_object_id_no_friendly_name.py": 24.12,
"tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 25.21,
"tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 25.14,
"tests/integration/test_online_image_bmp.py": 16.77,
"tests/integration/test_online_image_stalled_download.py": 17.06,
"tests/integration/test_oversized_payloads.py": 37.98,
"tests/integration/test_preference_key_stability.py": 13.43,
"tests/integration/test_runtime_stats.py": 12.02,
"tests/integration/test_safe_mode_loop_runs.py": 11.39,
"tests/integration/test_scheduler_background_wake.py": 17.85,
"tests/integration/test_scheduler_blocking_warning.py": 30.7,
"tests/integration/test_scheduler_bulk_cleanup.py": 12.73,
"tests/integration/test_scheduler_defer_cancel.py": 13.05,
"tests/integration/test_scheduler_defer_cancel_regular.py": 12.12,
"tests/integration/test_scheduler_defer_fifo_simple.py": 12.73,
"tests/integration/test_scheduler_defer_stress.py": 12.9,
"tests/integration/test_scheduler_heap_stress.py": 13.61,
"tests/integration/test_scheduler_internal_id_no_collision.py": 9.06,
"tests/integration/test_scheduler_interval_reschedule.py": 12.19,
"tests/integration/test_scheduler_interval_zero_coerced.py": 3.33,
"tests/integration/test_scheduler_loop_phase_timeout.py": 10.81,
"tests/integration/test_scheduler_null_name.py": 3.45,
"tests/integration/test_scheduler_numeric_id_test.py": 13.88,
"tests/integration/test_scheduler_pool.py": 13.86,
"tests/integration/test_scheduler_rapid_cancellation.py": 4.54,
"tests/integration/test_scheduler_recursive_timeout.py": 4.32,
"tests/integration/test_scheduler_removed_item_race.py": 13.06,
"tests/integration/test_scheduler_self_keyed.py": 4.29,
"tests/integration/test_scheduler_simultaneous_callbacks.py": 12.79,
"tests/integration/test_scheduler_string_test.py": 8.51,
"tests/integration/test_script_array_params.py": 8.05,
"tests/integration/test_script_delay_params.py": 14.57,
"tests/integration/test_script_on_stop_and_choose.py": 14.11,
"tests/integration/test_script_queued.py": 15.68,
"tests/integration/test_script_queued_idle_loop.py": 8.46,
"tests/integration/test_script_wait_on_boot.py": 12.91,
"tests/integration/test_sdl_headless_screenshot.py": 15.05,
"tests/integration/test_select_stringref_trigger.py": 11.48,
"tests/integration/test_sensor_filters_delta.py": 13.28,
"tests/integration/test_sensor_filters_ring_buffer.py": 13.95,
"tests/integration/test_sensor_filters_sliding_window.py": 40.27,
"tests/integration/test_sensor_filters_value_list.py": 14.95,
"tests/integration/test_sensor_raw_state.py": 24.93,
"tests/integration/test_sensor_timeout_filter.py": 14.93,
"tests/integration/test_sensor_timeout_throttle_filter.py": 16.23,
"tests/integration/test_set_internal_at_boot.py": 12.69,
"tests/integration/test_snapshot_display.py": 15.83,
"tests/integration/test_socket_ipv4_resolve.py": 10.41,
"tests/integration/test_socket_tcp_client_link.py": 12.96,
"tests/integration/test_socket_wake_gate_tcp.py": 11.61,
"tests/integration/test_socket_wake_gate_udp.py": 10.45,
"tests/integration/test_status_flags.py": 20.81,
"tests/integration/test_status_momentary.py": 15.46,
"tests/integration/test_strftime_to.py": 12.97,
"tests/integration/test_syslog.py": 21.29,
"tests/integration/test_tcp_uart_bridge.py": 16.41,
"tests/integration/test_tcp_uart_server.py": 13.43,
"tests/integration/test_template_alarm_control_panel_many_sensors.py": 13.34,
"tests/integration/test_template_climate_basic.py": 12.78,
"tests/integration/test_template_climate_custom_modes.py": 14.81,
"tests/integration/test_template_climate_nonoptimistic.py": 8.48,
"tests/integration/test_template_climate_on_control_ordering.py": 7.38,
"tests/integration/test_template_climate_publish_all_fields.py": 8.89,
"tests/integration/test_template_climate_sensor_push.py": 15.73,
"tests/integration/test_template_climate_set_actions.py": 11.23,
"tests/integration/test_template_climate_two_point_temperature.py": 9.47,
"tests/integration/test_template_text_save.py": 13.65,
"tests/integration/test_template_time.py": 9.1,
"tests/integration/test_text_command.py": 12.29,
"tests/integration/test_text_sensor_raw_state.py": 13.19,
"tests/integration/test_uart_mock_bl0906_stray_rx.py": 13.99,
"tests/integration/test_uart_mock_dfrobot_sen0465.py": 20.14,
"tests/integration/test_uart_mock_dfrobot_sen0609.py": 10.57,
"tests/integration/test_uart_mock_ld2410.py": 31.61,
"tests/integration/test_uart_mock_ld2410s.py": 49.83,
"tests/integration/test_uart_mock_ld2412.py": 89.93,
"tests/integration/test_uart_mock_ld2420.py": 20.19,
"tests/integration/test_uart_mock_ld2450.py": 16.45,
"tests/integration/test_uart_mock_ld2450_has_target_only.py": 17.25,
"tests/integration/test_uart_mock_ld2450_polygon_zone_restore.py": 10.7,
"tests/integration/test_uart_mock_ld2450_polygon_zone_timeout.py": 15.95,
"tests/integration/test_uart_mock_ld2450_polygon_zones.py": 11.68,
"tests/integration/test_uart_mock_ld2460.py": 15.46,
"tests/integration/test_uart_mock_modbus.py": 214.58,
"tests/integration/test_uart_mock_pylontech.py": 14.66,
"tests/integration/test_uart_mock_ufm01.py": 70.02,
"tests/integration/test_uart_tcp_bridge.py": 10.46,
"tests/integration/test_udp.py": 9.38,
"tests/integration/test_udp_ipv6.py": 8.74,
"tests/integration/test_udp_multicast.py": 11.71,
"tests/integration/test_use_address_runtime.py": 15.24,
"tests/integration/test_valve_control_action.py": 13.75,
"tests/integration/test_varint_five_byte_device_id.py": 13.56,
"tests/integration/test_wait_until_mid_loop_timing.py": 8.57,
"tests/integration/test_wait_until_on_boot.py": 9.51,
"tests/integration/test_wait_until_ordering.py": 4.48,
"tests/integration/test_wait_until_reentrant_restart.py": 5.63,
"tests/integration/test_wake_loop_forces_phase_b.py": 9.4,
"tests/integration/test_water_heater_template.py": 28.16
}
@@ -1,71 +0,0 @@
"""Integration test for the switch binary sensor, number sensor and text text sensor platforms."""
from __future__ import annotations
import asyncio
from aioesphomeapi import (
BinarySensorState,
EntityState,
NumberInfo,
SensorState,
SwitchInfo,
TextInfo,
TextSensorState,
)
import pytest
from .state_utils import require_entity, subscribe_after_initial_states
from .types import APIClientConnectedFactory, RunCompiledFunction
INITIAL: dict[str, tuple[type[EntityState], float | bool | str | None]] = {
"switch_state": (BinarySensorState, True),
# A switch that has not published leaves its binary sensor unknown
"quiet_switch_state": (BinarySensorState, None),
"number_value": (SensorState, 4.0),
"text_value": (TextSensorState, "hello"),
}
UPDATED: dict[str, tuple[type[EntityState], float | bool | str | None]] = {
"switch_state": (BinarySensorState, False),
"quiet_switch_state": (BinarySensorState, True),
"number_value": (SensorState, 6.5),
"text_value": (TextSensorState, "bye"),
}
@pytest.mark.asyncio
async def test_source_entity_follow(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Each entity holds its source's state from boot, then follows it."""
async with run_compiled(yaml_config), api_client_connected() as client:
entities, _ = await client.list_entities_services()
keys = {
name: require_entity(entities, name, exact=True).key for name in INITIAL
}
switch = require_entity(entities, "source_switch", SwitchInfo)
quiet = require_entity(entities, "quiet_switch", SwitchInfo)
number = require_entity(entities, "source_number", NumberInfo)
text = require_entity(entities, "source_text", TextInfo)
sub = await subscribe_after_initial_states(client, entities)
for name, (cls, state) in INITIAL.items():
key = keys[name]
expected = (
cls(key=key, missing_state=True)
if state is None
else cls(key=key, state=state)
)
assert sub.initial_states[key] == expected, name
names = list(UPDATED)
waits = [sub.waiter.next_state(keys[name]) for name in names]
client.switch_command(switch.key, False)
client.switch_command(quiet.key, True)
client.number_command(number.key, 6.5)
client.text_command(text.key, "bye")
for name, got in zip(names, await asyncio.gather(*waits), strict=True):
cls, state = UPDATED[name]
assert got == cls(key=keys[name], state=state), name
-42
View File
@@ -486,45 +486,3 @@ def test_populate_registers_platform_for_platform_entry() -> None:
assert registered == ["sensor", "binary_sensor"]
assert config["sensor"] == [{"platform": "gpio"}]
assert config["binary_sensor"] == [{"platform": "gpio"}]
# ---------------------------------------------------------------------------
# resolve_dependencies_with_overrides
# ---------------------------------------------------------------------------
def test_resolve_includes_dependency_added_by_dependency_override() -> None:
"""An override on a component found only as a dependency can add a
dependency of its own, and that one must be included too.
"""
graph: dict[str, set[str]] = {"tested": {"middle"}, "middle": set(), "extra": set()}
loaded: list[list[str]] = []
def loader(components: list[str]) -> None:
loaded.append(list(components))
if "middle" in components:
graph["middle"] = {"extra"}
def all_deps(names: set[str]) -> set[str]:
result = set(names)
for name in names:
result |= graph[name]
return result
with patch("build_helpers.get_all_dependencies", side_effect=all_deps):
result = build_helpers.resolve_dependencies_with_overrides(["tested"], loader)
assert result == ["extra", "middle", "tested"]
assert loaded[-1] == ["extra", "middle", "tested"]
def test_resolve_stops_when_no_new_dependencies() -> None:
loader = MagicMock()
with patch(
"build_helpers.get_all_dependencies", side_effect=lambda names: names | {"dep"}
):
result = build_helpers.resolve_dependencies_with_overrides(["tested"], loader)
assert result == ["dep", "tested"]
assert loader.call_count == 2
@@ -1522,6 +1522,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"),
@@ -1713,6 +1714,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(
@@ -1732,6 +1739,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],