Compare commits

..
31 changed files with 292 additions and 646 deletions

No files matched your search

+1 -1
View File
@@ -741,7 +741,7 @@ esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic
esphome/components/weikai_i2c/* @DrCoolZic
esphome/components/weikai_spi/* @DrCoolZic
esphome/components/whirlpool/* @arpena @glmnet @Rodrigoah
esphome/components/whirlpool/* @arpena @glmnet
esphome/components/whynter/* @aeonsablaze
esphome/components/wiegand/* @ssieb
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
+42 -21
View File
@@ -1,6 +1,7 @@
from collections.abc import Awaitable, Callable, Sequence
from contextvars import ContextVar
from dataclasses import dataclass, field
from functools import partial
import logging
import math
import re
@@ -1151,9 +1152,6 @@ IfAction = cg.esphome_ns.class_("IfAction", Action)
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
WaitUntilAction = cg.esphome_ns.class_("WaitUntilAction", Action, cg.Component)
UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action)
ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation")
TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder")
TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder")
@@ -1771,25 +1769,44 @@ async def lambda_action_to_code(
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
register_simple_action(
"component.update",
UpdateComponentAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
_POLLING_COMPONENT_SCHEMA = maybe_simple_id(
{cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}
)
register_simple_action(
"component.suspend",
SuspendComponentAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
)
async def _polling_component_action(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
calls: list[str],
) -> MockObj:
"""Run ``calls`` on the component once it is ready, through ``PollingComponent``.
An if block, not an early return, so actions merged after this one still run.
"""
parent = await _apply_parent(config)
statements = [
f"{cg.PollingComponent} &esphome__component = *{parent};",
"if (esphome__component.is_ready()) {",
*(f" esphome__component.{call};" for call in calls),
"}",
]
return apply_statements_action(action_id, template_arg, args, statements)
for _name, _call in (
("component.update", "update()"),
("component.suspend", "stop_poller()"),
):
register_action(_name, ApplyAction, _POLLING_COMPONENT_SCHEMA, synchronous=True)(
partial(_polling_component_action, calls=[_call])
)
@register_action(
"component.resume",
ResumeComponentAction,
ApplyAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
@@ -1806,12 +1823,16 @@ async def component_resume_action_to_code(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, comp)
if CONF_UPDATE_INTERVAL in config:
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
cg.add(var.set_update_interval(template_))
return var
calls: list[str] = []
if (interval := config.get(CONF_UPDATE_INTERVAL)) is not None:
if isinstance(interval, Lambda):
# Built with the trigger's own argument types and always called, so the body
# may modify its by-value arguments
lamb = await cg.process_lambda(interval, args, return_type=cg.uint32)
interval = CallExpression(lamb, *(MockObj(name) for _, name in args))
calls.append(f"set_update_interval({cg.safe_exp(interval)})")
calls.append("start_poller()")
return await _polling_component_action(config, action_id, template_arg, args, calls)
async def build_action(
@@ -95,19 +95,4 @@ 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,42 +6,9 @@
#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,8 +7,6 @@
#include "esphome/core/automation.h"
#include "esphome/components/network/ip_address.h"
#include <atomic>
#ifdef USE_ETHERNET
#ifdef USE_ESP32
@@ -368,9 +366,6 @@ 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,14 +129,6 @@ 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_) {
@@ -778,8 +770,7 @@ 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->ip_state_pending_.store(1, std::memory_order_release);
global_eth_component->enable_loop_soon_any_context();
global_eth_component->notify_ip_state_listeners_();
#endif
#if USE_NETWORK_IPV6
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
@@ -807,9 +798,6 @@ 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
@@ -817,6 +805,9 @@ 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
@@ -828,9 +819,6 @@ 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);
@@ -839,6 +827,9 @@ 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 */
+25 -12
View File
@@ -50,25 +50,34 @@ 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};
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 = {
struct Code {
uint8_t value; // a climate enum, all of which are uint8_t
uint8_t code;
};
static constexpr Code 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 IRCode FANS[] PROGMEM = {
static constexpr Code 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; }
@@ -142,11 +151,14 @@ 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] = 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);
frame[7] = mode.has_value() ? mode->code : BYTE7_MODE_AUTO;
frame[8] = fan.has_value() ? fan->code : BYTE8_FAN_AUTO;
if (this->swing_mode != climate::CLIMATE_SWING_OFF)
frame[8] |= BYTE8_FAN_SWING;
frame[STATE_MESSAGE_LENGTH - 1] = checksum_state(frame);
@@ -197,7 +209,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_ir_code(MODES, d[7], true);
const auto mode = find_entry(MODES, d[7], true);
if (!mode.has_value()) {
ESP_LOGV(TAG, "Ignoring unknown mode 0x%02X", d[7]);
return false;
@@ -212,7 +224,8 @@ 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;
this->fan_mode = from_ir_code(FANS, d[8] & ~BYTE8_FAN_SWING, climate::CLIMATE_FAN_AUTO);
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->publish_state();
return true;
+11 -7
View File
@@ -21,16 +21,19 @@ constexpr uint8_t MISTRAL_POWER_ON = 0x20;
constexpr uint8_t MISTRAL_TEMP_OFFSET = 0x9F;
constexpr uint8_t MISTRAL_MODE_AUTO = 0x10;
// 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},
// 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 = {
{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;
@@ -75,6 +78,7 @@ 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]));
@@ -84,7 +88,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] = climate_ir::to_ir_code(MODES, this->mode, MISTRAL_MODE_AUTO);
frame[4] = mode.has_value() ? mode->code : MISTRAL_MODE_AUTO;
frame[5] = reverse_bits(static_cast<uint8_t>(MISTRAL_TEMP_OFFSET - temp));
frame[6] = fan.byte6;
frame[11] = compute_checksum(frame);
@@ -112,12 +116,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 = climate_ir::find_ir_code(MODES, frame[4], true);
const auto mode = find_entry(MODES, [frame](const ModeCode &m) { return m.code == frame[4]; });
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 = static_cast<climate::ClimateMode>(mode->value);
this->mode = mode->mode;
this->target_temperature =
clamp<float>(MISTRAL_TEMP_OFFSET - reverse_bits(frame[5]), MISTRAL_TEMP_MIN, MISTRAL_TEMP_MAX);
this->fan_mode = fan->fan;
+33 -15
View File
@@ -47,28 +47,46 @@ static bool is_powered_on(const uint8_t *state) {
return get_field(state, POWER_1) == POWER_ON && get_field(state, POWER_2) == POWER_ON;
}
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 = {
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 = {
{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 IRCode FANS[] PROGMEM = {
static constexpr Code FANS[] PROGMEM = {
{climate::CLIMATE_FAN_AUTO, 0},
{climate::CLIMATE_FAN_LOW, 2},
{climate::CLIMATE_FAN_MEDIUM, 4},
{climate::CLIMATE_FAN_HIGH, 5},
};
static constexpr IRCode SWINGS[] PROGMEM = {
static constexpr Code 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) {
@@ -105,13 +123,13 @@ void SamsungClimate::transmit_state() {
if (on) {
progmem_memcpy(state, RESET_STATE, SAMSUNG_AC_STATE_LENGTH);
set_field(state, MODE, to_ir_code(MODES, this->mode, 0));
set_field(state, MODE, to_code(MODES, this->mode));
} // 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_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, 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, POWER_1, on ? POWER_ON : 0);
set_field(state, POWER_2, on ? POWER_ON : 0);
@@ -194,11 +212,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_ir_code(SWINGS, get_field(state, SWING), climate::CLIMATE_SWING_OFF);
this->swing_mode = from_code<climate::ClimateSwingMode>(SWINGS, get_field(state, SWING));
this->target_temperature = get_field(state, TEMP) + SAMSUNG_AC_TEMP_MIN;
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;
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;
ESP_LOGD(TAG, "Received %u byte frame, power %s", length, ONOFF(is_powered_on(state)));
this->publish_state();
@@ -6,32 +6,70 @@ namespace esphome::siemens_ira211 {
using namespace remote_base;
// 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)},
struct PresetMapping {
climate::ClimatePreset preset;
IRA211Mode mode;
};
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) {
return static_cast<IRA211Mode>(
climate_ir::to_ir_code(PRESETS, preset, static_cast<uint8_t>(IRA211Mode::IRA211_MODE_COMFORT)));
for (const auto &slot : PRESETS) {
const auto entry = load_entry(slot);
if (entry.preset == preset)
return entry.mode;
}
return load_entry(PRESETS[0]).mode;
}
static climate::ClimatePreset to_climate_preset(IRA211Mode mode) {
return climate_ir::from_ir_code(PRESETS, static_cast<uint8_t>(mode), climate::CLIMATE_PRESET_COMFORT);
for (const auto &slot : PRESETS) {
const auto entry = load_entry(slot);
if (entry.mode == mode)
return entry.preset;
}
return load_entry(PRESETS[0]).preset;
}
static IRA211Fan to_ira211_fan(climate::ClimateFanMode fan_mode) {
return static_cast<IRA211Fan>(
climate_ir::to_ir_code(FANS, fan_mode, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_AUTO)));
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;
}
static climate::ClimateFanMode to_climate_fan_mode(IRA211Fan fan) {
return climate_ir::from_ir_code(FANS, static_cast<uint8_t>(fan), climate::CLIMATE_FAN_AUTO);
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;
}
void SiemensIRA211Climate::setup() {
+2 -6
View File
@@ -5,15 +5,13 @@ from esphome.const import CONF_MODEL
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@glmnet", "@arpena", "@Rodrigoah"]
CODEOWNERS = ["@glmnet", "@arpena"]
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", "HITACHI_HCRA31NEWH", prefix="MODEL_"
)
MODELS = Model.options("DG11J1-3A", "DG11J1-91", "HISENSE_ARG", prefix="MODEL_")
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(WhirlpoolClimate).extend(
{
@@ -28,5 +26,3 @@ 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")
+15 -61
View File
@@ -2,8 +2,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::whirlpool {
ESPHOME_LOG_TAG(TAG, "whirlpool.climate");
@@ -46,17 +44,6 @@ 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,
@@ -68,19 +55,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));
put_be16(remote_state, this->remote_id_());
remote_state[0] = 0x83;
remote_state[1] = 0x06;
remote_state[6] = 0x80;
remote_state[18] = 0x08;
remote_state[18] = 0x08; // the same on every supported model
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;
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
const bool power_toggle = 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
@@ -106,10 +93,6 @@ 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;
}
@@ -119,7 +102,6 @@ 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 {
@@ -149,6 +131,7 @@ 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;
@@ -156,11 +139,6 @@ 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]);
@@ -170,7 +148,6 @@ 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);
@@ -203,6 +180,10 @@ 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;
@@ -231,27 +212,6 @@ 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) {
@@ -274,8 +234,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP))
return false;
}
// 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_()) {
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid()) {
// 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;
@@ -302,18 +261,13 @@ 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 (encode_uint16(remote_state[0], remote_state[1]) != this->remote_id_())
if (remote_state[0] != 0x83 || remote_state[1] != 0x06)
return false;
// powr on/off button
ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & 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) {
if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
if (powered_on) {
@@ -355,7 +309,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 = std::min(temp + static_cast<int>(this->temperature_min_()), static_cast<int>(this->temperature_max_()));
temp += static_cast<int>(this->temperature_min_());
ESP_LOGVV(TAG, "Temperature Climate: %u", temp);
this->target_temperature = temp;
}
+4 -19
View File
@@ -6,10 +6,9 @@ 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_HITACHI_HCRA31NEWH = 3, /// Hitachi with the HCRA31NEWH remote, 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
};
// Temperature
@@ -30,9 +29,7 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override {
// 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;
send_swing_cmd_ = call.get_swing_mode().has_value();
climate_ir::ClimateIR::control(call);
}
@@ -58,22 +55,10 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
return false;
#endif
}
/// False when the model is compiled out, so the compiler drops the Hitachi branches.
bool is_hitachi_hcra31newh_() const {
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
return this->model_ == MODEL_HITACHI_HCRA31NEWH;
#else
return false;
#endif
}
/// The remote ID sent in bytes 0 and 1.
uint16_t remote_id_() const { return this->is_hitachi_hcra31newh_() ? 0xA959 : 0x8306; }
/// Modes in which the Hisense ARG model sends a fixed temperature.
bool fixed_temp_mode_() const;
/// Add the Hisense ARG bytes on top of the common whirlpool frame.
void encode_hisense_arg_(uint8_t *remote_state, bool power_toggle, uint8_t temp_step) const;
/// Add the Hitachi HCRA31NEWH bytes on top of the common whirlpool frame.
void encode_hitachi_hcra31newh_(uint8_t *remote_state) const;
/// Set the time of the last transmission.
uint32_t last_transmit_time_{};
+1 -1
View File
@@ -563,7 +563,7 @@ class WiFiComponent final : public Component {
// (In most use cases you won't need these)
/// Setup WiFi interface.
void setup() override;
#if defined(USE_ESP32) || defined(USE_LN882X) || defined(USE_BK72XX)
#if defined(USE_LN882X) || defined(USE_BK72XX)
void on_powerdown() override;
#endif
void start();
@@ -1635,21 +1635,6 @@ bool WiFiComponent::wifi_dpp_start_listen_() {
void WiFiComponent::wifi_dpp_stop_listen_() { esp_supp_dpp_stop_listen(); }
#endif
void WiFiComponent::on_powerdown() {
// Leave the AP before esp_restart() or deep sleep stops Wi-Fi. esp_wifi_stop() while associated
// makes the ESP32 retransmit its Disassociation up to the retry limit although the AP ACKs it
// (32 copies with protected management frames), which some APs handle badly.
// Upstream bug: https://github.com/espressif/esp-idf/issues/19443 - remove this once it is fixed
// in the ESP-IDF versions we support.
// Ask the driver: s_sta_connected lags until the main loop has handled the connect event.
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK)
return;
ESP_LOGD(TAG, "Disconnecting before powerdown");
esp_wifi_disconnect();
delay(100); // NOLINT: once per reboot or deep sleep, lets the disassociation go out
}
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
bssid_t WiFiComponent::wifi_bssid() {
-2
View File
@@ -294,8 +294,6 @@ 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() {
-48
View File
@@ -595,52 +595,4 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
uint8_t stop_generation_{0};
};
template<typename... Ts> class UpdateComponentAction : public Action<Ts...> {
public:
UpdateComponentAction(PollingComponent *component) : component_(component) {}
void play(const Ts &...x) override {
if (!this->component_->is_ready())
return;
this->component_->update();
}
protected:
PollingComponent *component_;
};
template<typename... Ts> class SuspendComponentAction : public Action<Ts...> {
public:
SuspendComponentAction(PollingComponent *component) : component_(component) {}
void play(const Ts &...x) override {
if (!this->component_->is_ready())
return;
this->component_->stop_poller();
}
protected:
PollingComponent *component_;
};
template<typename... Ts> class ResumeComponentAction : public Action<Ts...> {
public:
ResumeComponentAction(PollingComponent *component) : component_(component) {}
TEMPLATABLE_VALUE(uint32_t, update_interval)
void play(const Ts &...x) override {
if (!this->component_->is_ready()) {
return;
}
optional<uint32_t> update_interval = this->update_interval_.optional_value(x...);
if (update_interval.has_value()) {
this->component_->set_update_interval(update_interval.value());
}
this->component_->start_poller();
}
protected:
PollingComponent *component_;
};
} // namespace esphome
+2 -4
View File
@@ -185,12 +185,10 @@ class Component {
*/
virtual bool teardown() { return true; }
/** Called last before a reboot or deep sleep to power down hardware.
/** Called after teardown is complete to power down hardware.
*
* On a safe shutdown this is called after all components have finished their teardown process,
* 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() {}
-1
View File
@@ -387,7 +387,6 @@
#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
@@ -1,43 +0,0 @@
#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
@@ -2,7 +2,3 @@ packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
# IPv6 with an IP sensor: compiles the IPv6 event handler's listener notification
network:
enable_ipv6: true
+1 -2
View File
@@ -10,8 +10,7 @@ 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 and Hitachi models, which climate.py adds only when configured
# Compiles in the Hisense ARG model, which climate.py adds only when it is configured
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
manifest.to_code = to_code_testing
-5
View File
@@ -8,8 +8,3 @@ 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,9 +252,6 @@ 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);
@@ -264,9 +261,6 @@ 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
@@ -391,90 +385,4 @@ 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
@@ -1,23 +0,0 @@
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]))
@@ -1,10 +0,0 @@
#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
@@ -1,33 +0,0 @@
#pragma once
#include "esphome/core/component.h"
namespace esphome::reboot_hook_recorder {
/// Logs every shutdown lifecycle hook it receives, so a test can check their order.
class RebootHookRecorder : public Component {
public:
void set_name(const char *name) { this->name_ = name; }
void on_safe_shutdown() override { this->record_("on_safe_shutdown"); }
void on_shutdown() override { this->record_("on_shutdown"); }
// Needs a second call to finish, like a component waiting for a connection to close
bool teardown() override {
if (++this->teardown_calls_ < 2) {
this->record_("teardown_pending");
return false;
}
if (this->teardown_calls_ == 2)
this->record_("teardown_done");
return true;
}
void on_powerdown() override { this->record_("on_powerdown"); }
protected:
void record_(const char *hook);
const char *name_{""};
uint8_t teardown_calls_{0};
};
} // namespace esphome::reboot_hook_recorder
@@ -1,19 +0,0 @@
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
@@ -1,19 +0,0 @@
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
@@ -1,80 +0,0 @@
"""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"),
]
+93 -17
View File
@@ -2441,16 +2441,25 @@ def polling_component() -> None:
@pytest.mark.asyncio
@pytest.mark.usefixtures("polling_component")
@pytest.mark.parametrize(
"name",
["component.update", "component.suspend", "component.resume"],
("name", "call"),
[
("component.update", "update()"),
("component.suspend", "stop_poller()"),
("component.resume", "start_poller()"),
],
)
async def test_component_action_codegen(name: str) -> None:
"""Each action is an object constructed with the component, with no interval set."""
async def test_component_action_codegen(name: str, call: str) -> None:
"""The call goes through ``PollingComponent`` and only once the component is ready."""
await build_action(_component_action(name), TEMPLATE_ARG, [])
class_name = str(ACTION_REGISTRY[name].type_id)
assert f"new(act) {class_name}<>(comp);" in CORE.cpp_main_section
assert "set_update_interval" not in CORE.cpp_main_section
assert "new(act) ApplyAction<esphome__act__fn>();" in CORE.cpp_main_section
assert (
" PollingComponent &esphome__component = *::comp;\n"
" if (esphome__component.is_ready()) {\n"
f" esphome__component.{call};\n"
" }\n"
) in CORE.cpp_global_section
assert "set_update_interval" not in CORE.cpp_global_section
@pytest.mark.asyncio
@@ -2466,14 +2475,17 @@ async def test_component_resume_constant_interval_codegen() -> None:
)
assert (
"act->set_update_interval([]() -> uint32_t {\n return 250;\n});"
) in CORE.cpp_main_section
" if (esphome__component.is_ready()) {\n"
" esphome__component.set_update_interval(250);\n"
" esphome__component.start_poller();\n"
" }\n"
) in CORE.cpp_global_section
@pytest.mark.asyncio
@pytest.mark.usefixtures("polling_component")
async def test_component_resume_interval_lambda_codegen() -> None:
"""A templated interval is a lambda over the trigger args."""
"""A templated interval is computed from the trigger args, only once ready."""
await build_action(
_component_action(
"component.resume", {CONF_UPDATE_INTERVAL: Lambda("return x * 1000;")}
@@ -2482,27 +2494,91 @@ async def test_component_resume_interval_lambda_codegen() -> None:
[(cg.uint32, "x")],
)
main = CORE.cpp_main_section
assert "new(act) ResumeComponentAction<uint32_t>(comp);" in main
assert (
"act->set_update_interval([](uint32_t x) -> uint32_t {\n"
"static void esphome__act__fn(const std::remove_cvref_t<uint32_t> & x) {\n"
" PollingComponent &esphome__component = *::comp;\n"
" if (esphome__component.is_ready()) {\n"
" esphome__component.set_update_interval([](uint32_t x) -> uint32_t {\n"
" return x * 1000;\n"
"});"
) in main
" }(x));\n"
" esphome__component.start_poller();\n"
" }\n"
) in CORE.cpp_global_section
@pytest.mark.asyncio
@pytest.mark.usefixtures("apply_actions")
async def test_component_update_not_ready_still_runs_merged_actions() -> None:
"""The ready check skips only the update, never the actions merged after it."""
cg.new_Pvariable(ID("pcomp", is_declaration=True, type=cg.PollingComponent))
update = _component_action("component.update")
update["component.update"][CONF_ID] = ID("pcomp", type=cg.PollingComponent)
result = await build_action_list(
[update, _action("my.a", "act_a")], TEMPLATE_ARG, []
)
assert [str(action) for action in result] == ["act"]
text = CORE.cpp_global_section
assert text.index("esphome__component.update();") < text.index("::comp->do_a();")
# The only return is the stop check between the two actions
assert text.count("return;") == 1
@pytest.mark.asyncio
@pytest.mark.usefixtures("apply_actions")
async def test_callback_automation_if_with_component_update_is_direct() -> None:
"""An ``if`` whose branches update components becomes plain C++ in the callback."""
cg.new_Pvariable(ID("pcomp", is_declaration=True, type=cg.PollingComponent))
def update(id_: str) -> ConfigType:
return {
"component.update": {CONF_ID: ID("pcomp", type=cg.PollingComponent)},
CONF_TYPE_ID: ID(
id_, is_declaration=True, type=ApplyAction, is_manual=False
),
}
conf = _automation_conf(
{
"if": {
CONF_CONDITION: _inline_condition(),
CONF_THEN: [update("upd_then")],
CONF_ELSE: [update("upd_else")],
},
CONF_TYPE_ID: ID(
"act_if", is_declaration=True, type=IfAction, is_manual=False
),
}
)
await build_callback_automation(CALLBACK_PARENT, "add_cb", [], conf)
assert "Action" not in CORE.cpp_main_section
assert "Condition" not in CORE.cpp_main_section
text = CORE.cpp_global_section
assert "if (esphome__condition) {" in text
assert text.count("esphome__component.update();") == 2
@pytest.mark.asyncio
@pytest.mark.usefixtures("polling_component")
@pytest.mark.parametrize("body", ["x *= 1000; return x;", "return x *= 1000;"])
async def test_component_resume_interval_lambda_takes_args_by_value(body: str) -> None:
"""The user's interval lambda gets the trigger args by value, so it may modify them."""
"""The user's interval lambda gets the trigger args by value, so it may modify them.
It is called rather than reduced, so even a single return modifies only its own copy.
"""
await build_action(
_component_action("component.resume", {CONF_UPDATE_INTERVAL: Lambda(body)}),
cg.TemplateArguments(cg.uint32),
[(cg.uint32, "x")],
)
assert f"[](uint32_t x) -> uint32_t {{\n {body}\n}}" in CORE.cpp_main_section
assert (
f"set_update_interval([](uint32_t x) -> uint32_t {{\n {body}\n }}(x));"
in CORE.cpp_global_section
)
COMPONENT_ID = ID("comp", type=cg.Component)