mirror of
https://github.com/esphome/esphome.git
synced 2026-10-26 07:52:14 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf1cccfa67 | ||
|
|
9371116e63 | ||
|
|
3131a8bf15 | ||
|
|
055ea07380 | ||
|
|
70d3e26975 | ||
|
|
af3c41004a | ||
|
|
a590b8f735 | ||
|
|
eeccf7c68e | ||
|
|
03c44ffe4c | ||
|
|
af8f919456 | ||
|
|
a04efc2f72 | ||
|
|
e431ab8347 | ||
|
|
f0a090bc86 |
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-2
@@ -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();
|
||||
+1
-1
@@ -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,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")
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-2
@@ -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");
|
||||
+1
-1
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
+2
-2
@@ -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");
|
||||
+1
-1
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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],
|
||||
|
||||
Reference in new issue
Block a user