Compare commits

..
41 changed files with 551 additions and 685 deletions

No files matched your search

+1 -1
View File
@@ -741,7 +741,7 @@ esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic
esphome/components/weikai_i2c/* @DrCoolZic
esphome/components/weikai_spi/* @DrCoolZic
esphome/components/whirlpool/* @arpena @glmnet
esphome/components/whirlpool/* @arpena @glmnet @Rodrigoah
esphome/components/whynter/* @aeonsablaze
esphome/components/wiegand/* @ssieb
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
+2 -55
View File
@@ -5,7 +5,6 @@ import logging
import math
import re
import string
import struct
from typing import Any
import voluptuous as vol
@@ -2124,17 +2123,6 @@ def _direct_function(
)
def _call_target(
plan: list[MockObj | list[_InlineAction]],
args: TemplateArgsType,
config: ConfigType,
) -> MockObj | str:
"""What a generated callback calls: the direct function, else ``<automation>->trigger``."""
if (target := _direct_function(plan, args, config)) is None:
target = parent_ref(_automation_from_plan(plan, args, config)).trigger
return target
def _when_call(
when: str | ApplyCall | None, parent: MockObj | None
) -> ApplyCall | None:
@@ -2152,48 +2140,6 @@ async def build_automation(
return await new_automation(args, config, trigger)
async def build_component_automation(
config: ConfigType,
component_class: MockObjClass,
priority: float | None = None,
*,
id_key: str = CONF_TRIGGER_ID,
template_args: Sequence[SafeExpType] = (),
) -> MockObj:
"""Create and register ``component_class<function[, priority bits], *template_args>``,
whose function runs the actions of a no-argument automation.
An id under ``id_key`` set by hand keeps its Trigger, built with ``priority`` if given and
``template_args`` as constructor arguments.
"""
trigger_id: ID = config[id_key]
priority_list = [] if priority is None else [priority]
if trigger_id.is_manual:
var = cg.new_Pvariable(trigger_id, *priority_list, *template_args)
await cg.register_component(var, config)
await build_automation(var, [], config)
return var
# Declared and registered before the actions are built, which can wait for other ids
name = _apply_function_name(trigger_id)
define = cg.declare_static_function(name, cg.void, [])
id_ = trigger_id.copy()
id_.type = component_class
bits = [] if priority is None else [_float_bits(priority)]
var = cg.new_Pvariable(
id_, cg.TemplateArguments(cg.RawExpression(name), *bits, *template_args)
)
await cg.register_component(var, config)
plan = await _build_automation_plan([], config)
define([str(cg.statement(CallExpression(_call_target(plan, [], config))))])
return var
def _float_bits(value: float) -> HexInt:
"""``value`` as a C++ float's bit pattern, for a template argument."""
(bits,) = struct.unpack("<I", struct.pack("<f", value))
return HexInt(bits)
async def build_trigger_callback(
args: TemplateArgsType,
config: ConfigType,
@@ -2227,7 +2173,8 @@ async def _trigger_callback(
call: ApplyCall | None,
parent: MockObj | None,
) -> LambdaExpression:
target = _call_target(plan, args, config)
if (target := _direct_function(plan, args, config)) is None:
target = f"{parent_ref(_automation_from_plan(plan, args, config))}->trigger"
statements: list[str] = []
if call is not None:
members = call.members
-1
View File
@@ -31,7 +31,6 @@ from esphome.cpp_generator import ( # noqa: F401
add_global,
add_library,
add_platformio_option,
declare_static_function,
extern_progmem_array,
get_variable,
get_variable_with_full_id,
@@ -7,6 +7,8 @@
#include "esphome/core/automation.h"
#include "esphome/components/network/ip_address.h"
#include <atomic>
#ifdef USE_ETHERNET
#ifdef USE_ESP32
@@ -366,6 +368,9 @@ class EthernetComponent final : public Component {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
StaticVector<EthernetIPStateListener *, ESPHOME_ETHERNET_IP_STATE_LISTENERS> ip_state_listeners_;
#ifdef USE_ESP32
std::atomic<uint8_t> ip_state_pending_{0};
#endif
#endif
#ifdef USE_ETHERNET_CONNECT_TRIGGER
@@ -129,6 +129,14 @@ void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *me
void EthernetComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
// The IP events arrive on the event task; the listeners publish states, so notify them here
if (this->ip_state_pending_.load(std::memory_order_acquire) != 0) {
this->ip_state_pending_.store(0, std::memory_order_relaxed);
this->notify_ip_state_listeners_();
}
#endif
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
@@ -770,7 +778,8 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
global_eth_component->notify_ip_state_listeners_();
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
global_eth_component->enable_loop_soon_any_context();
#endif
#if USE_NETWORK_IPV6
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
@@ -798,6 +807,9 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
const esp_netif_ip_info_t *ip_info = &event->ip_info;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
global_eth_component->got_ipv4_address_ = true;
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
#endif
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
@@ -805,9 +817,6 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#if USE_NETWORK_IPV6
@@ -819,6 +828,9 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
// after a link flap fires another event for the same address.
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
global_eth_component->ipv6_count_ = esp_netif_get_all_ip6(global_eth_component->eth_netif_, if_ip6s);
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
#endif
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
@@ -827,9 +839,6 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#endif /* USE_NETWORK_IPV6 */
+6 -12
View File
@@ -11,10 +11,6 @@ IntervalTrigger = interval_ns.class_(
"IntervalTrigger", automation.Trigger.template(), cg.PollingComponent
)
DelayedIntervalTrigger = interval_ns.class_("DelayedIntervalTrigger", IntervalTrigger)
IntervalCallback = interval_ns.class_("IntervalCallback", cg.PollingComponent)
DelayedIntervalCallback = interval_ns.class_(
"DelayedIntervalCallback", IntervalCallback
)
CONFIG_SCHEMA = automation.validate_automation(
cv.Schema(
@@ -31,14 +27,12 @@ CONFIG_SCHEMA = automation.validate_automation(
async def to_code(config: list[ConfigType]) -> None:
for conf in config:
# Cut to 32 bits, as the uint32_t setter always did
if startup_delay := conf[CONF_STARTUP_DELAY].total_milliseconds & 0xFFFFFFFF:
# A hand-set id keeps the trigger, which takes the delay in its constructor
if startup_delay := conf[CONF_STARTUP_DELAY].total_milliseconds:
conf[CONF_ID].type = DelayedIntervalTrigger
component_class, template_args = DelayedIntervalCallback, [startup_delay]
var = cg.new_Pvariable(conf[CONF_ID], startup_delay)
else:
component_class, template_args = IntervalCallback, []
var = await automation.build_component_automation(
conf, component_class, id_key=CONF_ID, template_args=template_args
)
var = cg.new_Pvariable(conf[CONF_ID])
await cg.register_component(var, conf)
await automation.build_automation(var, [], conf)
cg.add(var.set_update_interval(conf[CONF_INTERVAL]))
-10
View File
@@ -1,10 +0,0 @@
#include "interval.h"
namespace esphome::interval {
void IntervalBase::start_after_delay_(uint32_t startup_delay) {
this->stop_poller();
this->set_timeout(startup_delay, [this] { this->start_poller(); });
}
} // namespace esphome::interval
+5 -20
View File
@@ -6,13 +6,7 @@
namespace esphome::interval {
/// Base of the interval components; holds the first update back for a startup delay.
class IntervalBase : public PollingComponent {
protected:
void start_after_delay_(uint32_t startup_delay);
};
class IntervalTrigger : public Trigger<>, public IntervalBase {
class IntervalTrigger : public Trigger<>, public PollingComponent {
public:
void update() override { this->trigger(); }
};
@@ -22,22 +16,13 @@ class DelayedIntervalTrigger final : public IntervalTrigger {
public:
explicit DelayedIntervalTrigger(uint32_t startup_delay) : startup_delay_(startup_delay) {}
void setup() override { this->start_after_delay_(this->startup_delay_); }
void setup() override {
this->stop_poller();
this->set_timeout(this->startup_delay_, [this] { this->start_poller(); });
}
protected:
const uint32_t startup_delay_;
};
/// Calls Fn from update(), for an interval without a hand-set id.
template<auto Fn> class IntervalCallback : public IntervalBase {
public:
void update() override { Fn(); }
};
/// An IntervalCallback whose first run waits STARTUP_DELAY ms; only used when the delay is not zero.
template<auto Fn, uint32_t STARTUP_DELAY> class DelayedIntervalCallback final : public IntervalCallback<Fn> {
public:
void setup() override { this->start_after_delay_(STARTUP_DELAY); }
};
} // namespace esphome::interval
+6 -2
View File
@@ -5,13 +5,15 @@ from esphome.const import CONF_MODEL
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@glmnet", "@arpena"]
CODEOWNERS = ["@glmnet", "@arpena", "@Rodrigoah"]
whirlpool_ns = cg.esphome_ns.namespace("whirlpool")
WhirlpoolClimate = whirlpool_ns.class_("WhirlpoolClimate", climate_ir.ClimateIR)
Model = whirlpool_ns.enum("Model")
MODELS = Model.options("DG11J1-3A", "DG11J1-91", "HISENSE_ARG", prefix="MODEL_")
MODELS = Model.options(
"DG11J1-3A", "DG11J1-91", "HISENSE_ARG", "HITACHI_HCRA31NEWH", prefix="MODEL_"
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(WhirlpoolClimate).extend(
{
@@ -26,3 +28,5 @@ async def to_code(config: ConfigType) -> None:
# cv.enum keeps the YAML key as a str subclass; the MockObj value cannot be compared
if config[CONF_MODEL] == "HISENSE_ARG":
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
elif config[CONF_MODEL] == "HITACHI_HCRA31NEWH":
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
+61 -15
View File
@@ -2,6 +2,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::whirlpool {
ESPHOME_LOG_TAG(TAG, "whirlpool.climate");
@@ -44,6 +46,17 @@ static constexpr uint8_t HISENSE_ARG_POWER_TOGGLE = 0x01; // byte 15
static constexpr uint8_t HISENSE_ARG_SETTINGS_ONLY = 0x02; // byte 15
static constexpr uint8_t HISENSE_ARG_FIXED_TEMP_STEP = 7; // auto and fan-only send 23 C
// Hitachi HCRA31NEWH model: power is a state in every frame
static constexpr uint8_t HITACHI_BYTE10 = 0x20; // byte 10
static constexpr uint8_t HITACHI_COOL = 0x02; // byte 15
static constexpr uint8_t HITACHI_OFF = 0x01; // byte 15
static constexpr uint8_t HITACHI_BYTE18_ON = 0x38; // byte 18
static constexpr uint8_t HITACHI_BYTE18_OFF = 0x28; // byte 18
static constexpr uint8_t HITACHI_BYTE19 = 0x08; // byte 19
// Header, 21 bytes of 8 bits, two dividers and the footer mark
static constexpr uint32_t WHIRLPOOL_FRAME_TIMINGS = 2 + WHIRLPOOL_STATE_LENGTH * 16 + 2 * 2 + 1;
WhirlpoolClimate::WhirlpoolClimate()
: climate_ir::ClimateIR(
WHIRLPOOL_DG11J1_3A_TEMP_MIN, WHIRLPOOL_DG11J1_3A_TEMP_MAX, 1.0f, true, true,
@@ -55,19 +68,19 @@ void WhirlpoolClimate::transmit_state() {
this->last_transmit_time_ = millis(); // setting the time of the last transmission.
uint8_t remote_state[WHIRLPOOL_STATE_LENGTH];
memset(remote_state, 0, sizeof(remote_state));
remote_state[0] = 0x83;
remote_state[1] = 0x06;
put_be16(remote_state, this->remote_id_());
remote_state[6] = 0x80;
remote_state[18] = 0x08; // the same on every supported model
remote_state[18] = 0x08;
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
const bool power_toggle = powered_on != this->powered_on_assumed;
const bool powered_on = this->mode != climate::CLIMATE_MODE_OFF;
// Hitachi sends power as a state instead of a toggle
const bool power_toggle = !this->is_hitachi_hcra31newh_() && powered_on != this->powered_on_assumed;
if (power_toggle) {
// Set power toggle command
remote_state[2] = 4;
remote_state[15] = 1;
this->powered_on_assumed = powered_on;
}
this->powered_on_assumed = powered_on;
switch (this->mode) {
case climate::CLIMATE_MODE_HEAT_COOL:
// set fan auto
@@ -93,6 +106,10 @@ void WhirlpoolClimate::transmit_state() {
remote_state[15] = 6;
break;
case climate::CLIMATE_MODE_OFF:
// These remotes keep the cool code in the power-off frame
if (this->is_hisense_arg_() || this->is_hitachi_hcra31newh_())
remote_state[3] = WHIRLPOOL_COOL;
break;
default:
break;
}
@@ -102,6 +119,7 @@ void WhirlpoolClimate::transmit_state() {
// Temperature
auto temp = (uint8_t) roundf(clamp(this->target_temperature, this->temperature_min_(), this->temperature_max_()));
const auto temp_step = (uint8_t) (temp - this->temperature_min_());
this->target_temperature = temp; // publish what the unit was sent
if (hisense_arg && this->fixed_temp_mode_()) {
remote_state[3] |= HISENSE_ARG_FIXED_TEMP_STEP << 4;
} else {
@@ -131,7 +149,6 @@ void WhirlpoolClimate::transmit_state() {
remote_state[2] |= WHIRLPOOL_SWING_MASK;
remote_state[8] = WHIRLPOOL_SWING_BYTE8;
}
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
} else if (this->send_swing_cmd_) {
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) {
remote_state[2] |= WHIRLPOOL_SWING_MASK;
@@ -139,6 +156,11 @@ void WhirlpoolClimate::transmit_state() {
}
}
if (hisense_arg)
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
if (this->is_hitachi_hcra31newh_())
this->encode_hitachi_hcra31newh_(remote_state);
// Checksum
remote_state[13] = xor_bytes(&remote_state[2], 11, remote_state[13]);
remote_state[20] = xor_bytes(&remote_state[14], 6, remote_state[20]);
@@ -148,6 +170,7 @@ void WhirlpoolClimate::transmit_state() {
// Send code
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->reserve(WHIRLPOOL_FRAME_TIMINGS);
data->set_carrier_frequency(38000);
@@ -180,10 +203,6 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
remote_state[15] = power_toggle ? HISENSE_ARG_POWER_TOGGLE : HISENSE_ARG_SETTINGS_ONLY;
switch (this->mode) {
case climate::CLIMATE_MODE_OFF:
// The remote keeps the cool code in the power-off frame
remote_state[3] = (remote_state[3] & 0xF0) | WHIRLPOOL_COOL;
break;
case climate::CLIMATE_MODE_DRY:
// The unit runs these fan speeds itself; the user's choice is kept for the next mode change
remote_state[2] &= ~WHIRLPOOL_FAN_MASK;
@@ -212,6 +231,27 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
#endif
}
void WhirlpoolClimate::encode_hitachi_hcra31newh_(uint8_t *remote_state) const {
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
remote_state[10] = HITACHI_BYTE10;
remote_state[18] = HITACHI_BYTE18_ON;
remote_state[19] = HITACHI_BYTE19;
// The unit takes the whole state from every frame: only the off frame carries the power bit
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
remote_state[15] = HITACHI_COOL;
break;
case climate::CLIMATE_MODE_OFF:
remote_state[2] |= WHIRLPOOL_POWER;
remote_state[15] = HITACHI_OFF;
remote_state[18] = HITACHI_BYTE18_OFF;
break;
default:
break;
}
#endif
}
bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
// Check if the esp isn't currently transmitting.
if (millis() - this->last_transmit_time_ < 500) {
@@ -234,7 +274,8 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP))
return false;
}
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid()) {
// The Hitachi remote always sends all 21 bytes; byte 18 holds its power state
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid() && !this->is_hitachi_hcra31newh_()) {
// Remote control only sent 14 bytes, nothing more to read, not even the footer
ESP_LOGV(TAG, "Remote control only sent %d bytes", i);
skip_footer = true;
@@ -261,13 +302,18 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
ESP_LOGV_HEX(TAG, remote_state, WHIRLPOOL_STATE_LENGTH, WHIRLPOOL_STATE_LENGTH, "Received: %s");
// verify header remote code
if (remote_state[0] != 0x83 || remote_state[1] != 0x06)
if (encode_uint16(remote_state[0], remote_state[1]) != this->remote_id_())
return false;
// powr on/off button
ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & WHIRLPOOL_POWER));
if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
if (this->is_hitachi_hcra31newh_()) {
// Power is a state: byte 18 says whether the unit is off
this->powered_on_assumed = remote_state[18] != HITACHI_BYTE18_OFF;
if (!this->powered_on_assumed)
this->mode = climate::CLIMATE_MODE_OFF;
} else if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
if (powered_on) {
@@ -309,7 +355,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
int temp = remote_state[3] & 0xF0;
ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
temp = (uint8_t) temp >> 4;
temp += static_cast<int>(this->temperature_min_());
temp = std::min(temp + static_cast<int>(this->temperature_min_()), static_cast<int>(this->temperature_max_()));
ESP_LOGVV(TAG, "Temperature Climate: %u", temp);
this->target_temperature = temp;
}
+19 -4
View File
@@ -6,9 +6,10 @@ namespace esphome::whirlpool {
/// Simple enum to represent models.
enum Model {
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
MODEL_HITACHI_HCRA31NEWH = 3, /// Hitachi with the HCRA31NEWH remote, 16 to 30
};
// Temperature
@@ -29,7 +30,9 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override {
send_swing_cmd_ = call.get_swing_mode().has_value();
// The toggle remotes flip swing on every press, so only send one when the mode really changes
auto swing = call.get_swing_mode();
this->send_swing_cmd_ = swing.has_value() && *swing != this->swing_mode;
climate_ir::ClimateIR::control(call);
}
@@ -55,10 +58,22 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
return false;
#endif
}
/// False when the model is compiled out, so the compiler drops the Hitachi branches.
bool is_hitachi_hcra31newh_() const {
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
return this->model_ == MODEL_HITACHI_HCRA31NEWH;
#else
return false;
#endif
}
/// The remote ID sent in bytes 0 and 1.
uint16_t remote_id_() const { return this->is_hitachi_hcra31newh_() ? 0xA959 : 0x8306; }
/// Modes in which the Hisense ARG model sends a fixed temperature.
bool fixed_temp_mode_() const;
/// Add the Hisense ARG bytes on top of the common whirlpool frame.
void encode_hisense_arg_(uint8_t *remote_state, bool power_toggle, uint8_t temp_step) const;
/// Add the Hitachi HCRA31NEWH bytes on top of the common whirlpool frame.
void encode_hitachi_hcra31newh_(uint8_t *remote_state) const;
/// Set the time of the last transmission.
uint32_t last_transmit_time_{};
+1 -1
View File
@@ -563,7 +563,7 @@ class WiFiComponent final : public Component {
// (In most use cases you won't need these)
/// Setup WiFi interface.
void setup() override;
#if defined(USE_LN882X) || defined(USE_BK72XX)
#if defined(USE_ESP32) || defined(USE_LN882X) || defined(USE_BK72XX)
void on_powerdown() override;
#endif
void start();
@@ -1635,6 +1635,21 @@ bool WiFiComponent::wifi_dpp_start_listen_() {
void WiFiComponent::wifi_dpp_stop_listen_() { esp_supp_dpp_stop_listen(); }
#endif
void WiFiComponent::on_powerdown() {
// Leave the AP before esp_restart() or deep sleep stops Wi-Fi. esp_wifi_stop() while associated
// makes the ESP32 retransmit its Disassociation up to the retry limit although the AP ACKs it
// (32 copies with protected management frames), which some APs handle badly.
// Upstream bug: https://github.com/espressif/esp-idf/issues/19443 - remove this once it is fixed
// in the ESP-IDF versions we support.
// Ask the driver: s_sta_connected lags until the main loop has handled the connect event.
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK)
return;
ESP_LOGD(TAG, "Disconnecting before powerdown");
esp_wifi_disconnect();
delay(100); // NOLINT: once per reboot or deep sleep, lets the disassociation go out
}
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
bssid_t WiFiComponent::wifi_bssid() {
+2
View File
@@ -294,6 +294,8 @@ void Application::reboot() {
for (auto &component : std::ranges::reverse_view(this->components_)) {
component->on_shutdown();
}
// No teardown on a forced reboot, but still power hardware down (Wi-Fi leaves the AP)
this->run_powerdown_hooks();
arch_restart();
}
void Application::safe_reboot() {
-35
View File
@@ -171,41 +171,6 @@ class LoopTrigger : public Trigger<>, public Component {
float get_setup_priority() const override { return setup_priority::DATA; }
};
/// Component whose setup priority is the float with bit pattern PRIORITY_BITS, since GCC 10 (ESP8266)
/// has no float template parameters. Templated on the priority only, so each priority has one copy.
template<uint32_t PRIORITY_BITS> class PriorityComponent : public Component {
public:
float get_setup_priority() const override { return bit_cast<float>(PRIORITY_BITS); }
};
/// Bit pattern of setup_priority::DATA (600.0f), the Component default.
static constexpr uint32_t SETUP_PRIORITY_DATA_BITS = 0x44160000;
// bit_cast is constexpr only where std::bit_cast exists, which GCC 10 (ESP8266) lacks
#if __cpp_lib_bit_cast >= 201806
static_assert(bit_cast<uint32_t>(setup_priority::DATA) == SETUP_PRIORITY_DATA_BITS);
#endif
/// The default priority needs no override.
template<> class PriorityComponent<SETUP_PRIORITY_DATA_BITS> : public Component {};
/// Calls Fn from setup(), for on_boot.
template<auto Fn, uint32_t PRIORITY_BITS> class StartupCallback final : public PriorityComponent<PRIORITY_BITS> {
public:
void setup() override { Fn(); }
};
/// Calls Fn from on_shutdown(), for on_shutdown.
template<auto Fn, uint32_t PRIORITY_BITS> class ShutdownCallback final : public PriorityComponent<PRIORITY_BITS> {
public:
void on_shutdown() override { Fn(); }
};
/// Calls Fn from loop(), for on_loop.
template<auto Fn> class LoopCallback final : public Component {
public:
void loop() override { Fn(); }
};
#ifdef ESPHOME_PROJECT_NAME
class ProjectUpdateTrigger : public Trigger<std::string>, public Component {
public:
+4 -2
View File
@@ -185,10 +185,12 @@ class Component {
*/
virtual bool teardown() { return true; }
/** Called after teardown is complete to power down hardware.
/** Called last before a reboot or deep sleep to power down hardware.
*
* This is called after all components have finished their teardown process,
* On a safe shutdown this is called after all components have finished their teardown process,
* making it safe to power down hardware like ethernet PHY.
* On a forced reboot (Application::reboot()) it is called right after on_shutdown() with no
* teardown, so implementations must not assume teardown() has run.
*/
virtual void on_powerdown() {}
+14 -12
View File
@@ -124,9 +124,6 @@ ShutdownTrigger = cg.esphome_ns.class_(
LoopTrigger = cg.esphome_ns.class_(
"LoopTrigger", cg.Component, automation.Trigger.template()
)
StartupCallback = cg.esphome_ns.class_("StartupCallback", cg.Component)
ShutdownCallback = cg.esphome_ns.class_("ShutdownCallback", cg.Component)
LoopCallback = cg.esphome_ns.class_("LoopCallback", cg.Component)
ProjectUpdateTrigger = cg.esphome_ns.class_(
"ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string)
)
@@ -788,15 +785,20 @@ async def _add_environment_variables(env_vars: dict[str, str]) -> None:
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _add_automations(config):
for key, component_class in (
(CONF_ON_BOOT, StartupCallback),
(CONF_ON_SHUTDOWN, ShutdownCallback),
(CONF_ON_LOOP, LoopCallback),
):
for conf in config.get(key, []):
await automation.build_component_automation(
conf, component_class, conf.get(CONF_PRIORITY)
)
for conf in config.get(CONF_ON_BOOT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
await cg.register_component(trigger, conf)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_SHUTDOWN, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
await cg.register_component(trigger, conf)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_LOOP, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
await cg.register_component(trigger, conf)
await automation.build_automation(trigger, [], conf)
# Datetime component has special subtypes that need additional defines
+1
View File
@@ -387,6 +387,7 @@
#define USE_WATER_HEATER
#define USE_WATER_HEATER_VISUAL_OVERRIDES
#define USE_WHIRLPOOL_HISENSE_ARG
#define USE_WHIRLPOOL_HITACHI_HCRA31NEWH
#define USE_ZWAVE_PROXY
// Feature flags which do not work for zephyr
+9 -22
View File
@@ -883,13 +883,14 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
return Pvariable(id_, rhs)
def declare_static_function(
def static_function(
name: str,
return_type: SafeExpType,
parameters: TemplateArgsType,
) -> Callable[[list[str | Statement]], RawExpression]:
"""Declare ``static <return_type> <name>(parameters)`` now and return the function that
emits its body, for a caller that names the function before the body is known.
body: list[str | Statement],
) -> RawExpression:
"""Emit ``static <return_type> <name>(parameters) { body }`` at global scope and return an
expression naming it, for use as a template argument or a function pointer.
The declaration goes with the other globals, so a template argument can name the function
at once; the body goes after the user includes, where every id and everything those
@@ -899,24 +900,10 @@ def declare_static_function(
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
)
add_global(RawStatement(f"{signature};"))
def define(body: list[str | Statement]) -> RawExpression:
add_global_after_includes(
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
)
return RawExpression(name)
return define
def static_function(
name: str,
return_type: SafeExpType,
parameters: TemplateArgsType,
body: list[str | Statement],
) -> RawExpression:
"""``declare_static_function`` with the body known at once."""
return declare_static_function(name, return_type, parameters)(body)
add_global_after_includes(
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
)
return RawExpression(name)
@dataclass
+23 -10
View File
@@ -8,7 +8,6 @@ import re
from typing import NamedTuple
from esphome.helpers import fnv1_hash
from tests.component_tests.helpers import constructed, function_body
CONFIG = "tests/component_tests/api/test_user_services.yaml"
LEGACY_CONFIG = "tests/component_tests/api/test_user_services_legacy.yaml"
@@ -23,10 +22,24 @@ class Service(NamedTuple):
type: str
def _constructed(main_cpp: str, id_: str) -> str:
match = re.search(rf"\bnew\({id_}\) (.*?);\n", main_cpp, re.DOTALL)
assert match is not None, id_
return match.group(1)
def _actions_of(main_cpp: str, automation: str) -> list[str]:
return re.findall(rf"\b{automation}->add_action\((\w+)\);", main_cpp)
def _function_body(main_cpp: str, name: str) -> str:
match = re.search(
rf"static void {name}\([^;{{]*\) \{{(.*?)\n\}}\n", main_cpp, re.DOTALL
)
assert match is not None, name
return match.group(1)
def _strings(main_cpp: str, name: str) -> tuple[str, str]:
"""Return the id and the entries of the strings table of action ``name``."""
match = re.search(
@@ -57,14 +70,14 @@ def _automation(main_cpp: str, trigger: str) -> tuple[str, str]:
def _action_code(main_cpp: str, action: str) -> str:
"""Return the code an action runs: its generated function, or its constructor."""
code = constructed(main_cpp, action)
if match := re.match(r"ApplyRunAction<(\w+)[,>]", code):
return function_body(main_cpp, match.group(1))
return code
constructed = _constructed(main_cpp, action)
if match := re.match(r"ApplyRunAction<(\w+)[,>]", constructed):
return _function_body(main_cpp, match.group(1))
return constructed
def _action_class(main_cpp: str, action: str) -> str:
return re.match(r"[\w:]+", constructed(main_cpp, action)).group(0)
return re.match(r"[\w:]+", _constructed(main_cpp, action)).group(0)
def test_no_args_service(generate_main) -> None:
@@ -138,12 +151,12 @@ def test_respond_optional(generate_main) -> None:
automation, args = _automation(main_cpp, service.id)
assert args == "uint32_t, bool, int32_t"
respond, unregister = _actions_of(main_cpp, automation)
assert constructed(main_cpp, respond) == (
assert _constructed(main_cpp, respond) == (
"api::APIRespondAction<uint32_t, bool, int32_t>(api_apiserver_id)"
)
assert f"{respond}->set_is_optional_mode(true);" in main_cpp
assert unregister == f"{service.id}__unregister"
assert constructed(main_cpp, unregister) == (
assert _constructed(main_cpp, unregister) == (
"api::APIUnregisterServiceCallAction<uint32_t, bool, int32_t>(api_apiserver_id)"
)
@@ -157,12 +170,12 @@ def test_respond_only(generate_main) -> None:
assert args == "uint32_t, StringRef"
log, respond, unregister = _actions_of(main_cpp, automation)
assert "RESPOND_ONLY %s" in _action_code(main_cpp, log)
assert constructed(main_cpp, respond) == (
assert _constructed(main_cpp, respond) == (
"api::APIRespondAction<uint32_t, StringRef>(api_apiserver_id)"
)
assert f"{respond}->set_is_optional_mode" not in main_cpp
assert unregister == f"{service.id}__unregister"
assert constructed(main_cpp, unregister) == (
assert _constructed(main_cpp, unregister) == (
"api::APIUnregisterServiceCallAction<uint32_t, StringRef>(api_apiserver_id)"
)
@@ -6,8 +6,6 @@ import re
import pytest
from tests.component_tests.helpers import actions_body, constructed, delayed_automation
CONFIG = "core_automations.yaml"
@@ -19,6 +17,12 @@ def main_cpp(
return generate_main(component_config_path(CONFIG))
def _constructed(main_cpp: str, var: str) -> str:
match = re.search(rf"new\({var}\) ([^;]+);", main_cpp)
assert match is not None, var
return match.group(1)
def _automation_of(main_cpp: str, trigger: str) -> str:
"""The Automation built on ``trigger``, found by its trigger rather than an id counter."""
match = re.search(rf"new\((\w+)\) Automation<>\({trigger}\);", main_cpp)
@@ -26,6 +30,14 @@ def _automation_of(main_cpp: str, trigger: str) -> str:
return match.group(1)
def _has_delay(main_cpp: str, trigger: str) -> bool:
automation = _automation_of(main_cpp, trigger)
return (
re.search(rf"{automation}->add_action\(delayaction_id\w*\);", main_cpp)
is not None
)
def _registered(main_cpp: str) -> list[str]:
return re.findall(r"App\.register_component_\((\w+), \d+\);", main_cpp)
@@ -45,65 +57,39 @@ def test_core_automations_registration_order(main_cpp: str) -> None:
def test_on_boot_inline_priorities(main_cpp: str) -> None:
"""All-inline on_boot blocks call one function from setup() at their priority, default 600.
The priority is the float's bit pattern: 0x44480000 is 800.0f, 0x44160000 is 600.0f.
"""
assert constructed(main_cpp, "startuptrigger_id") == (
"StartupCallback<esphome__startuptrigger_id__fn, 0x44480000>()"
)
assert constructed(main_cpp, "startuptrigger_id_2") == (
"StartupCallback<esphome__startuptrigger_id_2__fn, 0x44160000>()"
)
assert "Automation<>(startuptrigger_id" not in main_cpp
# Both actions of the first block run in the one function, with no stop checks
body = actions_body(main_cpp, "esphome__startuptrigger_id__fn")
assert "boot high" in body
assert "boot high lambda" in body
assert "esphome__running" not in body
assert "Automation" not in body
assert "boot default" in actions_body(main_cpp, "esphome__startuptrigger_id_2__fn")
"""All-inline on_boot blocks keep their configured priority, default 600."""
assert _constructed(main_cpp, "startuptrigger_id") == "StartupTrigger(800.0f)"
assert _constructed(main_cpp, "startuptrigger_id_2") == "StartupTrigger(600.0f)"
_automation_of(main_cpp, "startuptrigger_id")
_automation_of(main_cpp, "startuptrigger_id_2")
def test_on_boot_delay_triggers_automation(main_cpp: str) -> None:
"""A delay keeps the Automation of separate actions, which the function triggers."""
assert constructed(main_cpp, "startuptrigger_id_3") == (
"StartupCallback<esphome__startuptrigger_id_3__fn, 0xC2C80000>()"
)
delayed_automation(main_cpp, "esphome__startuptrigger_id_3__fn")
def test_on_boot_delay_keeps_trigger(main_cpp: str) -> None:
"""A delay needs the Trigger and its Automation of separate actions."""
assert _constructed(main_cpp, "startuptrigger_id_3") == "StartupTrigger(-100.0f)"
assert _has_delay(main_cpp, "startuptrigger_id_3")
def test_on_boot_manual_trigger_id_keeps_trigger(main_cpp: str) -> None:
"""A trigger_id set by hand can be used from lambdas, so the Trigger stays."""
assert constructed(main_cpp, "manual_boot") == "StartupTrigger(250.5f)"
assert _constructed(main_cpp, "manual_boot") == "StartupTrigger(250.5f)"
_automation_of(main_cpp, "manual_boot")
def test_on_shutdown_priorities(main_cpp: str) -> None:
"""on_shutdown calls its function at its priority; a delay keeps the Automation."""
assert constructed(main_cpp, "shutdowntrigger_id") == (
"ShutdownCallback<esphome__shutdowntrigger_id__fn, 0x442F0000>()"
)
assert "shutdown inline" in actions_body(
main_cpp, "esphome__shutdowntrigger_id__fn"
)
assert constructed(main_cpp, "shutdowntrigger_id_2") == (
"ShutdownCallback<esphome__shutdowntrigger_id_2__fn, 0x44160000>()"
)
delayed_automation(main_cpp, "esphome__shutdowntrigger_id_2__fn")
"""on_shutdown blocks keep their priority; a delay keeps the Trigger."""
assert _constructed(main_cpp, "shutdowntrigger_id") == "ShutdownTrigger(700.0f)"
_automation_of(main_cpp, "shutdowntrigger_id")
assert _constructed(main_cpp, "shutdowntrigger_id_2") == "ShutdownTrigger(600.0f)"
assert _has_delay(main_cpp, "shutdowntrigger_id_2")
def test_on_loop(main_cpp: str) -> None:
"""on_loop calls its function each loop; a delay keeps the Automation."""
assert constructed(main_cpp, "looptrigger_id") == (
"LoopCallback<esphome__looptrigger_id__fn>()"
)
assert "loop inline" in actions_body(main_cpp, "esphome__looptrigger_id__fn")
assert "HasLoopOverride<LoopCallback<esphome__looptrigger_id__fn>>" in main_cpp
assert constructed(main_cpp, "looptrigger_id_2") == (
"LoopCallback<esphome__looptrigger_id_2__fn>()"
)
delayed_automation(main_cpp, "esphome__looptrigger_id_2__fn")
"""on_loop blocks are looping components; a delay keeps the Trigger."""
assert _constructed(main_cpp, "looptrigger_id") == "LoopTrigger()"
_automation_of(main_cpp, "looptrigger_id")
assert _constructed(main_cpp, "looptrigger_id_2") == "LoopTrigger()"
assert _has_delay(main_cpp, "looptrigger_id_2")
def test_on_boot_registered_before_its_actions_are_built(
-35
View File
@@ -47,41 +47,6 @@ def get_define_value(name: str) -> str | None:
return None
def constructed(main_cpp: str, var: str) -> str:
"""The expression placement-new builds ``var`` with, e.g. ``Foo<bar>()``."""
match = re.search(rf"\bnew\({var}\) (.*?);\n", main_cpp, re.DOTALL)
assert match is not None, var
return match.group(1)
def function_body(main_cpp: str, name: str) -> str:
"""The body of the generated ``static void name(...)``."""
match = re.search(
rf"static void {name}\([^;{{]*\) \{{(.*?)\n\}}\n", main_cpp, re.DOTALL
)
assert match is not None, name
return match.group(1)
def actions_body(main_cpp: str, function: str) -> str:
"""The body of the function of inline actions that ``function`` calls."""
match = re.fullmatch(r"\n (\w+)\(\);", function_body(main_cpp, function))
assert match is not None, function
return function_body(main_cpp, match.group(1))
def delayed_automation(main_cpp: str, function: str) -> str:
"""The Automation that ``function`` triggers, checked to hold a delay."""
match = re.fullmatch(
r"\n ::(\w+)->trigger\(\);", function_body(main_cpp, function)
)
assert match is not None, function
automation = match.group(1)
assert f"new({automation}) Automation<>();" in main_cpp
assert re.search(rf"{automation}->add_action\(delayaction_id\w*\);", main_cpp)
return automation
# ESPBTUUID constant for ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD (bytes in BLE wire order).
BLE_UUID128_FACTORY = (
"from_raw(std::array<uint8_t, 16>{" + ", ".join(["0xCD", "0xAB"] * 8) + "}.data())"
@@ -1,42 +0,0 @@
esphome:
name: test
esp32:
board: nodemcu-32s
logger:
interval:
- interval: 1s
then:
- logger.log: inline tick
- lambda: ESP_LOGD("test", "inline lambda");
- interval: 2s
startup_delay: 5s
then:
- logger.log: delayed start tick
- interval: 3s
then:
- logger.log: before delay
- delay: 10ms
- logger.log: after delay
- id: named_interval
interval: 4s
startup_delay: 3s
then:
- logger.log: named tick
- interval: 5s
setup_priority: 250
then:
# The script is declared later, so building this action waits for it
- script.execute: later_script
script:
- id: later_script
then:
- logger.log: script ran
sensor:
- platform: template
id: some_sensor
lambda: return 1.0;
@@ -1,78 +0,0 @@
"""Tests for the code generated for interval blocks."""
from collections.abc import Callable
from pathlib import Path
import re
import pytest
from tests.component_tests.helpers import constructed, delayed_automation
CONFIG = "interval_codegen.yaml"
@pytest.fixture
def main_cpp(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> str:
return generate_main(component_config_path(CONFIG))
def _callback(var: str, startup_delay: int = 0) -> str:
if startup_delay:
return (
f"interval::DelayedIntervalCallback<esphome__{var}__fn, {startup_delay}>()"
)
return f"interval::IntervalCallback<esphome__{var}__fn>()"
@pytest.mark.parametrize(
("var", "expected", "update_interval"),
[
("interval_intervaltrigger_id", _callback("interval_intervaltrigger_id"), 1000),
(
"interval_intervaltrigger_id_2",
_callback("interval_intervaltrigger_id_2", 5000),
2000,
),
(
"interval_intervaltrigger_id_3",
_callback("interval_intervaltrigger_id_3"),
3000,
),
("named_interval", "interval::DelayedIntervalTrigger(3000)", 4000),
(
"interval_intervaltrigger_id_4",
_callback("interval_intervaltrigger_id_4"),
5000,
),
],
)
def test_interval_constructed(
main_cpp: str, var: str, expected: str, update_interval: int
) -> None:
"""Each interval is built with its startup delay, if any, and gets its update interval."""
assert constructed(main_cpp, var) == expected
assert f"{var}->set_update_interval({update_interval});" in main_cpp
def test_interval_delay_triggers_automation(main_cpp: str) -> None:
"""A delay keeps the Automation of separate actions, which the function triggers."""
delayed_automation(main_cpp, "esphome__interval_intervaltrigger_id_3__fn")
def test_interval_registration_order(main_cpp: str) -> None:
"""Intervals register in config order; one waiting for a later id keeps its place."""
registered = re.findall(r"App\.register_component_\((\w+), \d+\);", main_cpp)
assert registered[-6:] == [
"interval_intervaltrigger_id",
"interval_intervaltrigger_id_2",
"interval_intervaltrigger_id_3",
"named_interval",
"interval_intervaltrigger_id_4",
"some_sensor",
]
assert main_cpp.index(
"App.register_component_(interval_intervaltrigger_id_4"
) < main_cpp.index("new(later_script)")
@@ -6,11 +6,23 @@ ids, so adding or changing one script does not break the others.
import re
from tests.component_tests.helpers import constructed, function_body
CONFIG = "tests/component_tests/script/test_script_codegen.yaml"
def _constructed(main_cpp: str, id_: str) -> str:
match = re.search(rf"\bnew\({id_}\) (.*?);\n", main_cpp, re.DOTALL)
assert match is not None, id_
return match.group(1)
def _function_body(main_cpp: str, name: str) -> str:
match = re.search(
rf"static void {name}\([^;{{]*\) \{{(.*?)\n\}}\n", main_cpp, re.DOTALL
)
assert match is not None, name
return match.group(1)
def _automation(main_cpp: str, trigger: str) -> str:
match = re.search(rf"\bnew\((\w+)\) Automation<.*?>\({trigger}\);", main_cpp)
assert match is not None, trigger
@@ -28,17 +40,17 @@ def _single_action(main_cpp: str, script: str) -> str:
def _run_function(main_cpp: str, script: str) -> str:
"""Return the function of the one merged action the script's Automation runs."""
code = constructed(main_cpp, _single_action(main_cpp, script))
match = re.match(r"ApplyRunAction<(\w+)[,>]", code)
assert match is not None, code
constructed = _constructed(main_cpp, _single_action(main_cpp, script))
match = re.match(r"ApplyRunAction<(\w+)[,>]", constructed)
assert match is not None, constructed
return match.group(1)
def test_inline_actions_merge_into_one_action(generate_main) -> None:
"""A script of inline actions runs them as one merged action of an Automation."""
main_cpp = generate_main(CONFIG)
assert constructed(main_cpp, "inline_single") == "script::SingleScript<>()"
body = function_body(main_cpp, _run_function(main_cpp, "inline_single"))
assert _constructed(main_cpp, "inline_single") == "script::SingleScript<>()"
body = _function_body(main_cpp, _run_function(main_cpp, "inline_single"))
assert '"INLINE first"' in body
assert "counter->value() += 1;" in body
# A script can be stopped from inside, so the rest of the run checks for it
@@ -48,40 +60,40 @@ def test_inline_actions_merge_into_one_action(generate_main) -> None:
def test_one_lambda_action(generate_main) -> None:
"""A single inline action keeps its own action object."""
main_cpp = generate_main(CONFIG)
code = constructed(main_cpp, _single_action(main_cpp, "one_action"))
assert code.startswith("StatelessLambdaAction<>(")
assert '"ONE action"' in code
constructed = _constructed(main_cpp, _single_action(main_cpp, "one_action"))
assert constructed.startswith("StatelessLambdaAction<>(")
assert '"ONE action"' in constructed
def test_modes(generate_main) -> None:
"""Each mode has its own class; max_runs is set on queued and parallel scripts."""
main_cpp = generate_main(CONFIG)
assert constructed(main_cpp, "inline_restart") == "script::RestartScript<>()"
assert constructed(main_cpp, "inline_queued") == "script::QueueingScript<>()"
assert constructed(main_cpp, "inline_parallel") == "script::ParallelScript<>()"
assert _constructed(main_cpp, "inline_restart") == "script::RestartScript<>()"
assert _constructed(main_cpp, "inline_queued") == "script::QueueingScript<>()"
assert _constructed(main_cpp, "inline_parallel") == "script::ParallelScript<>()"
assert "inline_queued->set_max_runs(3);" in main_cpp
assert "inline_parallel->set_max_runs(2);" in main_cpp
assert re.search(r"App\.register_component_\(inline_queued, \d+\);", main_cpp)
for script in ("inline_queued", "inline_parallel"):
code = constructed(main_cpp, _single_action(main_cpp, script))
assert code.startswith("StatelessLambdaAction<>(")
constructed = _constructed(main_cpp, _single_action(main_cpp, script))
assert constructed.startswith("StatelessLambdaAction<>(")
def test_restart_executes_itself(generate_main) -> None:
"""A script can execute itself; on_stop has its own Automation."""
main_cpp = generate_main(CONFIG)
body = function_body(main_cpp, _run_function(main_cpp, "inline_restart"))
body = _function_body(main_cpp, _run_function(main_cpp, "inline_restart"))
assert "::inline_restart->execute();" in body
stop = _automation(main_cpp, r"inline_restart->get_stop_trigger\(\)")
(action,) = _actions_of(main_cpp, stop)
assert '"RESTART stopped"' in constructed(main_cpp, action)
assert '"RESTART stopped"' in _constructed(main_cpp, action)
def test_parameters_by_value(generate_main) -> None:
"""Lambdas take the parameters by value, so ``x *= 2`` compiles in every form."""
main_cpp = generate_main(CONFIG)
assert (
constructed(main_cpp, "with_params")
_constructed(main_cpp, "with_params")
== "script::SingleScript<int, std::string>()"
)
name = _run_function(main_cpp, "with_params")
@@ -91,7 +103,7 @@ def test_parameters_by_value(generate_main) -> None:
r"const std::remove_cvref_t<std::string> & s\)",
main_cpp,
)
body = function_body(main_cpp, name)
body = _function_body(main_cpp, name)
assert body.count("[](int x, std::string s) -> void {") == 2
assert body.count("[](int x, std::string s) -> int {") == 2
assert "x *= 2;\n ESP_LOGI(" in body
@@ -104,7 +116,7 @@ def test_parameters_by_value(generate_main) -> None:
def test_script_stop_inside(generate_main) -> None:
"""``script.stop`` without an id stops the script it is in; the next action checks."""
main_cpp = generate_main(CONFIG)
body = function_body(main_cpp, _run_function(main_cpp, "stops_itself"))
body = _function_body(main_cpp, _run_function(main_cpp, "stops_itself"))
stop, rest = body.split("::stops_itself->stop();")
assert '"STOPS start"' in stop
assert rest.lstrip("\n }").startswith("if (!esphome__running)\n return;")
@@ -114,8 +126,8 @@ def test_script_stop_inside(generate_main) -> None:
def test_calls_itself_with_parameter(generate_main) -> None:
"""A parallel script with a parameter can execute itself from an ``if``."""
main_cpp = generate_main(CONFIG)
assert constructed(main_cpp, "calls_itself") == "script::ParallelScript<int>()"
body = function_body(main_cpp, _run_function(main_cpp, "calls_itself"))
assert _constructed(main_cpp, "calls_itself") == "script::ParallelScript<int>()"
body = _function_body(main_cpp, _run_function(main_cpp, "calls_itself"))
assert "return n > 0;" in body
assert "::calls_itself->execute(std::move(esphome__arg_0));" in body
@@ -123,11 +135,11 @@ def test_calls_itself_with_parameter(generate_main) -> None:
def test_delay_keeps_separate_actions(generate_main) -> None:
"""A delay keeps the Automation and its separate actions."""
main_cpp = generate_main(CONFIG)
assert constructed(main_cpp, "with_delay") == "script::SingleScript<>()"
assert _constructed(main_cpp, "with_delay") == "script::SingleScript<>()"
before, delay, after = _actions_of(main_cpp, _automation(main_cpp, "with_delay"))
assert constructed(main_cpp, delay) == "DelayAction<>()"
assert '"DELAY before"' in constructed(main_cpp, before)
assert '"DELAY after"' in constructed(main_cpp, after)
assert _constructed(main_cpp, delay) == "DelayAction<>()"
assert '"DELAY before"' in _constructed(main_cpp, before)
assert '"DELAY after"' in _constructed(main_cpp, after)
def test_manual_automation_id(generate_main) -> None:
@@ -135,7 +147,7 @@ def test_manual_automation_id(generate_main) -> None:
main_cpp = generate_main(CONFIG)
assert _automation(main_cpp, "manual_automation") == "manual_automation_id"
(action,) = _actions_of(main_cpp, "manual_automation_id")
assert '"MANUAL"' in constructed(main_cpp, action)
assert '"MANUAL"' in _constructed(main_cpp, action)
def test_script_wait(generate_main) -> None:
@@ -143,6 +155,6 @@ def test_script_wait(generate_main) -> None:
main_cpp = generate_main(CONFIG)
(action,) = _actions_of(main_cpp, _automation(main_cpp, "waits"))
assert (
constructed(main_cpp, action)
_constructed(main_cpp, action)
== "script::ScriptWaitAction<script::SingleScript<>>(inline_single)"
)
@@ -1,64 +0,0 @@
#include <gtest/gtest.h>
#include "esphome/core/base_automation.h"
namespace esphome::core::testing {
namespace {
int calls = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void count_call() { calls++; }
// Bit patterns of 800.0f, 600.0f, -100.0f and 250.5f, as the code generator writes them
constexpr uint32_t PRIORITY_800 = 0x44480000;
constexpr uint32_t PRIORITY_MINUS_100 = 0xC2C80000;
constexpr uint32_t PRIORITY_250_5 = 0x437A8000;
} // namespace
TEST(CoreAutomationCallbacks, StartupCallbackRunsFromSetupAtItsPriority) {
calls = 0;
StartupCallback<count_call, PRIORITY_800> high;
StartupCallback<count_call, PRIORITY_MINUS_100> late;
StartupCallback<count_call, PRIORITY_250_5> fractional;
EXPECT_EQ(high.get_setup_priority(), 800.0f);
EXPECT_EQ(late.get_setup_priority(), -100.0f);
EXPECT_EQ(fractional.get_setup_priority(), 250.5f);
StartupCallback<count_call, SETUP_PRIORITY_DATA_BITS> standard;
EXPECT_EQ(standard.get_setup_priority(), setup_priority::DATA);
high.loop();
high.on_shutdown();
EXPECT_EQ(calls, 0);
high.setup();
EXPECT_EQ(calls, 1);
}
TEST(CoreAutomationCallbacks, ShutdownCallbackRunsFromOnShutdownAtItsPriority) {
calls = 0;
ShutdownCallback<count_call, PRIORITY_MINUS_100> shutdown;
EXPECT_EQ(shutdown.get_setup_priority(), -100.0f);
shutdown.setup();
shutdown.loop();
EXPECT_EQ(calls, 0);
shutdown.on_shutdown();
EXPECT_EQ(calls, 1);
}
TEST(CoreAutomationCallbacks, LoopCallbackRunsEachLoopAtDataPriority) {
calls = 0;
LoopCallback<count_call> looping;
EXPECT_EQ(looping.get_setup_priority(), setup_priority::DATA);
EXPECT_TRUE(HasLoopOverride<LoopCallback<count_call>>::value);
EXPECT_FALSE((HasLoopOverride<StartupCallback<count_call, PRIORITY_800>>::value));
looping.setup();
EXPECT_EQ(calls, 0);
looping.loop();
looping.loop();
EXPECT_EQ(calls, 2);
}
} // namespace esphome::core::testing
@@ -2,3 +2,7 @@ packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
# IPv6 with an IP sensor: compiles the IPv6 event handler's listener notification
network:
enable_ipv6: true
@@ -1,41 +0,0 @@
#include <gtest/gtest.h>
#include "esphome/components/interval/interval.h"
#include "esphome/core/application.h"
namespace esphome::interval::testing {
namespace {
int calls = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void count_call() { calls++; }
} // namespace
TEST(IntervalCallback, UpdateCallsFnAtDataPriority) {
calls = 0;
IntervalCallback<count_call> interval;
EXPECT_EQ(interval.get_setup_priority(), setup_priority::DATA);
EXPECT_FALSE(HasLoopOverride<IntervalCallback<count_call>>::value);
interval.set_update_interval(1000);
EXPECT_EQ(interval.get_update_interval(), 1000u);
interval.setup();
EXPECT_EQ(calls, 0);
interval.update();
interval.update();
EXPECT_EQ(calls, 2);
}
TEST(IntervalCallback, DelayedIntervalCallbackUpdateCallsFn) {
// Only the delayed class overrides setup(), so a zero delay keeps Component::setup()
EXPECT_FALSE((HasLoopOverride<DelayedIntervalCallback<count_call, 5000>>::value));
calls = 0;
DelayedIntervalCallback<count_call, 5000> delayed;
EXPECT_EQ(delayed.get_setup_priority(), setup_priority::DATA);
delayed.update();
EXPECT_EQ(calls, 1);
}
} // namespace esphome::interval::testing
@@ -1,34 +0,0 @@
#include <gtest/gtest.h>
#include "esphome/components/interval/interval.h"
#include "esphome/core/application.h"
namespace esphome::interval::testing {
class CountAction : public Action<> {
public:
explicit CountAction(int *count) : count_(count) {}
void play() override { (*this->count_)++; }
protected:
int *count_;
};
TEST(IntervalTrigger, UpdateRunsTheAutomationAtDataPriority) {
IntervalTrigger trigger;
Automation<> automation{&trigger};
int count = 0;
CountAction action{&count};
automation.add_action(&action);
EXPECT_EQ(trigger.get_setup_priority(), setup_priority::DATA);
EXPECT_FALSE(HasLoopOverride<IntervalTrigger>::value);
trigger.set_update_interval(1000);
EXPECT_EQ(trigger.get_update_interval(), 1000u);
trigger.update();
trigger.update();
EXPECT_EQ(count, 2);
}
} // namespace esphome::interval::testing
+2 -1
View File
@@ -10,7 +10,8 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.auto_load = ["climate_ir", "climate"]
async def to_code_testing(config: ConfigType) -> None:
# Compiles in the Hisense ARG model, which climate.py adds only when it is configured
# Compiles in the Hisense ARG and Hitachi models, which climate.py adds only when configured
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
manifest.to_code = to_code_testing
+5
View File
@@ -8,3 +8,8 @@ climate:
model: HISENSE_ARG
transmitter_id: xmitr
receiver_id: rcvr
- platform: whirlpool
name: Whirlpool Hitachi HCRA31NEWH Climate
model: HITACHI_HCRA31NEWH
transmitter_id: xmitr
receiver_id: rcvr
@@ -252,6 +252,9 @@ TEST(WhirlpoolHisenseArg, SwingIsSentOnEveryTransmit) {
TEST(WhirlpoolDG11J13A, SwingIsSentOnlyWhenItChanges) {
Fixture f(MODEL_DG11J1_3A);
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_OFF);
EXPECT_EQ(f.last_frame()[2] & 0x80, 0) << "swing is already off";
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_VERTICAL);
Frame frame = f.last_frame();
EXPECT_EQ(frame[2] & 0x80, 0x80);
@@ -261,6 +264,9 @@ TEST(WhirlpoolDG11J13A, SwingIsSentOnlyWhenItChanges) {
frame = f.last_frame();
EXPECT_EQ(frame[2] & 0x80, 0);
EXPECT_EQ(frame[8], 0x00);
f.command(climate::CLIMATE_MODE_COOL, 25, climate::CLIMATE_FAN_LOW, climate::CLIMATE_SWING_VERTICAL);
EXPECT_EQ(f.last_frame()[2] & 0x80, 0) << "swing is already vertical";
}
// Cool 24 C (step 8), fan low, powered on assumed
@@ -385,4 +391,90 @@ TEST(WhirlpoolHisenseArg, EveryTransmitHasValidChecksums) {
}
}
// Cool 24 C (step 8), fan low, unit on
static Frame hitachi_cool_frame() {
return with_checksums({0xA9, 0x59, 0x03, 0x82, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x20,
0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x38, 0x08, 0x00});
}
// The off frame: power bit, cool code, byte 15 0x01 and byte 18 0x28
static Frame hitachi_off_frame() {
return with_checksums({0xA9, 0x59, 0x07, 0x82, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x20,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x28, 0x08, 0x00});
}
TEST(WhirlpoolHitachiHCRA31NEWH, CoolAndOffMatchKnownFrames) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
f.command(climate::CLIMATE_MODE_COOL, 24, climate::CLIMATE_FAN_LOW);
// Turning on sends the full state with no power bit
expect_frame(f.last_frame(), hitachi_cool_frame());
f.command(climate::CLIMATE_MODE_OFF, 24, climate::CLIMATE_FAN_LOW);
expect_frame(f.last_frame(), hitachi_off_frame());
// A second off command still sends the power bit
f.command(climate::CLIMATE_MODE_OFF, 24, climate::CLIMATE_FAN_LOW);
EXPECT_EQ(f.last_frame()[2] & 0x04, 0x04);
f.command(climate::CLIMATE_MODE_HEAT, 24, climate::CLIMATE_FAN_LOW);
const Frame frame = f.last_frame();
EXPECT_EQ(frame[2] & 0x04, 0);
EXPECT_EQ(frame[3], 0x80);
EXPECT_EQ(frame[15], 0x06);
EXPECT_EQ(frame[18], 0x38);
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceivePowerIsAState) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
ASSERT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
// An on frame restores the mode even though it has no power bit
EXPECT_TRUE(receive(f.climate, hitachi_cool_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_COOL);
EXPECT_FLOAT_EQ(f.climate.target_temperature, 24);
EXPECT_EQ(f.climate.fan_mode, climate::CLIMATE_FAN_LOW);
// The same frame again keeps it on
EXPECT_TRUE(receive(f.climate, hitachi_cool_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_COOL);
EXPECT_TRUE(receive(f.climate, hitachi_off_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
// A repeated off frame does not toggle
EXPECT_TRUE(receive(f.climate, hitachi_off_frame()));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceiveChecksRemoteId) {
Fixture hitachi(MODEL_HITACHI_HCRA31NEWH);
EXPECT_FALSE(receive(hitachi.climate, hisense_cool_frame()));
Fixture whirlpool(MODEL_DG11J1_91);
EXPECT_FALSE(receive(whirlpool.climate, hitachi_cool_frame()));
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceivedTemperatureIsClamped) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
Frame frame = hitachi_cool_frame();
frame[3] = 0xF2; // step 15 would be 31 C
EXPECT_TRUE(receive(f.climate, with_checksums(frame)));
EXPECT_FLOAT_EQ(f.climate.target_temperature, 30);
}
TEST(WhirlpoolDG11J13A, TargetIsClampedAndRoundedToWhatWasSent) {
Fixture f(MODEL_DG11J1_3A);
f.command(climate::CLIMATE_MODE_COOL, 35, climate::CLIMATE_FAN_LOW);
EXPECT_FLOAT_EQ(f.climate.target_temperature, 32);
f.command(climate::CLIMATE_MODE_COOL, 24.4f, climate::CLIMATE_FAN_LOW);
EXPECT_FLOAT_EQ(f.climate.target_temperature, 24);
}
TEST(WhirlpoolHitachiHCRA31NEWH, ReceiveRejectsShortFrame) {
Fixture f(MODEL_HITACHI_HCRA31NEWH);
remote_base::RawTimings raw = waveform(hitachi_cool_frame());
// Cut after the second divider: 14 bytes, no footer
raw.resize(2 + 14 * 16 + 2 * 2);
remote_base::RemoteReceiveData data(raw, 25, remote_base::TOLERANCE_MODE_PERCENTAGE);
EXPECT_FALSE(static_cast<remote_base::RemoteReceiverListener &>(f.climate).on_receive(data));
EXPECT_EQ(f.climate.mode, climate::CLIMATE_MODE_OFF);
}
} // namespace esphome::whirlpool::testing
@@ -0,0 +1,23 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/tests"]
MULTI_CONF = True
reboot_hook_recorder_ns = cg.esphome_ns.namespace("reboot_hook_recorder")
RebootHookRecorder = reboot_hook_recorder_ns.class_("RebootHookRecorder", cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(RebootHookRecorder),
cv.Required(CONF_NAME): cv.string,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_name(config[CONF_NAME]))
@@ -0,0 +1,10 @@
#include "reboot_hook_recorder.h"
#include "esphome/core/log.h"
namespace esphome::reboot_hook_recorder {
ESPHOME_LOG_TAG(TAG, "reboot_hook_recorder");
void RebootHookRecorder::record_(const char *hook) { ESP_LOGI(TAG, "HOOK %s %s", this->name_, hook); }
} // namespace esphome::reboot_hook_recorder
@@ -0,0 +1,33 @@
#pragma once
#include "esphome/core/component.h"
namespace esphome::reboot_hook_recorder {
/// Logs every shutdown lifecycle hook it receives, so a test can check their order.
class RebootHookRecorder : public Component {
public:
void set_name(const char *name) { this->name_ = name; }
void on_safe_shutdown() override { this->record_("on_safe_shutdown"); }
void on_shutdown() override { this->record_("on_shutdown"); }
// Needs a second call to finish, like a component waiting for a connection to close
bool teardown() override {
if (++this->teardown_calls_ < 2) {
this->record_("teardown_pending");
return false;
}
if (this->teardown_calls_ == 2)
this->record_("teardown_done");
return true;
}
void on_powerdown() override { this->record_("on_powerdown"); }
protected:
void record_(const char *hook);
const char *name_{""};
uint8_t teardown_calls_{0};
};
} // namespace esphome::reboot_hook_recorder
@@ -1,46 +0,0 @@
esphome:
name: interval-runs
host:
api:
logger:
level: DEBUG
globals:
- id: inline_count
type: int
initial_value: "0"
- id: action_count
type: int
initial_value: "0"
- id: startup_count
type: int
initial_value: "0"
interval:
# All actions inline
- interval: 50ms
then:
- lambda: id(inline_count) += 1;
- lambda: |-
if (id(inline_count) == 1) {
ESP_LOGI("test", "INLINE first uptime=%u", (unsigned) millis());
} else if (id(inline_count) == 20) {
ESP_LOGI("test", "INLINE ran 20 times");
}
# A delay between the actions
- interval: 100ms
then:
- lambda: |-
id(action_count) += 1;
ESP_LOGI("test", "ACTION start %d", id(action_count));
- delay: 20ms
- lambda: ESP_LOGI("test", "ACTION end %d", id(action_count));
# First run waits for the startup delay
- interval: 50ms
startup_delay: 1000ms
then:
- lambda: |-
if (++id(startup_count) == 1) {
ESP_LOGI("test", "STARTUP first uptime=%u inline_count=%d", (unsigned) millis(), id(inline_count));
}
@@ -0,0 +1,19 @@
esphome:
name: reboot-hooks-forced
host:
logger:
level: DEBUG
api:
# Reboot once the test has connected, so that it sees every hook
on_client_connected:
- delay: 100ms
- lambda: App.reboot();
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
reboot_hook_recorder:
- name: first
- name: second
@@ -0,0 +1,19 @@
esphome:
name: reboot-hooks-safe
host:
logger:
level: DEBUG
api:
# Reboot once the test has connected, so that it sees every hook
on_client_connected:
- delay: 100ms
- lambda: App.safe_reboot();
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
reboot_hook_recorder:
- name: first
- name: second
-44
View File
@@ -1,44 +0,0 @@
"""Integration test that intervals run repeatedly and honour their startup delay."""
from __future__ import annotations
import re
import pytest
from .log_utils import LineWaiter
from .types import RunCompiledFunction
INLINE_FIRST = re.compile(r"INLINE first uptime=(\d+)")
STARTUP_FIRST = re.compile(r"STARTUP first uptime=(\d+) inline_count=(\d+)")
ACTION = re.compile(r"ACTION (start|end) (\d+)")
@pytest.mark.asyncio
async def test_interval_runs(
yaml_config: str,
run_compiled: RunCompiledFunction,
) -> None:
"""Inline and delayed actions repeat; the startup delay holds the first run back."""
waiter = LineWaiter()
inline_future = waiter.expect(INLINE_FIRST)
startup_future = waiter.expect(STARTUP_FIRST)
async with run_compiled(yaml_config, line_callback=waiter.callback):
inline = await waiter.wait(inline_future)
startup = await waiter.wait(startup_future)
await waiter.wait_for_each("INLINE ran 20 times", "ACTION end 3")
# The startup delay held the first run back while the inline interval kept running
assert int(startup.group(1)) - int(inline.group(1)) >= 800
assert int(startup.group(2)) >= 10
# Each run of the delayed actions finished before the next started
actions = [m.groups() for line in waiter.lines if (m := ACTION.search(line))]
assert actions[:6] == [
("start", "1"),
("end", "1"),
("start", "2"),
("end", "2"),
("start", "3"),
("end", "3"),
]
@@ -0,0 +1,80 @@
"""Test the order of the shutdown lifecycle hooks on a forced and on a safe reboot."""
import re
import pytest
from .log_utils import LineWaiter
from .types import APIClientConnectedFactory, RunCompiledFunction
from .wait_utils import wait_or_fail
HOOK_PATTERN = re.compile(r"HOOK (\w+) (\w+)")
# Hooks run in reverse component order, so "first" gets each hook last.
LAST_HOOK = "HOOK first on_powerdown"
async def _record_reboot_hooks(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> list[tuple[str, str]]:
"""Connect, which makes the device reboot, and return its (component, hook) calls in order."""
waiter = LineWaiter()
hooks: list[tuple[str, str]] = []
def on_line(line: str) -> None:
if match := HOOK_PATTERN.search(line):
hooks.append((match[1], match[2]))
waiter.callback(line)
async with (
run_compiled(yaml_config, line_callback=on_line),
api_client_connected(),
):
await wait_or_fail(
waiter.wait_for(LAST_HOOK),
5.0,
"Device did not run the powerdown hooks",
hooks=hooks,
)
return hooks
@pytest.mark.asyncio
async def test_reboot_hook_order_forced(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""A forced reboot runs on_shutdown then on_powerdown, with no safe shutdown or teardown."""
hooks = await _record_reboot_hooks(yaml_config, run_compiled, api_client_connected)
assert hooks == [
("second", "on_shutdown"),
("first", "on_shutdown"),
("second", "on_powerdown"),
("first", "on_powerdown"),
]
@pytest.mark.asyncio
async def test_reboot_hook_order_safe(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""A safe reboot powers down only after every component has finished its teardown."""
hooks = await _record_reboot_hooks(yaml_config, run_compiled, api_client_connected)
assert hooks == [
("second", "on_safe_shutdown"),
("first", "on_safe_shutdown"),
("second", "on_shutdown"),
("first", "on_shutdown"),
("second", "teardown_pending"),
("first", "teardown_pending"),
("second", "teardown_done"),
("first", "teardown_done"),
("second", "on_powerdown"),
("first", "on_powerdown"),
]
-15
View File
@@ -1173,21 +1173,6 @@ def test_static_function_declares_early_and_defines_after_includes() -> None:
]
def test_declare_static_function_defines_the_body_later() -> None:
"""The declaration is emitted at once; the body only when define() is called."""
define = cg.declare_static_function("esphome__act__fn", ct.void, [])
assert [str(st) for st in CORE.global_statements] == [
"static void esphome__act__fn();"
]
assert CORE.global_statements_after_includes == []
fn = define(["first();", "second();"])
assert str(fn) == "esphome__act__fn"
assert [str(st) for st in CORE.global_statements_after_includes] == [
"static void esphome__act__fn() {\n first();\n second();\n}"
]
def test_static_function_accepts_statements() -> None:
call = cg.MockObj("foo")(cg.MockObj("x") >> 2)
cg.static_function(