mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d7a8ef338 | ||
|
|
904f0df920 | ||
|
|
7128d22f07 | ||
|
|
825e2c3dd0 | ||
|
|
170a3b7374 | ||
|
|
0eee107373 | ||
|
|
2347e0dcef |
No files matched your search
+1
-1
@@ -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
|
||||
|
||||
@@ -95,4 +95,19 @@ void ClimateIR::dump_config() {
|
||||
YESNO(this->modes_.count(climate::CLIMATE_MODE_HEAT_COOL)));
|
||||
}
|
||||
|
||||
int find_ir_code_index(const IRCode *table, size_t count, uint8_t key, bool match_code) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (progmem_read_byte(match_code ? &table[i].code : &table[i].value) == key)
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t lookup_ir_code(const IRCode *table, size_t count, uint8_t key, bool match_code, uint8_t fallback) {
|
||||
const int index = find_ir_code_index(table, count, key, match_code);
|
||||
if (index < 0)
|
||||
return fallback;
|
||||
return progmem_read_byte(match_code ? &table[index].value : &table[index].code);
|
||||
}
|
||||
|
||||
} // namespace esphome::climate_ir
|
||||
@@ -6,9 +6,42 @@
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/components/remote_transmitter/remote_transmitter.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
namespace esphome::climate_ir {
|
||||
|
||||
/// One entry of a PROGMEM table that maps a climate enum value to the unit's code.
|
||||
struct IRCode {
|
||||
uint8_t value; // a climate enum; they are all uint8_t
|
||||
uint8_t code;
|
||||
};
|
||||
|
||||
/// Index of the entry whose value (or, with match_code, code) is key, or -1. Reads the table from PROGMEM.
|
||||
int find_ir_code_index(const IRCode *table, size_t count, uint8_t key, bool match_code);
|
||||
|
||||
/// The entry whose value (or, with match_code, code) is key, read from PROGMEM.
|
||||
template<size_t N>
|
||||
ESPHOME_ALWAYS_INLINE optional<IRCode> find_ir_code(const IRCode (&table)[N], uint8_t key, bool match_code) {
|
||||
const int index = find_ir_code_index(table, N, key, match_code);
|
||||
if (index < 0)
|
||||
return {};
|
||||
return IRCode{progmem_read_byte(&table[index].value), progmem_read_byte(&table[index].code)};
|
||||
}
|
||||
|
||||
/// The code for value (or, with match_code, the value for code), or fallback when the table has no such entry.
|
||||
uint8_t lookup_ir_code(const IRCode *table, size_t count, uint8_t key, bool match_code, uint8_t fallback);
|
||||
|
||||
/// The code for value, or fallback when the table has no such value.
|
||||
template<size_t N> uint8_t to_ir_code(const IRCode (&table)[N], uint8_t value, uint8_t fallback) {
|
||||
return lookup_ir_code(table, N, value, false, fallback);
|
||||
}
|
||||
/// The value for code, or fallback when the table has no such code; pass the current state to keep it.
|
||||
template<typename T, size_t N> T from_ir_code(const IRCode (&table)[N], uint8_t code, T fallback) {
|
||||
static_assert(sizeof(T) == sizeof(uint8_t), "IRCode values are one byte");
|
||||
return static_cast<T>(lookup_ir_code(table, N, code, true, static_cast<uint8_t>(fallback)));
|
||||
}
|
||||
|
||||
/* A base for climate which works by sending (and receiving) IR codes
|
||||
|
||||
To send IR codes implement
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -50,34 +50,25 @@ static constexpr uint8_t STATE_TEMPLATE[STATE_MESSAGE_LENGTH] PROGMEM = {
|
||||
static constexpr uint8_t OFF_FRAME[STATE_MESSAGE_LENGTH_UTIL] PROGMEM = {0x00, 0x08, 0x08, BYTE3_POWER_OFF,
|
||||
255 - BYTE3_POWER_OFF};
|
||||
|
||||
struct Code {
|
||||
uint8_t value; // a climate enum, all of which are uint8_t
|
||||
uint8_t code;
|
||||
};
|
||||
static constexpr Code MODES[] PROGMEM = {
|
||||
using climate_ir::find_ir_code;
|
||||
using climate_ir::from_ir_code;
|
||||
using climate_ir::IRCode;
|
||||
using climate_ir::to_ir_code;
|
||||
|
||||
// A received mode that is not listed is ignored
|
||||
static constexpr IRCode MODES[] PROGMEM = {
|
||||
{climate::CLIMATE_MODE_HEAT_COOL, BYTE7_MODE_AUTO},
|
||||
{climate::CLIMATE_MODE_COOL, 0x80},
|
||||
{climate::CLIMATE_MODE_DRY, 0x40},
|
||||
{climate::CLIMATE_MODE_FAN_ONLY, 0xC0},
|
||||
{climate::CLIMATE_MODE_HEAT, 0x20},
|
||||
};
|
||||
static constexpr Code FANS[] PROGMEM = {
|
||||
static constexpr IRCode FANS[] PROGMEM = {
|
||||
{climate::CLIMATE_FAN_AUTO, BYTE8_FAN_AUTO}, {climate::CLIMATE_FAN_HIGH, 0x80},
|
||||
{climate::CLIMATE_FAN_MEDIUM, 0x40}, {climate::CLIMATE_FAN_LOW, 0xC0},
|
||||
{climate::CLIMATE_FAN_QUIET, 0x20},
|
||||
};
|
||||
|
||||
// The entry whose value (or code) matches key. Entries are copied out of PROGMEM.
|
||||
template<size_t N> static optional<Code> find_entry(const Code (&table)[N], uint8_t key, bool match_code) {
|
||||
for (const Code &slot : table) {
|
||||
Code entry;
|
||||
progmem_memcpy(&entry, &slot, sizeof(entry));
|
||||
if ((match_code ? entry.code : entry.value) == key)
|
||||
return entry;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// The temperature code is the 4-bit reversal of (F - TEMP_MIN) / TEMP_STEP; the mapping is its own inverse.
|
||||
static uint8_t reverse_nibble(uint8_t value) { return reverse_bits(value) >> 4; }
|
||||
|
||||
@@ -151,14 +142,11 @@ void FriedrichClimate::transmit_state() {
|
||||
temperature += temperature % 2;
|
||||
this->target_temperature = temperature;
|
||||
|
||||
const auto mode = find_entry(MODES, this->mode, false);
|
||||
const auto fan = find_entry(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), false);
|
||||
|
||||
uint8_t frame[STATE_MESSAGE_LENGTH];
|
||||
progmem_memcpy(frame, STATE_TEMPLATE, sizeof(frame));
|
||||
frame[6] |= reverse_nibble((temperature - TEMP_MIN) / TEMP_STEP);
|
||||
frame[7] = mode.has_value() ? mode->code : BYTE7_MODE_AUTO;
|
||||
frame[8] = fan.has_value() ? fan->code : BYTE8_FAN_AUTO;
|
||||
frame[7] = to_ir_code(MODES, this->mode, BYTE7_MODE_AUTO);
|
||||
frame[8] = to_ir_code(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), BYTE8_FAN_AUTO);
|
||||
if (this->swing_mode != climate::CLIMATE_SWING_OFF)
|
||||
frame[8] |= BYTE8_FAN_SWING;
|
||||
frame[STATE_MESSAGE_LENGTH - 1] = checksum_state(frame);
|
||||
@@ -209,7 +197,7 @@ bool FriedrichClimate::on_receive(remote_base::RemoteReceiveData src) {
|
||||
return false;
|
||||
|
||||
// Ignore frames with a mode this component does not expose (e.g. MIN_HEAT, COIL_DRY)
|
||||
const auto mode = find_entry(MODES, d[7], true);
|
||||
const auto mode = find_ir_code(MODES, d[7], true);
|
||||
if (!mode.has_value()) {
|
||||
ESP_LOGV(TAG, "Ignoring unknown mode 0x%02X", d[7]);
|
||||
return false;
|
||||
@@ -224,8 +212,7 @@ bool FriedrichClimate::on_receive(remote_base::RemoteReceiveData src) {
|
||||
this->target_temperature = TEMP_MIN + temp_index * TEMP_STEP;
|
||||
|
||||
this->swing_mode = (d[8] & BYTE8_FAN_SWING) ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
|
||||
const auto fan = find_entry(FANS, d[8] & ~BYTE8_FAN_SWING, true);
|
||||
this->fan_mode = fan.has_value() ? static_cast<climate::ClimateFanMode>(fan->value) : climate::CLIMATE_FAN_AUTO;
|
||||
this->fan_mode = from_ir_code(FANS, d[8] & ~BYTE8_FAN_SWING, climate::CLIMATE_FAN_AUTO);
|
||||
|
||||
this->publish_state();
|
||||
return true;
|
||||
|
||||
@@ -21,19 +21,16 @@ constexpr uint8_t MISTRAL_POWER_ON = 0x20;
|
||||
constexpr uint8_t MISTRAL_TEMP_OFFSET = 0x9F;
|
||||
constexpr uint8_t MISTRAL_MODE_AUTO = 0x10;
|
||||
|
||||
// The tables live in flash, so entries are copied out before use
|
||||
struct ModeCode {
|
||||
climate::ClimateMode mode;
|
||||
uint8_t code;
|
||||
};
|
||||
static const ModeCode MODES[] PROGMEM = {
|
||||
// OFF sends the auto mode code and only clears the power byte
|
||||
static constexpr climate_ir::IRCode MODES[] PROGMEM = {
|
||||
{climate::CLIMATE_MODE_HEAT_COOL, MISTRAL_MODE_AUTO},
|
||||
{climate::CLIMATE_MODE_COOL, 0xC0},
|
||||
{climate::CLIMATE_MODE_DRY, 0x40},
|
||||
{climate::CLIMATE_MODE_FAN_ONLY, 0x80},
|
||||
{climate::CLIMATE_MODE_HEAT, 0x60},
|
||||
{climate::CLIMATE_MODE_HEAT_COOL, MISTRAL_MODE_AUTO},
|
||||
};
|
||||
|
||||
// Each fan speed sets two bytes; the table lives in flash, so entries are copied out before use
|
||||
struct FanCode {
|
||||
climate::ClimateFanMode fan;
|
||||
uint8_t byte1;
|
||||
@@ -78,7 +75,6 @@ static bool is_valid_frame(const uint8_t *frame) {
|
||||
}
|
||||
|
||||
void MistralIR::transmit_state() {
|
||||
const auto mode = find_entry(MODES, [this](const ModeCode &m) { return m.mode == this->mode; });
|
||||
const climate::ClimateFanMode fan_mode = this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO);
|
||||
const FanCode fan =
|
||||
find_entry(FANS, [fan_mode](const FanCode &f) { return f.fan == fan_mode; }).value_or(load_entry(FANS[0]));
|
||||
@@ -88,7 +84,7 @@ void MistralIR::transmit_state() {
|
||||
progmem_memcpy(frame, FRAME_TEMPLATE, sizeof(frame));
|
||||
frame[1] = fan.byte1;
|
||||
frame[3] = this->mode == climate::CLIMATE_MODE_OFF ? MISTRAL_POWER_OFF : MISTRAL_POWER_ON;
|
||||
frame[4] = mode.has_value() ? mode->code : MISTRAL_MODE_AUTO;
|
||||
frame[4] = climate_ir::to_ir_code(MODES, this->mode, MISTRAL_MODE_AUTO);
|
||||
frame[5] = reverse_bits(static_cast<uint8_t>(MISTRAL_TEMP_OFFSET - temp));
|
||||
frame[6] = fan.byte6;
|
||||
frame[11] = compute_checksum(frame);
|
||||
@@ -116,12 +112,12 @@ bool MistralIR::on_receive(remote_base::RemoteReceiveData data) {
|
||||
return false;
|
||||
|
||||
// Decode everything before touching the state so an unknown code leaves it untouched
|
||||
const auto mode = find_entry(MODES, [frame](const ModeCode &m) { return m.code == frame[4]; });
|
||||
const auto mode = climate_ir::find_ir_code(MODES, frame[4], true);
|
||||
const auto fan = find_entry(FANS, [frame](const FanCode &f) { return f.byte1 == frame[1] && f.byte6 == frame[6]; });
|
||||
if (!mode.has_value() || !fan.has_value())
|
||||
return false;
|
||||
|
||||
this->mode = mode->mode;
|
||||
this->mode = static_cast<climate::ClimateMode>(mode->value);
|
||||
this->target_temperature =
|
||||
clamp<float>(MISTRAL_TEMP_OFFSET - reverse_bits(frame[5]), MISTRAL_TEMP_MIN, MISTRAL_TEMP_MAX);
|
||||
this->fan_mode = fan->fan;
|
||||
|
||||
@@ -47,46 +47,28 @@ static bool is_powered_on(const uint8_t *state) {
|
||||
return get_field(state, POWER_1) == POWER_ON && get_field(state, POWER_2) == POWER_ON;
|
||||
}
|
||||
|
||||
struct Code {
|
||||
uint8_t value; // a climate enum, all of which are uint8_t
|
||||
uint8_t code;
|
||||
};
|
||||
// The first entry of each table is the fallback in both directions
|
||||
static constexpr Code MODES[] PROGMEM = {
|
||||
using climate_ir::from_ir_code;
|
||||
using climate_ir::IRCode;
|
||||
using climate_ir::to_ir_code;
|
||||
|
||||
// Codes not listed fall back to auto (mode and fan) and to swing off
|
||||
static constexpr IRCode MODES[] PROGMEM = {
|
||||
{climate::CLIMATE_MODE_HEAT_COOL, 0}, {climate::CLIMATE_MODE_COOL, 1}, {climate::CLIMATE_MODE_DRY, 2},
|
||||
{climate::CLIMATE_MODE_FAN_ONLY, 3}, {climate::CLIMATE_MODE_HEAT, 4},
|
||||
};
|
||||
static constexpr Code FANS[] PROGMEM = {
|
||||
static constexpr IRCode FANS[] PROGMEM = {
|
||||
{climate::CLIMATE_FAN_AUTO, 0},
|
||||
{climate::CLIMATE_FAN_LOW, 2},
|
||||
{climate::CLIMATE_FAN_MEDIUM, 4},
|
||||
{climate::CLIMATE_FAN_HIGH, 5},
|
||||
};
|
||||
static constexpr Code SWINGS[] PROGMEM = {
|
||||
static constexpr IRCode SWINGS[] PROGMEM = {
|
||||
{climate::CLIMATE_SWING_OFF, 0b111},
|
||||
{climate::CLIMATE_SWING_VERTICAL, 0b010},
|
||||
{climate::CLIMATE_SWING_HORIZONTAL, 0b011},
|
||||
{climate::CLIMATE_SWING_BOTH, 0b100},
|
||||
};
|
||||
|
||||
// Returns the entry whose value (or code) matches key, else the first entry. Entries are copied out of PROGMEM.
|
||||
static Code find_entry(const Code *table, size_t count, uint8_t key, bool match_code) {
|
||||
Code entry{};
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
progmem_memcpy(&entry, &table[i], sizeof(entry));
|
||||
if ((match_code ? entry.code : entry.value) == key)
|
||||
return entry;
|
||||
}
|
||||
progmem_memcpy(&entry, table, sizeof(entry));
|
||||
return entry;
|
||||
}
|
||||
template<size_t N> static uint8_t to_code(const Code (&table)[N], uint8_t value) {
|
||||
return find_entry(table, N, value, false).code;
|
||||
}
|
||||
template<typename T, size_t N> static T from_code(const Code (&table)[N], uint8_t code) {
|
||||
return static_cast<T>(find_entry(table, N, code, true).value);
|
||||
}
|
||||
|
||||
// Inverted count of the one bits in a section, skipping the two nibbles that hold the checksum itself.
|
||||
// See https://github.com/crankyoldgit/IRremoteESP8266/issues/1538#issuecomment-894645947
|
||||
static uint8_t section_checksum(const uint8_t *section) {
|
||||
@@ -123,13 +105,13 @@ void SamsungClimate::transmit_state() {
|
||||
|
||||
if (on) {
|
||||
progmem_memcpy(state, RESET_STATE, SAMSUNG_AC_STATE_LENGTH);
|
||||
set_field(state, MODE, to_code(MODES, this->mode));
|
||||
set_field(state, MODE, to_ir_code(MODES, this->mode, 0));
|
||||
} // else keep the last mode so the power off frame still carries a valid state
|
||||
|
||||
const uint8_t temp = clamp<uint8_t>(this->target_temperature, SAMSUNG_AC_TEMP_MIN, SAMSUNG_AC_TEMP_MAX);
|
||||
set_field(state, TEMP, temp - SAMSUNG_AC_TEMP_MIN);
|
||||
set_field(state, SWING, to_code(SWINGS, this->swing_mode));
|
||||
set_field(state, FAN, to_code(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)));
|
||||
set_field(state, SWING, to_ir_code(SWINGS, this->swing_mode, 0b111));
|
||||
set_field(state, FAN, to_ir_code(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), 0));
|
||||
set_field(state, POWER_1, on ? POWER_ON : 0);
|
||||
set_field(state, POWER_2, on ? POWER_ON : 0);
|
||||
|
||||
@@ -212,11 +194,11 @@ bool SamsungClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
memcpy(state, frame, SAMSUNG_AC_SECTION_LENGTH);
|
||||
memcpy(state + SAMSUNG_AC_SECTION_LENGTH, frame + length - SAMSUNG_AC_SECTION_LENGTH, SAMSUNG_AC_SECTION_LENGTH);
|
||||
|
||||
this->swing_mode = from_code<climate::ClimateSwingMode>(SWINGS, get_field(state, SWING));
|
||||
this->swing_mode = from_ir_code(SWINGS, get_field(state, SWING), climate::CLIMATE_SWING_OFF);
|
||||
this->target_temperature = get_field(state, TEMP) + SAMSUNG_AC_TEMP_MIN;
|
||||
this->fan_mode = from_code<climate::ClimateFanMode>(FANS, get_field(state, FAN));
|
||||
this->mode =
|
||||
is_powered_on(state) ? from_code<climate::ClimateMode>(MODES, get_field(state, MODE)) : climate::CLIMATE_MODE_OFF;
|
||||
this->fan_mode = from_ir_code(FANS, get_field(state, FAN), climate::CLIMATE_FAN_AUTO);
|
||||
this->mode = is_powered_on(state) ? from_ir_code(MODES, get_field(state, MODE), climate::CLIMATE_MODE_HEAT_COOL)
|
||||
: climate::CLIMATE_MODE_OFF;
|
||||
|
||||
ESP_LOGD(TAG, "Received %u byte frame, power %s", length, ONOFF(is_powered_on(state)));
|
||||
this->publish_state();
|
||||
|
||||
@@ -6,70 +6,32 @@ namespace esphome::siemens_ira211 {
|
||||
|
||||
using namespace remote_base;
|
||||
|
||||
struct PresetMapping {
|
||||
climate::ClimatePreset preset;
|
||||
IRA211Mode mode;
|
||||
// Presets map to IRA211Mode and fan modes to IRA211Fan. Values the other side does not know fall back to comfort
|
||||
// and auto.
|
||||
static constexpr climate_ir::IRCode PRESETS[] PROGMEM = {
|
||||
{climate::CLIMATE_PRESET_COMFORT, static_cast<uint8_t>(IRA211Mode::IRA211_MODE_COMFORT)},
|
||||
{climate::CLIMATE_PRESET_ECO, static_cast<uint8_t>(IRA211Mode::IRA211_MODE_TIMER)},
|
||||
};
|
||||
static constexpr climate_ir::IRCode FANS[] PROGMEM = {
|
||||
{climate::CLIMATE_FAN_AUTO, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_AUTO)},
|
||||
{climate::CLIMATE_FAN_LOW, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_LOW)},
|
||||
{climate::CLIMATE_FAN_MEDIUM, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_MEDIUM)},
|
||||
{climate::CLIMATE_FAN_HIGH, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_HIGH)},
|
||||
};
|
||||
|
||||
static const PresetMapping PRESETS[] PROGMEM = {
|
||||
{climate::CLIMATE_PRESET_COMFORT, IRA211Mode::IRA211_MODE_COMFORT},
|
||||
{climate::CLIMATE_PRESET_ECO, IRA211Mode::IRA211_MODE_TIMER},
|
||||
};
|
||||
|
||||
struct FanMapping {
|
||||
climate::ClimateFanMode fan_mode;
|
||||
IRA211Fan fan;
|
||||
};
|
||||
|
||||
static const FanMapping FANS[] PROGMEM = {
|
||||
{climate::CLIMATE_FAN_AUTO, IRA211Fan::IRA211_FAN_AUTO},
|
||||
{climate::CLIMATE_FAN_LOW, IRA211Fan::IRA211_FAN_LOW},
|
||||
{climate::CLIMATE_FAN_MEDIUM, IRA211Fan::IRA211_FAN_MEDIUM},
|
||||
{climate::CLIMATE_FAN_HIGH, IRA211Fan::IRA211_FAN_HIGH},
|
||||
};
|
||||
|
||||
// The tables live in flash, so entries are copied out before use
|
||||
template<typename T> static T load_entry(const T &slot) {
|
||||
T entry;
|
||||
progmem_memcpy(&entry, &slot, sizeof(T));
|
||||
return entry;
|
||||
}
|
||||
|
||||
// The first entry of each table is the fallback for values the other side does not know
|
||||
static IRA211Mode to_ira211_mode(climate::ClimatePreset preset) {
|
||||
for (const auto &slot : PRESETS) {
|
||||
const auto entry = load_entry(slot);
|
||||
if (entry.preset == preset)
|
||||
return entry.mode;
|
||||
}
|
||||
return load_entry(PRESETS[0]).mode;
|
||||
return static_cast<IRA211Mode>(
|
||||
climate_ir::to_ir_code(PRESETS, preset, static_cast<uint8_t>(IRA211Mode::IRA211_MODE_COMFORT)));
|
||||
}
|
||||
|
||||
static climate::ClimatePreset to_climate_preset(IRA211Mode mode) {
|
||||
for (const auto &slot : PRESETS) {
|
||||
const auto entry = load_entry(slot);
|
||||
if (entry.mode == mode)
|
||||
return entry.preset;
|
||||
}
|
||||
return load_entry(PRESETS[0]).preset;
|
||||
return climate_ir::from_ir_code(PRESETS, static_cast<uint8_t>(mode), climate::CLIMATE_PRESET_COMFORT);
|
||||
}
|
||||
|
||||
static IRA211Fan to_ira211_fan(climate::ClimateFanMode fan_mode) {
|
||||
for (const auto &slot : FANS) {
|
||||
const auto entry = load_entry(slot);
|
||||
if (entry.fan_mode == fan_mode)
|
||||
return entry.fan;
|
||||
}
|
||||
return load_entry(FANS[0]).fan;
|
||||
return static_cast<IRA211Fan>(
|
||||
climate_ir::to_ir_code(FANS, fan_mode, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_AUTO)));
|
||||
}
|
||||
|
||||
static climate::ClimateFanMode to_climate_fan_mode(IRA211Fan fan) {
|
||||
for (const auto &slot : FANS) {
|
||||
const auto entry = load_entry(slot);
|
||||
if (entry.fan == fan)
|
||||
return entry.fan_mode;
|
||||
}
|
||||
return load_entry(FANS[0]).fan_mode;
|
||||
return climate_ir::from_ir_code(FANS, static_cast<uint8_t>(fan), climate::CLIMATE_FAN_AUTO);
|
||||
}
|
||||
|
||||
void SiemensIRA211Climate::setup() {
|
||||
|
||||
@@ -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")
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -337,6 +337,57 @@ size_t truncate_name_to(char *buffer, size_t buffer_size, const char *name, size
|
||||
|
||||
// Parsing & formatting
|
||||
|
||||
// A plain decimal converts exactly with one division: the digits accumulate
|
||||
// into an integer that stays below 2^24 and the fraction's scale is a power of
|
||||
// ten up to 1e9 (2^9 * 5^9), so both convert to float exactly and IEEE division
|
||||
// rounds the quotient correctly, the same result strtof produces.
|
||||
static constexpr uint32_t PARSE_FLOAT_MANTISSA_LIMIT = 1000000; // one more digit stays below 2^24
|
||||
static constexpr uint32_t PARSE_FLOAT_MAX_FRACTION_DIGITS = 9;
|
||||
|
||||
optional<float> parse_float(const char *str) {
|
||||
const char *p = str;
|
||||
const bool negative = *p == '-';
|
||||
if (negative || *p == '+')
|
||||
p++;
|
||||
const char *digits = p;
|
||||
const char *dot = nullptr;
|
||||
uint32_t mantissa = 0;
|
||||
for (;; p++) {
|
||||
const uint32_t d = static_cast<unsigned char>(*p) - '0';
|
||||
if (d > 9) {
|
||||
if (*p == '.' && dot == nullptr) {
|
||||
dot = p;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mantissa >= PARSE_FLOAT_MANTISSA_LIMIT)
|
||||
break;
|
||||
mantissa = mantissa * 10 + d;
|
||||
}
|
||||
const uint32_t fraction_digits = dot != nullptr ? p - dot - 1 : 0;
|
||||
const bool any_digit = p - digits > (dot != nullptr ? 1 : 0);
|
||||
if (*p == '\0' && any_digit && fraction_digits <= PARSE_FLOAT_MAX_FRACTION_DIGITS) {
|
||||
// The mantissa is exact in a float, so the sign goes on before the one rounding step
|
||||
float value = static_cast<float>(mantissa);
|
||||
if (negative)
|
||||
value = -value;
|
||||
if (fraction_digits != 0) {
|
||||
uint32_t scale = 10;
|
||||
for (uint32_t i = 1; i < fraction_digits; i++)
|
||||
scale *= 10;
|
||||
value /= static_cast<float>(scale);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
// Exponents, hex, inf, nan, leading whitespace and longer inputs take the libc path.
|
||||
char *end = nullptr;
|
||||
const float value = ::strtof(str, &end);
|
||||
if (end == str || *end != '\0' || value == HUGE_VALF)
|
||||
return {};
|
||||
return value;
|
||||
}
|
||||
|
||||
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
|
||||
size_t chars = std::min(length, 2 * count);
|
||||
for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) {
|
||||
|
||||
+2
-21
@@ -25,7 +25,6 @@
|
||||
#include <strings.h>
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/parse_float.h"
|
||||
#include "esphome/core/time_conversion.h"
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
@@ -1034,26 +1033,6 @@ inline float pow10_int(int8_t exp) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 10^n, exact while it fits in T.
|
||||
template<typename T> constexpr T pow10_exact(uint32_t n) {
|
||||
T result = 1;
|
||||
T base = 10;
|
||||
for (;;) {
|
||||
if ((n & 1) != 0)
|
||||
result *= base;
|
||||
n >>= 1;
|
||||
if (n == 0)
|
||||
return result;
|
||||
base *= base;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether c is '0' to '9'.
|
||||
constexpr bool is_digit(char c) { return static_cast<uint8_t>(c - '0') <= 9; }
|
||||
|
||||
/// Whether c is C-locale white space, without isspace's ctype table.
|
||||
constexpr bool is_space_c(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); }
|
||||
|
||||
/// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out).
|
||||
template<typename T, typename U> T remap(U value, U min, U max, T min_out, T max_out) {
|
||||
return (value - min) * (max_out - min_out) / (max - min) + min_out;
|
||||
@@ -1594,6 +1573,8 @@ template<typename T, enable_if_t<(std::is_integral<T>::value && std::is_signed<T
|
||||
optional<T> parse_number(const std::string &str) {
|
||||
return parse_number<T>(str.c_str());
|
||||
}
|
||||
/// Parse a decimal floating-point number; plain decimals skip the libc parser.
|
||||
optional<float> parse_float(const char *str);
|
||||
/// Parse a decimal floating-point number from a null-terminated string.
|
||||
template<typename T, enable_if_t<(std::is_same<T, float>::value), int> = 0> optional<T> parse_number(const char *str) {
|
||||
return parse_float(str);
|
||||
|
||||
@@ -1,372 +0,0 @@
|
||||
// picolibc 1.8.10's strtof, bit for bit, without libc's strtod: decimals round to 9 digits, then to the nearest float.
|
||||
// Float bits are built from integers, so chips that flush denormals give the same result.
|
||||
|
||||
#include "esphome/core/parse_float.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t FLOAT_SIGN_BIT = 0x80000000, FLOAT_INF_BITS = 0x7F800000;
|
||||
constexpr uint32_t FLOAT_NAN_BITS = 0x7FC00000; // nanf(""), which strtof returns for every NaN
|
||||
|
||||
constexpr uint32_t DECIMAL_DIGITS = 9, HEX_DIGITS = 8; // digits kept before the rest only round
|
||||
constexpr int32_t EXPONENT_CAP = 224; // picolibc stops adding exponent digits once the exponent reaches this
|
||||
// 0.<digits> * 10^exp10 is zero at or below MIN_EXP10 and infinity at or above MAX_EXP10, as in picolibc
|
||||
constexpr int32_t MIN_EXP10 = -46, MAX_EXP10 = 40;
|
||||
// Past this, one more digit could pass 2^24
|
||||
constexpr uint32_t PLAIN_MANTISSA_LIMIT = 1677720;
|
||||
|
||||
// Bits of the float nearest to (m + sticky fraction) * 2^e, ties to even; m > 0
|
||||
uint32_t round_to_float_bits(uint64_t m, int32_t e, bool sticky) {
|
||||
const int32_t msb = e + static_cast<int32_t>(std::bit_width(m)) - 1; // the value is in [2^msb, 2^(msb + 1))
|
||||
if (msb >= 128)
|
||||
return FLOAT_INF_BITS;
|
||||
if (msb < -150) // below half the smallest float
|
||||
return 0;
|
||||
// The unit of the float's last bit: 2^(msb - 23), or 2^-149 for a denormal
|
||||
const int32_t shift = std::max<int32_t>(msb - 23, -149) - e; // at most 64
|
||||
uint64_t q;
|
||||
if (shift <= 0) {
|
||||
q = m << -shift;
|
||||
} else {
|
||||
q = shift < 64 ? m >> shift : 0;
|
||||
const uint64_t rem = shift < 64 ? m & ((uint64_t{1} << shift) - 1) : m;
|
||||
const uint64_t half = uint64_t{1} << (shift - 1);
|
||||
if (rem > half || (rem == half && (sticky || (q & 1) != 0)))
|
||||
q++;
|
||||
}
|
||||
if (msb < -126)
|
||||
return static_cast<uint32_t>(q); // 2^23 here is the smallest normal float, as it should be
|
||||
// Rounding up past the largest float gives exactly FLOAT_INF_BITS
|
||||
return (static_cast<uint32_t>(msb + 127) << 23) + static_cast<uint32_t>(q - (uint64_t{1} << 23));
|
||||
}
|
||||
|
||||
// The decimal exponents that reach decimal_to_float_bits: 1 to 9 digits and MIN_EXP10 < digits + e10 < MAX_EXP10
|
||||
constexpr int32_t MIN_E10 = MIN_EXP10 + 1 - static_cast<int32_t>(DECIMAL_DIGITS), MAX_E10 = MAX_EXP10 - 2;
|
||||
// Top 64 bits of 10^(8k), rounded down: 10^(8k) = entry * 2^(floor_log2_pow10(8k) - 63), exact for k = 0..3
|
||||
constexpr int32_t POW10_BY_8_FIRST = MIN_E10 >> 3, POW10_BY_8_LAST = MAX_E10 >> 3;
|
||||
constexpr uint64_t POW10_BY_8[] PROGMEM = {
|
||||
0xFB158592BE068D2E, 0xBB127C53B17EC159, 0x8B61313BBABCE2C6, 0xCFB11EAD453994BA,
|
||||
0x9ABE14CD44753B52, 0xE69594BEC44DE15B, 0xABCC77118461CEFC, 0x8000000000000000,
|
||||
0xBEBC200000000000, 0x8E1BC9BF04000000, 0xD3C21BCECCEDA100, 0x9DC5ADA82B70B59D,
|
||||
};
|
||||
static_assert(POW10_BY_8_FIRST == -7 && POW10_BY_8_LAST == 4 &&
|
||||
sizeof(POW10_BY_8) / sizeof(POW10_BY_8[0]) == POW10_BY_8_LAST - POW10_BY_8_FIRST + 1,
|
||||
"POW10_BY_8 must cover MIN_E10..MAX_E10");
|
||||
|
||||
// floor(q * log2(10)), for |q| < 100
|
||||
constexpr int32_t floor_log2_pow10(int32_t q) { return (q * 217706) >> 16; }
|
||||
|
||||
// The top 64 bits of a * b, rounded down
|
||||
uint64_t mul_high(uint64_t a, uint64_t b) {
|
||||
const uint64_t a_lo = static_cast<uint32_t>(a), a_hi = a >> 32;
|
||||
const uint64_t b_lo = static_cast<uint32_t>(b), b_hi = b >> 32;
|
||||
const uint64_t hi_lo = a_hi * b_lo, lo_hi = a_lo * b_hi;
|
||||
const uint64_t middle = ((a_lo * b_lo) >> 32) + static_cast<uint32_t>(hi_lo) + static_cast<uint32_t>(lo_hi);
|
||||
return a_hi * b_hi + (hi_lo >> 32) + (lo_hi >> 32) + (middle >> 32);
|
||||
}
|
||||
|
||||
// Cold: a decimal within 16 units of a midpoint is an exact tie when m10 * 5^e10 is an integer
|
||||
__attribute__((noinline, cold)) uint32_t tie_float_bits(uint32_t m10, int32_t e10, uint32_t bits) {
|
||||
if (e10 >= 0) {
|
||||
if (e10 <= 14) // m10 * 5^14 stays below 2^64
|
||||
return round_to_float_bits(m10 * (pow10_exact<uint64_t>(e10) >> e10), e10, false);
|
||||
} else if (e10 >= -13) {
|
||||
const auto pow5 = static_cast<uint32_t>(pow10_exact<uint64_t>(-e10) >> -e10);
|
||||
if (m10 % pow5 == 0)
|
||||
return round_to_float_bits(m10 / pow5, e10, false);
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
// The float nearest to m10 * 10^e10 (0 < m10 <= 10^9). The product is under 4 units low; checked for all 85 billion
|
||||
// (m10, e10), only exact ties and one non-tie (6 units off, keeps its float) come within 34 units of a midpoint.
|
||||
uint32_t decimal_to_float_bits(uint32_t m10, int32_t e10) {
|
||||
// Both operands are exact floats, so the one float operation gives the nearest float
|
||||
const uint32_t power = e10 < 0 ? -e10 : e10;
|
||||
if (m10 < (1u << 24) && power <= 10) {
|
||||
const auto value = static_cast<float>(m10);
|
||||
// 10^10 overflows uint32_t
|
||||
const float factor = power == 10 ? 1e10f : static_cast<float>(pow10_exact<uint32_t>(power));
|
||||
return bit_cast<uint32_t>(e10 < 0 ? value / factor : value * factor);
|
||||
}
|
||||
const int32_t k8 = e10 >> 3; // rounds down
|
||||
const uint64_t x = uint64_t{m10} * pow10_exact<uint32_t>(e10 & 7);
|
||||
const auto zeros = static_cast<int32_t>(std::countl_zero(x));
|
||||
uint64_t m = x << zeros;
|
||||
int32_t e = -zeros;
|
||||
if (k8 == 0)
|
||||
return round_to_float_bits(m, e, false); // exact
|
||||
m = mul_high(m, POW10_BY_8[k8 - POW10_BY_8_FIRST]);
|
||||
e += floor_log2_pow10(8 * k8) + 1;
|
||||
if ((m >> 63) == 0) {
|
||||
m <<= 1;
|
||||
e--;
|
||||
}
|
||||
const uint32_t bits = round_to_float_bits(m, e, false);
|
||||
// The bits below the float's last one, and the midpoint among them
|
||||
const int32_t shift = std::max<int32_t>(40, -149 - e);
|
||||
if (shift <= 64) {
|
||||
const uint64_t low = shift < 64 ? m & ((uint64_t{1} << shift) - 1) : m;
|
||||
const uint64_t half = uint64_t{1} << (shift - 1);
|
||||
if (low - half + 16 > 32) // |low - half| > 16
|
||||
return bits;
|
||||
}
|
||||
return tie_float_bits(m10, e10, bits);
|
||||
}
|
||||
|
||||
uint32_t no_number(const char *str, const char **end, bool *negative) {
|
||||
*end = str;
|
||||
*negative = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The rest of an "nan" or "inf" after its first letter at p; returns the end, or nullptr for no number
|
||||
const char *read_inf_nan(const char *p) {
|
||||
const auto lower = [](char c) { return static_cast<char>(c | 0x20); };
|
||||
if (lower(*p) == 'n') {
|
||||
if (lower(p[1]) != 'a' || lower(p[2]) != 'n')
|
||||
return nullptr;
|
||||
p += 3;
|
||||
if (*p != '(')
|
||||
return p;
|
||||
// "nan(" must be closed after letters, digits and '_'
|
||||
do {
|
||||
p++;
|
||||
} while (static_cast<uint8_t>(lower(*p) - 'a') < 26 || is_digit(*p) || *p == '_');
|
||||
return *p == ')' ? p + 1 : nullptr;
|
||||
}
|
||||
if (lower(p[1]) != 'n' || lower(p[2]) != 'f')
|
||||
return nullptr;
|
||||
p += 3;
|
||||
if (lower(*p) != 'i')
|
||||
return p;
|
||||
// Past "infi", only "infinity" is a number
|
||||
return lower(p[1]) == 'n' && lower(p[2]) == 'i' && lower(p[3]) == 't' && lower(p[4]) == 'y' ? p + 5 : nullptr;
|
||||
}
|
||||
|
||||
// Reads a binary exponent "p" or decimal exponent "e" at p into *exp; returns the end of the number
|
||||
const char *read_exponent(const char *p, char letter, int32_t *exp) {
|
||||
if ((*p | 0x20) != letter)
|
||||
return p;
|
||||
const char *q = p + 1;
|
||||
const bool negative = *q == '-';
|
||||
if (negative || *q == '+')
|
||||
q++;
|
||||
if (!is_digit(*q))
|
||||
return p;
|
||||
int32_t value = 0;
|
||||
for (; is_digit(*q); q++) {
|
||||
if (value < EXPONENT_CAP)
|
||||
value = value * 10 + (*q - '0');
|
||||
}
|
||||
*exp += negative ? -value : value;
|
||||
return q;
|
||||
}
|
||||
|
||||
// Hex digits after the "x" at p, with the point and decimal exponent read so far (no non-zero digit yet)
|
||||
__attribute__((noinline, cold)) uint32_t read_hex(const char *p, bool dot, int32_t exp, const char **end) {
|
||||
uint32_t m = 0, kept = 0; // kept: digits in m from its first non-zero one
|
||||
bool sticky = false;
|
||||
for (;; p++) {
|
||||
const char lower = static_cast<char>(*p | 0x20);
|
||||
uint32_t digit = static_cast<uint8_t>(*p - '0');
|
||||
if (static_cast<uint8_t>(lower - 'a') <= 5) {
|
||||
digit = static_cast<uint8_t>(lower - 'a') + 10;
|
||||
} else if (digit > 9) {
|
||||
digit = 16; // picolibc takes ':' to '?' as 10 to 15; we stop there like newlib
|
||||
}
|
||||
if (digit < 16) {
|
||||
if (kept < HEX_DIGITS) {
|
||||
if (dot)
|
||||
exp--;
|
||||
m = m * 16 + digit;
|
||||
kept += m != 0 ? 1 : 0;
|
||||
} else {
|
||||
if (!dot)
|
||||
exp++;
|
||||
sticky |= digit != 0;
|
||||
}
|
||||
} else if (*p == '.' && !dot) {
|
||||
dot = true;
|
||||
} else if (lower != 'x' || m != 0) { // picolibc takes another "x" while the value is still zero
|
||||
break;
|
||||
}
|
||||
}
|
||||
exp *= 4;
|
||||
*end = read_exponent(p, 'p', &exp);
|
||||
return m == 0 ? 0 : round_to_float_bits(m, exp, sticky);
|
||||
}
|
||||
|
||||
// picolibc's conv_flt for a float from the first digit or point at p: returns the bits; *end is str for no number
|
||||
__attribute__((noinline)) uint32_t read_decimal(const char *str, const char *p, const char **end) {
|
||||
uint32_t m = 0, kept = 0; // the first (up to 9) significant digits, and how many from the first non-zero one
|
||||
int32_t exp = 0; // the value is m * 10^exp
|
||||
bool dot = false, any = false;
|
||||
uint32_t overflow = 0; // digits past the kept ones: 0 none, 1 below half, 2 half, 3 above half
|
||||
bool full = false; // a digit past the kept ones was read
|
||||
for (;; p++) {
|
||||
const uint32_t digit = static_cast<uint8_t>(*p - '0');
|
||||
if (digit <= 9) {
|
||||
any = true;
|
||||
if (kept < DECIMAL_DIGITS) {
|
||||
if (dot)
|
||||
exp--;
|
||||
m = m * 10 + digit;
|
||||
kept += m != 0 ? 1 : 0;
|
||||
} else {
|
||||
if (!dot)
|
||||
exp++;
|
||||
if (!full) {
|
||||
full = true;
|
||||
overflow = digit > 5 ? 3 : digit == 5 ? 2 : digit != 0 ? 1 : 0; // the first one decides the half
|
||||
} else {
|
||||
overflow |= digit != 0 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
} else if (*p == '.' && !dot) {
|
||||
dot = true;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((*p | 0x20) == 'x' && any && m == 0)
|
||||
return read_hex(p + 1, dot, exp, end);
|
||||
if (!any) {
|
||||
*end = str;
|
||||
return 0;
|
||||
}
|
||||
*end = read_exponent(p, 'e', &exp);
|
||||
if (m == 0)
|
||||
return 0;
|
||||
// Ties to even on the decimal digits
|
||||
overflow |= m & 1;
|
||||
m += (overflow + 1) >> 2;
|
||||
const int32_t exp10 = static_cast<int32_t>(kept) + exp;
|
||||
if (exp10 <= MIN_EXP10)
|
||||
return 0;
|
||||
if (exp10 >= MAX_EXP10)
|
||||
return FLOAT_INF_BITS;
|
||||
return decimal_to_float_bits(m, exp);
|
||||
}
|
||||
|
||||
// picolibc's strtof from the start: returns the bits without the sign, which goes in *negative
|
||||
__attribute__((noinline)) uint32_t parse_float_full(const char *str, const char **end, bool *negative) {
|
||||
// No number starts with a letter but i (inf) or n (nan): "unavailable", "on" and "unknown" stop here
|
||||
const char first = static_cast<char>(*str | 0x20);
|
||||
if (first >= 'a' && first <= 'z' && first != 'i' && first != 'n')
|
||||
return no_number(str, end, negative);
|
||||
const char *p = str;
|
||||
while (is_space_c(*p))
|
||||
p++;
|
||||
*negative = *p == '-';
|
||||
if (*negative || *p == '+')
|
||||
p++;
|
||||
const char lower = static_cast<char>(*p | 0x20);
|
||||
if (lower == 'i' || lower == 'n') {
|
||||
const char *stop = read_inf_nan(p);
|
||||
if (stop == nullptr)
|
||||
return no_number(str, end, negative);
|
||||
*end = stop;
|
||||
return lower == 'i' ? FLOAT_INF_BITS : FLOAT_NAN_BITS;
|
||||
}
|
||||
const uint32_t bits = read_decimal(str, p, end);
|
||||
if (*end == str)
|
||||
*negative = false;
|
||||
return bits;
|
||||
}
|
||||
|
||||
// A plain decimal below 2^24 with up to 9 digits after the point, in one float operation; nullptr otherwise
|
||||
ESPHOME_ALWAYS_INLINE inline const char *parse_plain_decimal(const char *str, uint32_t *bits, bool *negative) {
|
||||
const char *p = str;
|
||||
*negative = *p == '-';
|
||||
if (*negative || *p == '+')
|
||||
p++;
|
||||
const char *digits = p;
|
||||
const char *point = nullptr;
|
||||
uint32_t mantissa = 0;
|
||||
for (;; p++) {
|
||||
const uint32_t d = static_cast<uint8_t>(*p) - '0';
|
||||
if (d > 9) {
|
||||
if (*p == '.' && point == nullptr) {
|
||||
point = p;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mantissa > PLAIN_MANTISSA_LIMIT)
|
||||
return nullptr;
|
||||
mantissa = mantissa * 10 + d;
|
||||
}
|
||||
const uint32_t fraction = point != nullptr ? p - point - 1 : 0;
|
||||
const char c = static_cast<char>(*p | 0x20);
|
||||
if (p - digits == (point != nullptr ? 1 : 0) || fraction > 9 || c == 'e' || c == 'x')
|
||||
return nullptr;
|
||||
float value = static_cast<float>(mantissa);
|
||||
if (fraction != 0)
|
||||
value /= static_cast<float>(pow10_exact<uint32_t>(fraction)); // 10^9 is an exact float
|
||||
*bits = bit_cast<uint32_t>(value);
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
optional<float> parse_float(const char *str) {
|
||||
// Own copy of the plain-decimal loop: sharing parse_plain_decimal costs 12 to 18% more instructions
|
||||
const char *p = str;
|
||||
const bool negative = *p == '-';
|
||||
if (negative || *p == '+')
|
||||
p++;
|
||||
const char *digits = p;
|
||||
const char *point = nullptr;
|
||||
uint32_t mantissa = 0;
|
||||
for (;; p++) {
|
||||
const uint32_t d = static_cast<uint8_t>(*p) - '0';
|
||||
if (d > 9) {
|
||||
if (*p == '.' && point == nullptr) {
|
||||
point = p;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mantissa > PLAIN_MANTISSA_LIMIT)
|
||||
break;
|
||||
mantissa = mantissa * 10 + d;
|
||||
}
|
||||
const uint32_t fraction = point != nullptr ? p - point - 1 : 0;
|
||||
if (*p == '\0' && p - digits > (point != nullptr ? 1 : 0) && fraction <= 9) {
|
||||
float value = negative ? -static_cast<float>(mantissa) : static_cast<float>(mantissa);
|
||||
if (fraction != 0)
|
||||
value /= static_cast<float>(pow10_exact<uint32_t>(fraction));
|
||||
return value;
|
||||
}
|
||||
const char *end;
|
||||
bool full_negative;
|
||||
const uint32_t bits = parse_float_full(str, &end, &full_negative);
|
||||
// HUGE_VALF, positive overflow or "inf", is rejected like before
|
||||
if (end == str || *end != '\0' || (!full_negative && bits == FLOAT_INF_BITS))
|
||||
return {};
|
||||
return bit_cast<float>(full_negative ? bits | FLOAT_SIGN_BIT : bits);
|
||||
}
|
||||
|
||||
float str_to_float(const char *str, char **end) {
|
||||
const char *stop;
|
||||
bool negative;
|
||||
uint32_t bits;
|
||||
if (const char *plain_end = parse_plain_decimal(str, &bits, &negative)) {
|
||||
stop = plain_end;
|
||||
} else {
|
||||
bits = parse_float_full(str, &stop, &negative);
|
||||
}
|
||||
if (end != nullptr)
|
||||
*end = const_cast<char *>(stop); // strtof's signature
|
||||
return bit_cast<float>(negative ? bits | FLOAT_SIGN_BIT : bits);
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// The whole string as str_to_float's syntax (leading space, hex, inf, nan); trailing characters, "inf" and positive
|
||||
/// overflow are rejected, while -inf, negative overflow and NaN are accepted.
|
||||
optional<float> parse_float(const char *str);
|
||||
|
||||
/// strtof with picolibc's results on every platform, but it never sets errno (overflow gives +-HUGE_VALF, underflow 0);
|
||||
/// use it instead of strtof, or strtod/atof for a float result.
|
||||
float str_to_float(const char *str, char **end);
|
||||
|
||||
} // namespace esphome
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/parse_float.h"
|
||||
|
||||
#ifdef USE_JSON
|
||||
#include "esphome/components/json/json_util.h"
|
||||
@@ -294,7 +293,7 @@ inline bool operator!=(const std::string &lhs, const ProgmemStringRef &rhs) { re
|
||||
#endif
|
||||
|
||||
// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef)
|
||||
// Must be in esphome namespace for ADL to find them. Uses strtol, strtod and str_to_float to avoid heap allocation.
|
||||
// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation.
|
||||
namespace internal {
|
||||
// NOLINTBEGIN(google-runtime-int)
|
||||
template<typename R, typename F> inline R parse_number(const StringRef &str, size_t *pos, F conv) {
|
||||
@@ -335,7 +334,7 @@ inline long stol(const StringRef &str, size_t *pos = nullptr, int base = 10) {
|
||||
return internal::parse_number<long>(str, pos, base, std::strtol);
|
||||
}
|
||||
inline float stof(const StringRef &str, size_t *pos = nullptr) {
|
||||
return internal::parse_number<float>(str, pos, str_to_float);
|
||||
return internal::parse_number<float>(str, pos, std::strtof);
|
||||
}
|
||||
inline double stod(const StringRef &str, size_t *pos = nullptr) {
|
||||
return internal::parse_number<double>(str, pos, std::strtod);
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
static const char *const PLAIN[] = {"0.5", "255", "-12.75", "21.3"};
|
||||
static const char *const EXPONENT[] = {"1e3", "2.5e-2", "-6.02E5", "1.5e+1"};
|
||||
static const char *const PRECISE[] = {"3.14159265358979", "0.000123456789", "123456789.125", "-0.1000000000000000055"};
|
||||
// Home Assistant states that are not numbers
|
||||
static const char *const REJECT[] = {"unavailable", "unknown", "on", "off"};
|
||||
|
||||
template<size_t N, typename F> static void run(benchmark::State &state, const char *const (&inputs)[N], F parse) {
|
||||
for (auto _ : state) {
|
||||
float sum = 0.0f;
|
||||
for (int i = 0; i < kInnerIterations; i++)
|
||||
sum += parse(inputs[i % N]);
|
||||
benchmark::DoNotOptimize(sum);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
|
||||
static float parse_number_float(const char *str) { return parse_number<float>(str).value_or(0.0f); }
|
||||
|
||||
static void ParseNumberFloat_Plain(benchmark::State &state) { run(state, PLAIN, parse_number_float); }
|
||||
BENCHMARK(ParseNumberFloat_Plain);
|
||||
|
||||
static void ParseNumberFloat_Exponent(benchmark::State &state) { run(state, EXPONENT, parse_number_float); }
|
||||
BENCHMARK(ParseNumberFloat_Exponent);
|
||||
|
||||
static void ParseNumberFloat_Precise(benchmark::State &state) { run(state, PRECISE, parse_number_float); }
|
||||
BENCHMARK(ParseNumberFloat_Precise);
|
||||
|
||||
static void ParseNumberFloat_Reject(benchmark::State &state) { run(state, REJECT, parse_number_float); }
|
||||
BENCHMARK(ParseNumberFloat_Reject);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1,43 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome::climate_ir::testing {
|
||||
|
||||
static constexpr IRCode FANS[] PROGMEM = {
|
||||
{climate::CLIMATE_FAN_AUTO, 0x10},
|
||||
{climate::CLIMATE_FAN_LOW, 0x20},
|
||||
{climate::CLIMATE_FAN_HIGH, 0x30},
|
||||
};
|
||||
|
||||
TEST(ClimateIRCodeTest, FindByValue) {
|
||||
const auto entry = find_ir_code(FANS, climate::CLIMATE_FAN_LOW, false);
|
||||
ASSERT_TRUE(entry.has_value());
|
||||
EXPECT_EQ(entry->value, climate::CLIMATE_FAN_LOW);
|
||||
EXPECT_EQ(entry->code, 0x20);
|
||||
}
|
||||
|
||||
TEST(ClimateIRCodeTest, FindByCode) {
|
||||
const auto entry = find_ir_code(FANS, 0x30, true);
|
||||
ASSERT_TRUE(entry.has_value());
|
||||
EXPECT_EQ(entry->value, climate::CLIMATE_FAN_HIGH);
|
||||
}
|
||||
|
||||
TEST(ClimateIRCodeTest, FindMissing) {
|
||||
EXPECT_FALSE(find_ir_code(FANS, climate::CLIMATE_FAN_MEDIUM, false).has_value());
|
||||
// A code that is also a listed value must not match when looking up codes
|
||||
EXPECT_FALSE(find_ir_code(FANS, climate::CLIMATE_FAN_LOW, true).has_value());
|
||||
}
|
||||
|
||||
TEST(ClimateIRCodeTest, ToCode) {
|
||||
EXPECT_EQ(to_ir_code(FANS, climate::CLIMATE_FAN_HIGH, 0x00), 0x30);
|
||||
EXPECT_EQ(to_ir_code(FANS, climate::CLIMATE_FAN_MEDIUM, 0x00), 0x00);
|
||||
}
|
||||
|
||||
TEST(ClimateIRCodeTest, FromCode) {
|
||||
EXPECT_EQ(from_ir_code(FANS, 0x20, climate::CLIMATE_FAN_AUTO), climate::CLIMATE_FAN_LOW);
|
||||
EXPECT_EQ(from_ir_code(FANS, 0x99, climate::CLIMATE_FAN_AUTO), climate::CLIMATE_FAN_AUTO);
|
||||
// The current state as the fallback leaves it unchanged on an unknown code
|
||||
EXPECT_EQ(from_ir_code(FANS, 0x99, climate::CLIMATE_FAN_MEDIUM), climate::CLIMATE_FAN_MEDIUM);
|
||||
}
|
||||
|
||||
} // namespace esphome::climate_ir::testing
|
||||
@@ -1,74 +1,34 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/parse_float.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
namespace esphome::core::testing {
|
||||
|
||||
static uint32_t bits(float value) { return bit_cast<uint32_t>(value); }
|
||||
|
||||
// picolibc rounds a decimal to 9 significant digits and converts those to the nearest float, so up to 9
|
||||
// significant digits its result is the correctly rounded float the host's strtof (glibc, macOS) gives. Longer
|
||||
// input and the syntax where libcs differ are checked against values taken from picolibc 1.8.10's own strtof.
|
||||
static float reference_strtof(const char *str, char **end) {
|
||||
const float value = ::strtof(str, end);
|
||||
if (std::isnan(value))
|
||||
return std::signbit(value) ? -NAN : NAN;
|
||||
return value;
|
||||
static uint32_t bits(float value) {
|
||||
uint32_t out;
|
||||
memcpy(&out, &value, sizeof(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Every accepted input must give the reference's bits.
|
||||
// strtof is the reference: every accepted input must give the same bits.
|
||||
static void expect_matches_strtof(const char *str) {
|
||||
const float expected = reference_strtof(str, nullptr);
|
||||
char *end = nullptr;
|
||||
const float expected = ::strtof(str, &end);
|
||||
const auto parsed = parse_number<float>(str);
|
||||
if (!parsed.has_value()) {
|
||||
ADD_FAILURE() << str << " was rejected";
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(bits(parsed.value()), bits(expected)) << str;
|
||||
const float actual = parsed.value();
|
||||
EXPECT_EQ(bits(actual), bits(expected)) << str << " parsed " << actual << " expected " << expected;
|
||||
}
|
||||
|
||||
// The same bits and end as the host's strtof; for decimals of up to 9 significant digits
|
||||
static void expect_str_to_float_matches(const char *str) {
|
||||
char *expected_end = nullptr;
|
||||
const float expected = reference_strtof(str, &expected_end);
|
||||
char *end = nullptr;
|
||||
const float actual = str_to_float(str, &end);
|
||||
EXPECT_EQ(bits(actual), bits(expected)) << '"' << str << '"';
|
||||
EXPECT_EQ(end - str, expected_end - str) << '"' << str << '"';
|
||||
}
|
||||
|
||||
struct PicolibcCase {
|
||||
const char *str;
|
||||
uint32_t bits;
|
||||
int32_t end;
|
||||
};
|
||||
|
||||
// Bits and end from picolibc 1.8.10's strtof
|
||||
static void expect_picolibc(const PicolibcCase &c) {
|
||||
char *end = nullptr;
|
||||
const float value = str_to_float(c.str, &end);
|
||||
EXPECT_EQ(bits(value), c.bits) << '"' << c.str << '"';
|
||||
EXPECT_EQ(end - c.str, c.end) << '"' << c.str << '"';
|
||||
}
|
||||
|
||||
static void expect_picolibc(const std::string &str, uint32_t expected) {
|
||||
char *end = nullptr;
|
||||
EXPECT_EQ(bits(str_to_float(str.c_str(), &end)), expected) << str.substr(0, 40);
|
||||
EXPECT_EQ(static_cast<size_t>(end - str.c_str()), str.size()) << str.substr(0, 40);
|
||||
}
|
||||
|
||||
static float float_from(uint32_t value) { return bit_cast<float>(value); }
|
||||
|
||||
TEST(ParseFloat, PlainDecimalsMatchStrtof) {
|
||||
for (const char *str :
|
||||
{"0", "-0", "+0", "1", "-1", "100.50", "99.75", "0.1", "0.3",
|
||||
@@ -86,14 +46,6 @@ TEST(ParseFloat, SweepOfTwoDecimalValuesMatchesStrtof) {
|
||||
}
|
||||
}
|
||||
|
||||
// Eight-digit mantissas below 2^24 stay on the one-division path; the next digit past the limit falls back
|
||||
TEST(ParseFloat, EightDigitPlainDecimalsMatchStrtof) {
|
||||
for (const char *str : {"12345678", "-12345678", "1234.5678", "0.12345678", "16777209", "1677720.9", "0.16777209",
|
||||
"16777210", "16777215", "1677721.5", "99999999", "9999999.9", "16777209."}) {
|
||||
expect_matches_strtof(str);
|
||||
}
|
||||
}
|
||||
|
||||
// A fraction longer than the fast path accepts must fall back, however long it is.
|
||||
TEST(ParseFloat, LongFractionsMatchStrtof) {
|
||||
std::string str = "0.";
|
||||
@@ -116,293 +68,10 @@ TEST(ParseFloat, LibcPathInputsMatchStrtof) {
|
||||
// Positive overflow and a positive literal infinity both come back as HUGE_VALF from strtof, and the parser
|
||||
// rejects both; the negative forms pass through, as they did before the fast path.
|
||||
TEST(ParseFloat, RejectsInvalidInput) {
|
||||
for (const char *str : {"", "-", "+", ".", "abc", "12abc", "1.2.3", "1,5", "12 ", "--1", "1e", "1e400", "inf",
|
||||
"infinity", "infi", "nan(12 ab)", "nan(12"}) {
|
||||
for (const char *str :
|
||||
{"", "-", "+", ".", "abc", "12abc", "1.2.3", "1,5", "12 ", "--1", "1e", "1e400", "inf", "infinity"}) {
|
||||
EXPECT_FALSE(parse_number<float>(str).has_value()) << str;
|
||||
}
|
||||
}
|
||||
|
||||
// Above the midpoint 1 + 2^-24 by less than half a double: newlib's strtof rounds to the midpoint and then to
|
||||
// 1.0f; picolibc's 9 digits, 1.00000006, give the next float.
|
||||
TEST(ParseFloat, FullPathRoundsLikePicolibc) {
|
||||
EXPECT_EQ(bits(parse_number<float>("1.0000000596046448").value()), 0x3F800001u);
|
||||
EXPECT_EQ(bits(parse_number<float>("-1.0000000596046448").value()), 0xBF800001u);
|
||||
}
|
||||
|
||||
// picolibc takes "nan(" with letters, digits and '_' up to a ")", and nothing at all otherwise
|
||||
TEST(ParseFloat, NanForms) {
|
||||
EXPECT_TRUE(std::isnan(parse_number<float>("nan(12)").value()));
|
||||
EXPECT_TRUE(std::isnan(parse_number<float>("nan(xyz_1)").value()));
|
||||
EXPECT_FALSE(parse_number<float>("nan(x y)").has_value());
|
||||
}
|
||||
|
||||
// The callers' overflow check: a positive overflow is HUGE_VALF and rejected, a negative one is -inf and accepted
|
||||
TEST(ParseFloat, OverflowIsHugeValf) {
|
||||
EXPECT_FALSE(parse_number<float>("1e39").has_value());
|
||||
EXPECT_FALSE(parse_number<float>("3.40282357e38").has_value());
|
||||
EXPECT_FALSE(parse_number<float>("1e99999").has_value());
|
||||
EXPECT_EQ(bits(parse_number<float>("-1e39").value()), 0xFF800000u);
|
||||
EXPECT_EQ(bits(parse_number<float>("3.4028235e38").value()), 0x7F7FFFFFu);
|
||||
}
|
||||
|
||||
// Up to 9 significant digits, with leading and trailing zeros, a point anywhere and an exponent
|
||||
TEST(StrToFloat, RandomDecimalsMatchStrtof) {
|
||||
std::mt19937_64 rng(1); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) reproducible
|
||||
for (int i = 0; i < 50000; i++) {
|
||||
std::string str;
|
||||
if (rng() % 3 == 0)
|
||||
str += (rng() & 1) ? "-" : "+";
|
||||
const int zeros = static_cast<int>(rng() % 4);
|
||||
const int significant = 1 + static_cast<int>(rng() % 9);
|
||||
const int trailing = static_cast<int>(rng() % 4);
|
||||
std::string digits(zeros, '0');
|
||||
digits += static_cast<char>('1' + rng() % 9);
|
||||
for (int j = 1; j < significant; j++)
|
||||
digits += static_cast<char>('0' + rng() % 10);
|
||||
digits.append(trailing, '0');
|
||||
const int point = static_cast<int>(rng() % (digits.size() + 2)) - 1;
|
||||
for (int j = 0; j < static_cast<int>(digits.size()); j++) {
|
||||
if (j == point)
|
||||
str += '.';
|
||||
str += digits[j];
|
||||
}
|
||||
if (rng() & 1) {
|
||||
str += (rng() & 1) ? 'e' : 'E';
|
||||
char exp[8];
|
||||
snprintf(exp, sizeof(exp), "%d", static_cast<int>(rng() % 110) - 60);
|
||||
str += exp;
|
||||
}
|
||||
expect_str_to_float_matches(str.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Every 16411th float, printed with 6 and 9 digits
|
||||
TEST(StrToFloat, FloatsMatchStrtof) {
|
||||
char buf[64];
|
||||
for (uint32_t u = 0; u < 0x7F800000; u += 16411) {
|
||||
for (int precision : {6, 9}) {
|
||||
snprintf(buf, sizeof(buf), "%.*e", precision - 1, static_cast<double>(float_from(u)));
|
||||
expect_str_to_float_matches(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StrToFloat, SyntaxMatchesStrtof) {
|
||||
// Tokens whose combinations the libcs read alike: no "x", "nan(" or "infi"
|
||||
static const char *const TOKENS[] = {"0", "1", "9", ".", "e", "E", "+", "-", "a", "N", " ", "\t", "y", "00", "#", ""};
|
||||
for (const char *a : TOKENS) {
|
||||
for (const char *b : TOKENS) {
|
||||
for (const char *c : TOKENS) {
|
||||
for (const char *d : TOKENS) {
|
||||
const std::string str = std::string(a) + b + c + d;
|
||||
expect_str_to_float_matches(str.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Where newlib and glibc read the text differently from picolibc
|
||||
TEST(StrToFloat, SyntaxMatchesPicolibc) {
|
||||
static const PicolibcCase CASES[] = {
|
||||
// "x" after any zero digits starts hex, even after a point or a second "0x"
|
||||
{"000x", 0x00000000, 4},
|
||||
{"0x", 0x00000000, 2},
|
||||
{"-0x", 0x80000000, 3},
|
||||
{"00x1", 0x3F800000, 4},
|
||||
{"0x0x1", 0x3F800000, 5},
|
||||
{"0.0x1", 0x3B800000, 5},
|
||||
{".x1", 0x00000000, 0},
|
||||
{"0x.p1", 0x00000000, 5},
|
||||
{"0x.8", 0x3F000000, 4},
|
||||
{"0X1.8", 0x3FC00000, 5},
|
||||
{"0x1P-1", 0x3F000000, 6},
|
||||
{"0x1p+", 0x3F800000, 3},
|
||||
{"1x5", 0x3F800000, 1},
|
||||
// "inf" stops before anything but "i"; past "infi" only "infinity" is a number
|
||||
{"inf", 0x7F800000, 3},
|
||||
{"-inf", 0xFF800000, 4},
|
||||
{"INFINITY", 0x7F800000, 8},
|
||||
{"+infi0", 0x00000000, 0},
|
||||
{"infi", 0x00000000, 0},
|
||||
{"infinit", 0x00000000, 0},
|
||||
// "nan(" takes letters, digits and '_' up to ")", and is no number without the ")"
|
||||
{"NaN", 0x7FC00000, 3},
|
||||
{"nan()", 0x7FC00000, 5},
|
||||
{"nan(xyz)", 0x7FC00000, 8},
|
||||
{"nan(_a1)", 0x7FC00000, 8},
|
||||
{"-nan(1)", 0xFFC00000, 7},
|
||||
{"nan( )", 0x00000000, 0},
|
||||
{"nan(12 ab)", 0x00000000, 0},
|
||||
{"nan(12", 0x00000000, 0},
|
||||
// An exponent needs a digit
|
||||
{"1e", 0x3F800000, 1},
|
||||
{"1e+", 0x3F800000, 1},
|
||||
{"1e-x", 0x3F800000, 1},
|
||||
{"\t 12", 0x41400000, 4},
|
||||
{"- 1", 0x00000000, 0},
|
||||
{"1.2.3", 0x3F99999A, 3},
|
||||
};
|
||||
for (const PicolibcCase &c : CASES)
|
||||
expect_picolibc(c);
|
||||
}
|
||||
|
||||
// The inputs where newlib's strtof (decimal to double to float) and picolibc's were found to differ; every platform
|
||||
// now gives picolibc's result, which is also the correctly rounded float for these
|
||||
TEST(StrToFloat, RoundsLikePicolibcWhereNewlibDoubleRounds) {
|
||||
expect_picolibc({"1.0000000596046448", 0x3F800001, 18});
|
||||
expect_picolibc({"0x1.100b0d0000000c", 0x3F880587, 18});
|
||||
expect_picolibc("1.000000059604644775390625" + std::string(800, '0') + "1", 0x3F800001);
|
||||
}
|
||||
|
||||
// picolibc rounds a decimal to 9 significant digits, ties to even, before converting it: past 9 digits its float can
|
||||
// be one away from the nearest, and an exact tie between two floats can round up. Values from picolibc.
|
||||
TEST(StrToFloat, NineDigitRoundingLikePicolibc) {
|
||||
static const PicolibcCase CASES[] = {
|
||||
{"1.0856183295439e10", 0x5021C50E, 18}, // nearest 0x5021C50D
|
||||
{"3.97496754550e-4", 0x39D0671D, 16}, // nearest 0x39D0671C
|
||||
{"9.76178673657108930e23", 0x674EB6C1, 22}, // nearest 0x674EB6C0
|
||||
{"156.6562236e-26", 0x17F269ED, 15}, // nearest 0x17F269EC
|
||||
{"1.000000059604644775390625", 0x3F800001, 26}, // the tie 1 + 2^-24: nearest-even 0x3F800000
|
||||
{"1.000000059", 0x3F800001, 11}, // 1.00000006 after rounding
|
||||
{"7.0064923216240854e-46", 0x00000000, 22}, // 7.00649232e-46, below half the smallest float
|
||||
{"21.299999999999997", 0x41AA6666, 18},
|
||||
{"0.30000000000000004", 0x3E99999A, 19},
|
||||
{"0.1000000000000000055511151231257827", 0x3DCCCCCD, 36},
|
||||
};
|
||||
for (const PicolibcCase &c : CASES)
|
||||
expect_picolibc(c);
|
||||
}
|
||||
|
||||
// Nine digits converted exactly: ties to even between floats, where the 64-bit product cannot decide alone
|
||||
TEST(StrToFloat, NineDigitTiesGoToEven) {
|
||||
static const PicolibcCase CASES[] = {
|
||||
{"16777217", 0x4B800000, 8}, {"16777219", 0x4B800002, 8}, {"16777217.0", 0x4B800000, 10},
|
||||
{"8388608.5", 0x4B000000, 9}, {"8388609.5", 0x4B000002, 9}, {"3e10", 0x50DF8476, 4},
|
||||
{"5e10", 0x513A43B7, 4}, {"1.00000006", 0x3F800001, 10},
|
||||
};
|
||||
for (const PicolibcCase &c : CASES)
|
||||
expect_picolibc(c);
|
||||
}
|
||||
|
||||
// RP2350 flushes float denormals in its double to float conversion; #22677 bench. The parser builds the bits
|
||||
// with integer operations, so every denormal parses to its own value.
|
||||
TEST(StrToFloat, DenormalsParseWithoutFlushing) {
|
||||
char buf[64];
|
||||
const auto check = [&buf](uint32_t k) {
|
||||
snprintf(buf, sizeof(buf), "%.8e", std::ldexp(static_cast<double>(k), -149));
|
||||
EXPECT_EQ(bits(str_to_float(buf, nullptr)), k) << buf;
|
||||
};
|
||||
for (uint32_t k = 0; k <= 0x800000; k += 4099)
|
||||
check(k);
|
||||
// The largest denormal and FLT_MIN, where rounding carries into the exponent
|
||||
for (uint32_t k : {1u, 2u, 0x7FFFFEu, 0x7FFFFFu, 0x800000u})
|
||||
check(k);
|
||||
}
|
||||
|
||||
// Pico W and Pico 2 W bench, #22677: the RP2040 and RP2350 libcs flush float denormals to 0 in their double to
|
||||
// float conversion, and the RP2040 one turns a NaN into +infinity
|
||||
TEST(StrToFloat, Rp2040LibcFlushesAndNanToInfinity) {
|
||||
static const PicolibcCase CASES[] = {
|
||||
{"1e-40", 0x000116C2, 5}, {"1.4e-45", 0x00000001, 7}, {"1e-38", 0x006CE3EE, 5},
|
||||
{"1.1754942e-38", 0x007FFFFF, 13}, {"0x1p-149", 0x00000001, 8}, {"1.17549435e-38", 0x00800000, 14},
|
||||
{"nan(12)", 0x7FC00000, 7}, {"nan", 0x7FC00000, 3},
|
||||
};
|
||||
for (const PicolibcCase &c : CASES)
|
||||
expect_picolibc(c);
|
||||
}
|
||||
|
||||
// RP2350 bench: libc returned 0 and the first parser stalled stepping up from a flushed guess; #22677 bench
|
||||
TEST(StrToFloat, DenormalsAndFltMinMatchStrtof) {
|
||||
for (const char *str : {"1.4e-45", "-1.4e-45", "1e-40", "1e-38", "1.17549435e-38", "1.1754942e-38", "1.17549421e-38",
|
||||
"1.1754945e-38", "7.0064923e-46", "7.0064924e-46"}) {
|
||||
expect_str_to_float_matches(str);
|
||||
}
|
||||
}
|
||||
|
||||
// Home Assistant's non-numeric states; CodSpeed HomeassistantSensor_StateResponse_Unavailable on #22677
|
||||
TEST(StrToFloat, NonNumericStatesAreRejected) {
|
||||
for (const char *str : {"unavailable", "unknown", "on", "off", "", "-", ".", "none", "inactive"}) {
|
||||
expect_str_to_float_matches(str);
|
||||
EXPECT_FALSE(parse_number<float>(str).has_value()) << str;
|
||||
}
|
||||
}
|
||||
|
||||
// The one float operation path takes up to 2^24 - 1 and 10^10; #22677 review
|
||||
TEST(StrToFloat, PlainPathBoundaries) {
|
||||
for (const char *top : {"16777215", "16777216", "16777217"}) {
|
||||
for (const char *exp : {"e10", "e-10", "e11", "e-11", "e9", "e-9", ".5", ".25"}) {
|
||||
const std::string str = std::string(top) + exp;
|
||||
expect_str_to_float_matches(str.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The midpoint between FLT_MAX and 2^128 rounds to infinity; review
|
||||
TEST(StrToFloat, OverflowMidpoint) {
|
||||
expect_picolibc({"340282356779733661637539395458142568448", 0x7F800000, 39});
|
||||
expect_picolibc({"3.40282356e38", 0x7F7FFFFF, 13});
|
||||
expect_picolibc({"3.40282357e38", 0x7F800000, 13});
|
||||
EXPECT_EQ(bits(str_to_float("0x1.ffffffp127", nullptr)), 0x7F800000u);
|
||||
}
|
||||
|
||||
// picolibc stops adding exponent digits at 224, so the digits of the mantissa can move it no further
|
||||
TEST(StrToFloat, HugeExponents) {
|
||||
for (const char *str : {"1e-99999", "1e99999", "-1e99999", "0e99999", "1e9999999999999999999",
|
||||
"1e-9999999999999999999", "1e-999999999999", "0.000e99999", "1e224", "1e2240", "1e-2240"}) {
|
||||
expect_str_to_float_matches(str);
|
||||
}
|
||||
// 10^-3000 * 10^3000 is 1, but the exponent counts only as 300
|
||||
expect_picolibc("0." + std::string(2999, '0') + "1e3000", 0x00000000);
|
||||
expect_picolibc("1" + std::string(300, '0') + "e-300", 0x3F800000);
|
||||
}
|
||||
|
||||
TEST(StrToFloat, LongMantissas) {
|
||||
expect_picolibc("1" + std::string(799, '0') + "e-799", 0x3F800000);
|
||||
expect_picolibc("1." + std::string(3000, '0') + "1e-3001", 0x00000000);
|
||||
expect_picolibc("0." + std::string(400, '0') + "34028235" + std::string(500, '7') + "e440", 0x7F800000);
|
||||
expect_picolibc(std::string(300, '9') + "e-261", 0x7F800000);
|
||||
expect_picolibc("3" + std::string(799, '7') + "e-761", 0x7F800000);
|
||||
}
|
||||
|
||||
// picolibc takes ':' to '?' as hex digits 10 to 15 ("0x1:" is 26); these stop at them, as newlib does. #22677 review
|
||||
TEST(StrToFloat, HexStopsAtPunctuationAfterNine) {
|
||||
struct Case {
|
||||
const char *str;
|
||||
uint32_t bits;
|
||||
int32_t end;
|
||||
};
|
||||
const Case cases[] = {
|
||||
{"0x1:", 0x3F800000, 3}, {"0x1;", 0x3F800000, 3}, {"0x1<", 0x3F800000, 3}, {"0x1=", 0x3F800000, 3},
|
||||
{"0x1>", 0x3F800000, 3}, {"0x1?", 0x3F800000, 3}, {"0x1.:", 0x3F800000, 4}, {"0x:", 0x00000000, 2},
|
||||
{"0x9?", 0x41100000, 3}, {"0xA:", 0x41200000, 3},
|
||||
};
|
||||
for (const auto &c : cases) {
|
||||
char *end = nullptr;
|
||||
const float value = str_to_float(c.str, &end);
|
||||
EXPECT_EQ(bits(value), c.bits) << '"' << c.str << '"';
|
||||
EXPECT_EQ(end - c.str, c.end) << '"' << c.str << '"';
|
||||
EXPECT_FALSE(parse_number<float>(c.str).has_value()) << c.str;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(StrToFloat, HexDenormalsAndOverflow) {
|
||||
struct Case {
|
||||
const char *str;
|
||||
uint32_t bits;
|
||||
};
|
||||
static const Case CASES[] = {
|
||||
{"0x1p-149", 0x00000001}, {"0x1p-150", 0x00000000},
|
||||
{"0x1.8p-149", 0x00000002}, {"0x1.fffffcp-127", 0x007FFFFF},
|
||||
{"0x1.fffffep-127", 0x00800000}, {"-0x1p-149", 0x80000001},
|
||||
{"0x1p128", 0x7F800000}, {"0x1.fffffep127", 0x7F7FFFFF},
|
||||
{"0x1.fffffefp127", 0x7F7FFFFF}, {"0x1.0000000000001p-150", 0x00000001},
|
||||
{"0x1.000001p0", 0x3F800000}, {"0x1.000003p0", 0x3F800002},
|
||||
{"0x1p-99999999999", 0x00000000}, {"0x1p99999999999", 0x7F800000},
|
||||
};
|
||||
for (const Case &c : CASES)
|
||||
EXPECT_EQ(bits(str_to_float(c.str, nullptr)), c.bits) << c.str;
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
#endif // USE_HOST
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]))
|
||||
+10
@@ -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
|
||||
+33
@@ -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
|
||||
@@ -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
|
||||
@@ -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"),
|
||||
]
|
||||
Reference in new issue
Block a user