mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 01:25:57 +00:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
113eb659d5 | ||
|
|
a9ae0380cb | ||
|
|
9d65f4fe20 | ||
|
|
3482d33176 | ||
|
|
6ecd846999 | ||
|
|
4912a88528 | ||
|
|
475d794012 | ||
|
|
e0e25ac7cc | ||
|
|
948a9eeb27 | ||
|
|
65a83c12b6 | ||
|
|
d9d8518171 | ||
|
|
7815874624 | ||
|
|
0b6fc2c3c5 | ||
|
|
4705fbb703 | ||
|
|
ce70abbbb4 | ||
|
|
77574a50eb | ||
|
|
c52beb6d5e | ||
|
|
dde3aaf50e | ||
|
|
6f7af22b41 | ||
|
|
de0bf34e84 | ||
|
|
7bd7547983 | ||
|
|
aae52f2265 |
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 @Rodrigoah
|
||||
esphome/components/whirlpool/* @arpena @glmnet
|
||||
esphome/components/whynter/* @aeonsablaze
|
||||
esphome/components/wiegand/* @ssieb
|
||||
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
|
||||
|
||||
+47
-2
@@ -5,6 +5,7 @@ import logging
|
||||
import math
|
||||
import re
|
||||
import string
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -2123,6 +2124,17 @@ def _direct_function(
|
||||
)
|
||||
|
||||
|
||||
def _call_target(
|
||||
plan: list[MockObj | list[_InlineAction]],
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
) -> MockObj | str:
|
||||
"""What a generated callback calls: the direct function, else ``<automation>->trigger``."""
|
||||
if (target := _direct_function(plan, args, config)) is None:
|
||||
target = parent_ref(_automation_from_plan(plan, args, config)).trigger
|
||||
return target
|
||||
|
||||
|
||||
def _when_call(
|
||||
when: str | ApplyCall | None, parent: MockObj | None
|
||||
) -> ApplyCall | None:
|
||||
@@ -2140,6 +2152,40 @@ async def build_automation(
|
||||
return await new_automation(args, config, trigger)
|
||||
|
||||
|
||||
async def build_component_automation(
|
||||
config: ConfigType, component_class: MockObjClass, priority: float | None = None
|
||||
) -> MockObj:
|
||||
"""Create and register the Component that runs a no-argument automation.
|
||||
|
||||
That is ``component_class<function[, priority bits]>``, whose function runs the actions
|
||||
directly when they are all inline, else triggers their Automation. A ``trigger_id`` set
|
||||
by hand keeps its Trigger, built with ``priority`` if given.
|
||||
"""
|
||||
trigger_id: ID = config[CONF_TRIGGER_ID]
|
||||
if trigger_id.is_manual:
|
||||
var = cg.new_Pvariable(trigger_id, *([] if priority is None else [priority]))
|
||||
await cg.register_component(var, config)
|
||||
await build_automation(var, [], config)
|
||||
return var
|
||||
# Declared and registered before the actions are built, which can wait for other ids
|
||||
name = _apply_function_name(trigger_id)
|
||||
define = cg.declare_static_function(name, cg.void, [])
|
||||
id_ = trigger_id.copy()
|
||||
id_.type = component_class
|
||||
bits = [] if priority is None else [_float_bits(priority)]
|
||||
var = cg.new_Pvariable(id_, cg.TemplateArguments(cg.RawExpression(name), *bits))
|
||||
await cg.register_component(var, config)
|
||||
plan = await _build_automation_plan([], config)
|
||||
define([str(cg.statement(CallExpression(_call_target(plan, [], config))))])
|
||||
return var
|
||||
|
||||
|
||||
def _float_bits(value: float) -> HexInt:
|
||||
"""``value`` as a C++ float's bit pattern, for a template argument."""
|
||||
(bits,) = struct.unpack("<I", struct.pack("<f", value))
|
||||
return HexInt(bits)
|
||||
|
||||
|
||||
async def build_trigger_callback(
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
@@ -2173,8 +2219,7 @@ async def _trigger_callback(
|
||||
call: ApplyCall | None,
|
||||
parent: MockObj | None,
|
||||
) -> LambdaExpression:
|
||||
if (target := _direct_function(plan, args, config)) is None:
|
||||
target = f"{parent_ref(_automation_from_plan(plan, args, config))}->trigger"
|
||||
target = _call_target(plan, args, config)
|
||||
statements: list[str] = []
|
||||
if call is not None:
|
||||
members = call.members
|
||||
|
||||
@@ -31,6 +31,7 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
add_global,
|
||||
add_library,
|
||||
add_platformio_option,
|
||||
declare_static_function,
|
||||
extern_progmem_array,
|
||||
get_variable,
|
||||
get_variable_with_full_id,
|
||||
|
||||
@@ -7,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 */
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -171,6 +171,41 @@ class LoopTrigger : public Trigger<>, public Component {
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
};
|
||||
|
||||
/// Component whose setup priority is the float with bit pattern PRIORITY_BITS, since GCC 10 (ESP8266)
|
||||
/// has no float template parameters. Templated on the priority only, so each priority has one copy.
|
||||
template<uint32_t PRIORITY_BITS> class PriorityComponent : public Component {
|
||||
public:
|
||||
float get_setup_priority() const override { return bit_cast<float>(PRIORITY_BITS); }
|
||||
};
|
||||
|
||||
/// Bit pattern of setup_priority::DATA (600.0f), the Component default.
|
||||
static constexpr uint32_t SETUP_PRIORITY_DATA_BITS = 0x44160000;
|
||||
// bit_cast is constexpr only where std::bit_cast exists, which GCC 10 (ESP8266) lacks
|
||||
#if __cpp_lib_bit_cast >= 201806
|
||||
static_assert(bit_cast<uint32_t>(setup_priority::DATA) == SETUP_PRIORITY_DATA_BITS);
|
||||
#endif
|
||||
|
||||
/// The default priority needs no override.
|
||||
template<> class PriorityComponent<SETUP_PRIORITY_DATA_BITS> : public Component {};
|
||||
|
||||
/// Calls Fn from setup(), for on_boot.
|
||||
template<auto Fn, uint32_t PRIORITY_BITS> class StartupCallback final : public PriorityComponent<PRIORITY_BITS> {
|
||||
public:
|
||||
void setup() override { Fn(); }
|
||||
};
|
||||
|
||||
/// Calls Fn from on_shutdown(), for on_shutdown.
|
||||
template<auto Fn, uint32_t PRIORITY_BITS> class ShutdownCallback final : public PriorityComponent<PRIORITY_BITS> {
|
||||
public:
|
||||
void on_shutdown() override { Fn(); }
|
||||
};
|
||||
|
||||
/// Calls Fn from loop(), for on_loop.
|
||||
template<auto Fn> class LoopCallback final : public Component {
|
||||
public:
|
||||
void loop() override { Fn(); }
|
||||
};
|
||||
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
class ProjectUpdateTrigger : public Trigger<std::string>, public Component {
|
||||
public:
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
|
||||
+12
-14
@@ -124,6 +124,9 @@ ShutdownTrigger = cg.esphome_ns.class_(
|
||||
LoopTrigger = cg.esphome_ns.class_(
|
||||
"LoopTrigger", cg.Component, automation.Trigger.template()
|
||||
)
|
||||
StartupCallback = cg.esphome_ns.class_("StartupCallback", cg.Component)
|
||||
ShutdownCallback = cg.esphome_ns.class_("ShutdownCallback", cg.Component)
|
||||
LoopCallback = cg.esphome_ns.class_("LoopCallback", cg.Component)
|
||||
ProjectUpdateTrigger = cg.esphome_ns.class_(
|
||||
"ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string)
|
||||
)
|
||||
@@ -785,20 +788,15 @@ async def _add_environment_variables(env_vars: dict[str, str]) -> None:
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _add_automations(config):
|
||||
for conf in config.get(CONF_ON_BOOT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_SHUTDOWN, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY))
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_LOOP, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for key, component_class in (
|
||||
(CONF_ON_BOOT, StartupCallback),
|
||||
(CONF_ON_SHUTDOWN, ShutdownCallback),
|
||||
(CONF_ON_LOOP, LoopCallback),
|
||||
):
|
||||
for conf in config.get(key, []):
|
||||
await automation.build_component_automation(
|
||||
conf, component_class, conf.get(CONF_PRIORITY)
|
||||
)
|
||||
|
||||
|
||||
# Datetime component has special subtypes that need additional defines
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -883,14 +883,13 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
|
||||
return Pvariable(id_, rhs)
|
||||
|
||||
|
||||
def static_function(
|
||||
def declare_static_function(
|
||||
name: str,
|
||||
return_type: SafeExpType,
|
||||
parameters: TemplateArgsType,
|
||||
body: list[str | Statement],
|
||||
) -> RawExpression:
|
||||
"""Emit ``static <return_type> <name>(parameters) { body }`` at global scope and return an
|
||||
expression naming it, for use as a template argument or a function pointer.
|
||||
) -> Callable[[list[str | Statement]], RawExpression]:
|
||||
"""Declare ``static <return_type> <name>(parameters)`` now and return the function that
|
||||
emits its body, for a caller that names the function before the body is known.
|
||||
|
||||
The declaration goes with the other globals, so a template argument can name the function
|
||||
at once; the body goes after the user includes, where every id and everything those
|
||||
@@ -900,10 +899,24 @@ def static_function(
|
||||
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
|
||||
)
|
||||
add_global(RawStatement(f"{signature};"))
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
|
||||
)
|
||||
return RawExpression(name)
|
||||
|
||||
def define(body: list[str | Statement]) -> RawExpression:
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(map(str, body)))}\n}}")
|
||||
)
|
||||
return RawExpression(name)
|
||||
|
||||
return define
|
||||
|
||||
|
||||
def static_function(
|
||||
name: str,
|
||||
return_type: SafeExpType,
|
||||
parameters: TemplateArgsType,
|
||||
body: list[str | Statement],
|
||||
) -> RawExpression:
|
||||
"""``declare_static_function`` with the body known at once."""
|
||||
return declare_static_function(name, return_type, parameters)(body)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -30,14 +30,6 @@ def _automation_of(main_cpp: str, trigger: str) -> str:
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _has_delay(main_cpp: str, trigger: str) -> bool:
|
||||
automation = _automation_of(main_cpp, trigger)
|
||||
return (
|
||||
re.search(rf"{automation}->add_action\(delayaction_id\w*\);", main_cpp)
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
def _registered(main_cpp: str) -> list[str]:
|
||||
return re.findall(r"App\.register_component_\((\w+), \d+\);", main_cpp)
|
||||
|
||||
@@ -56,18 +48,59 @@ def test_core_automations_registration_order(main_cpp: str) -> None:
|
||||
]
|
||||
|
||||
|
||||
def _function_body(main_cpp: str, name: str) -> str:
|
||||
match = re.search(rf"static void {name}\(\) \{{(.*?)\n\}}\n", main_cpp, re.DOTALL)
|
||||
assert match is not None, name
|
||||
return match.group(1)
|
||||
|
||||
|
||||
def _actions_body(main_cpp: str, function: str) -> str:
|
||||
"""The body of the function of inline actions that ``function`` calls."""
|
||||
match = re.fullmatch(r"\n (\w+)\(\);", _function_body(main_cpp, function))
|
||||
assert match is not None, function
|
||||
return _function_body(main_cpp, match.group(1))
|
||||
|
||||
|
||||
def _delayed_automation(main_cpp: str, function: str) -> str:
|
||||
"""The Automation that ``function`` triggers, checked to hold a delay; found through the
|
||||
function rather than an id counter."""
|
||||
match = re.fullmatch(
|
||||
r"\n ::(\w+)->trigger\(\);", _function_body(main_cpp, function)
|
||||
)
|
||||
assert match is not None, function
|
||||
automation = match.group(1)
|
||||
assert f"new({automation}) Automation<>();" in main_cpp
|
||||
assert re.search(rf"{automation}->add_action\(delayaction_id\w*\);", main_cpp)
|
||||
return automation
|
||||
|
||||
|
||||
def test_on_boot_inline_priorities(main_cpp: str) -> None:
|
||||
"""All-inline on_boot blocks keep their configured priority, default 600."""
|
||||
assert _constructed(main_cpp, "startuptrigger_id") == "StartupTrigger(800.0f)"
|
||||
assert _constructed(main_cpp, "startuptrigger_id_2") == "StartupTrigger(600.0f)"
|
||||
_automation_of(main_cpp, "startuptrigger_id")
|
||||
_automation_of(main_cpp, "startuptrigger_id_2")
|
||||
"""All-inline on_boot blocks call one function from setup() at their priority, default 600.
|
||||
|
||||
The priority is the float's bit pattern: 0x44480000 is 800.0f, 0x44160000 is 600.0f.
|
||||
"""
|
||||
assert _constructed(main_cpp, "startuptrigger_id") == (
|
||||
"StartupCallback<esphome__startuptrigger_id__fn, 0x44480000>()"
|
||||
)
|
||||
assert _constructed(main_cpp, "startuptrigger_id_2") == (
|
||||
"StartupCallback<esphome__startuptrigger_id_2__fn, 0x44160000>()"
|
||||
)
|
||||
assert "Automation<>(startuptrigger_id" not in main_cpp
|
||||
# Both actions of the first block run in the one function, with no stop checks
|
||||
body = _actions_body(main_cpp, "esphome__startuptrigger_id__fn")
|
||||
assert "boot high" in body
|
||||
assert "boot high lambda" in body
|
||||
assert "esphome__running" not in body
|
||||
assert "Automation" not in body
|
||||
assert "boot default" in _actions_body(main_cpp, "esphome__startuptrigger_id_2__fn")
|
||||
|
||||
|
||||
def test_on_boot_delay_keeps_trigger(main_cpp: str) -> None:
|
||||
"""A delay needs the Trigger and its Automation of separate actions."""
|
||||
assert _constructed(main_cpp, "startuptrigger_id_3") == "StartupTrigger(-100.0f)"
|
||||
assert _has_delay(main_cpp, "startuptrigger_id_3")
|
||||
def test_on_boot_delay_triggers_automation(main_cpp: str) -> None:
|
||||
"""A delay keeps the Automation of separate actions, which the function triggers."""
|
||||
assert _constructed(main_cpp, "startuptrigger_id_3") == (
|
||||
"StartupCallback<esphome__startuptrigger_id_3__fn, 0xC2C80000>()"
|
||||
)
|
||||
_delayed_automation(main_cpp, "esphome__startuptrigger_id_3__fn")
|
||||
|
||||
|
||||
def test_on_boot_manual_trigger_id_keeps_trigger(main_cpp: str) -> None:
|
||||
@@ -77,19 +110,30 @@ def test_on_boot_manual_trigger_id_keeps_trigger(main_cpp: str) -> None:
|
||||
|
||||
|
||||
def test_on_shutdown_priorities(main_cpp: str) -> None:
|
||||
"""on_shutdown blocks keep their priority; a delay keeps the Trigger."""
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id") == "ShutdownTrigger(700.0f)"
|
||||
_automation_of(main_cpp, "shutdowntrigger_id")
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id_2") == "ShutdownTrigger(600.0f)"
|
||||
assert _has_delay(main_cpp, "shutdowntrigger_id_2")
|
||||
"""on_shutdown calls its function at its priority; a delay keeps the Automation."""
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id") == (
|
||||
"ShutdownCallback<esphome__shutdowntrigger_id__fn, 0x442F0000>()"
|
||||
)
|
||||
assert "shutdown inline" in _actions_body(
|
||||
main_cpp, "esphome__shutdowntrigger_id__fn"
|
||||
)
|
||||
assert _constructed(main_cpp, "shutdowntrigger_id_2") == (
|
||||
"ShutdownCallback<esphome__shutdowntrigger_id_2__fn, 0x44160000>()"
|
||||
)
|
||||
_delayed_automation(main_cpp, "esphome__shutdowntrigger_id_2__fn")
|
||||
|
||||
|
||||
def test_on_loop(main_cpp: str) -> None:
|
||||
"""on_loop blocks are looping components; a delay keeps the Trigger."""
|
||||
assert _constructed(main_cpp, "looptrigger_id") == "LoopTrigger()"
|
||||
_automation_of(main_cpp, "looptrigger_id")
|
||||
assert _constructed(main_cpp, "looptrigger_id_2") == "LoopTrigger()"
|
||||
assert _has_delay(main_cpp, "looptrigger_id_2")
|
||||
"""on_loop calls its function each loop; a delay keeps the Automation."""
|
||||
assert _constructed(main_cpp, "looptrigger_id") == (
|
||||
"LoopCallback<esphome__looptrigger_id__fn>()"
|
||||
)
|
||||
assert "loop inline" in _actions_body(main_cpp, "esphome__looptrigger_id__fn")
|
||||
assert "HasLoopOverride<LoopCallback<esphome__looptrigger_id__fn>>" in main_cpp
|
||||
assert _constructed(main_cpp, "looptrigger_id_2") == (
|
||||
"LoopCallback<esphome__looptrigger_id_2__fn>()"
|
||||
)
|
||||
_delayed_automation(main_cpp, "esphome__looptrigger_id_2__fn")
|
||||
|
||||
|
||||
def test_on_boot_registered_before_its_actions_are_built(
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/base_automation.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
int calls = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void count_call() { calls++; }
|
||||
|
||||
// Bit patterns of 800.0f, 600.0f, -100.0f and 250.5f, as the code generator writes them
|
||||
constexpr uint32_t PRIORITY_800 = 0x44480000;
|
||||
constexpr uint32_t PRIORITY_MINUS_100 = 0xC2C80000;
|
||||
constexpr uint32_t PRIORITY_250_5 = 0x437A8000;
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(CoreAutomationCallbacks, StartupCallbackRunsFromSetupAtItsPriority) {
|
||||
calls = 0;
|
||||
StartupCallback<count_call, PRIORITY_800> high;
|
||||
StartupCallback<count_call, PRIORITY_MINUS_100> late;
|
||||
StartupCallback<count_call, PRIORITY_250_5> fractional;
|
||||
EXPECT_EQ(high.get_setup_priority(), 800.0f);
|
||||
EXPECT_EQ(late.get_setup_priority(), -100.0f);
|
||||
EXPECT_EQ(fractional.get_setup_priority(), 250.5f);
|
||||
StartupCallback<count_call, SETUP_PRIORITY_DATA_BITS> standard;
|
||||
EXPECT_EQ(standard.get_setup_priority(), setup_priority::DATA);
|
||||
|
||||
high.loop();
|
||||
high.on_shutdown();
|
||||
EXPECT_EQ(calls, 0);
|
||||
high.setup();
|
||||
EXPECT_EQ(calls, 1);
|
||||
}
|
||||
|
||||
TEST(CoreAutomationCallbacks, ShutdownCallbackRunsFromOnShutdownAtItsPriority) {
|
||||
calls = 0;
|
||||
ShutdownCallback<count_call, PRIORITY_MINUS_100> shutdown;
|
||||
EXPECT_EQ(shutdown.get_setup_priority(), -100.0f);
|
||||
|
||||
shutdown.setup();
|
||||
shutdown.loop();
|
||||
EXPECT_EQ(calls, 0);
|
||||
shutdown.on_shutdown();
|
||||
EXPECT_EQ(calls, 1);
|
||||
}
|
||||
|
||||
TEST(CoreAutomationCallbacks, LoopCallbackRunsEachLoopAtDataPriority) {
|
||||
calls = 0;
|
||||
LoopCallback<count_call> looping;
|
||||
EXPECT_EQ(looping.get_setup_priority(), setup_priority::DATA);
|
||||
EXPECT_TRUE(HasLoopOverride<LoopCallback<count_call>>::value);
|
||||
EXPECT_FALSE((HasLoopOverride<StartupCallback<count_call, PRIORITY_800>>::value));
|
||||
|
||||
looping.setup();
|
||||
EXPECT_EQ(calls, 0);
|
||||
looping.loop();
|
||||
looping.loop();
|
||||
EXPECT_EQ(calls, 2);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -2,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
|
||||
@@ -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
|
||||
@@ -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]))
|
||||
-10
@@ -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
|
||||
-33
@@ -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"),
|
||||
]
|
||||
@@ -1173,6 +1173,21 @@ def test_static_function_declares_early_and_defines_after_includes() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_declare_static_function_defines_the_body_later() -> None:
|
||||
"""The declaration is emitted at once; the body only when define() is called."""
|
||||
define = cg.declare_static_function("esphome__act__fn", ct.void, [])
|
||||
assert [str(st) for st in CORE.global_statements] == [
|
||||
"static void esphome__act__fn();"
|
||||
]
|
||||
assert CORE.global_statements_after_includes == []
|
||||
|
||||
fn = define(["first();", "second();"])
|
||||
assert str(fn) == "esphome__act__fn"
|
||||
assert [str(st) for st in CORE.global_statements_after_includes] == [
|
||||
"static void esphome__act__fn() {\n first();\n second();\n}"
|
||||
]
|
||||
|
||||
|
||||
def test_static_function_accepts_statements() -> None:
|
||||
call = cg.MockObj("foo")(cg.MockObj("x") >> 2)
|
||||
cg.static_function(
|
||||
|
||||
Reference in new issue
Block a user