Compare commits

...
Author SHA1 Message Date
J. Nick Koston 3278ea5a04 Merge branch 'dev' into ln882x-crash-handler 2026-10-27 10:07:54 -10:00
c3348789eb [gree] Add IR receiver support and YB1FA model (#18435)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Leonardo Rivera <leodrivera@gmail.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-27 09:28:11 -10:00
J. Nick Koston 361a9580da [libretiny] Gate the vector-patch crash handler on one derived define
The RTL8720C and LN882X check was copied into hal.cpp, the header and the
source; USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH now names it once.
2026-10-27 09:13:25 -10:00
J. Nick Koston 43cc2a8e3d [libretiny] Arm the crash reboot watchdog before recording the fault
A fault while the record was being written spun on the first-fault guard until
the 10 s watchdog from arch_init(), not the 1 s one.
2026-10-27 09:08:25 -10:00
J. Nick Koston 281c5fcabc [libretiny] Keep the first fault when the SDK fault handler faults too
CmBacktrace on the LN882H walks past the end of RAM and faults inside the
fault handler, which overwrote the record with its own hard fault. Record the
first fault of a boot only and wait for the watchdog on any later one.
2026-10-27 09:00:07 -10:00
J. Nick Koston 79c945c76c Merge remote-tracking branch 'origin/dev' into ln882x-crash-handler 2026-10-27 08:32:17 -10:00
J. Nick Koston 92c54aea77 [libretiny] Report the previous boot's crash on LN882H
Put the fault recorder in front of the SDK's CmBacktrace fault handlers through
the RAM vector table, as on the RTL8720C, and keep the record in the RETENTION
RAM that startup does not clear. The Cortex-M capture is now shared and sizes
the exception frame from EXC_RETURN, since the LN882H has an FPU.
2026-10-27 08:18:45 -10:00
30 changed files with 2368 additions and 432 deletions

No files matched your search

+13 -2
View File
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import climate_ir
from esphome.components import climate_ir, remote_base
import esphome.config_validation as cv
from esphome.const import CONF_MODEL, CONF_USE_FAHRENHEIT
from esphome.types import ConfigType
@@ -13,7 +13,16 @@ GreeClimate = gree_ns.class_("GreeClimate", climate_ir.ClimateIR)
Model = gree_ns.enum("Model")
MODELS = Model.options(
"generic", "yan", "yaa", "yac", "yac1fb9", "yx1ff", "yag", "yaw1f", prefix="GREE_"
"generic",
"yan",
"yaa",
"yac",
"yac1fb9",
"yb1fa",
"yx1ff",
"yag",
"yaw1f",
prefix="GREE_",
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
@@ -32,3 +41,5 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_use_fahrenheit(True))
if config[CONF_MODEL] == "yaw1f":
cg.add_define("USE_GREE_YAW1F")
if remote_base.CONF_RECEIVER_ID in config:
cg.add_define("USE_GREE_RECEIVER")
+537 -190
View File
@@ -4,8 +4,6 @@
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/helpers.h"
#include <algorithm>
namespace esphome::gree {
ESPHOME_LOG_TAG(TAG, "gree.climate");
@@ -13,27 +11,25 @@ ESPHOME_LOG_TAG(TAG, "gree.climate");
static constexpr remote_base::BitTimings GREE_BIT_TIMINGS{
.bit_mark = GREE_BIT_MARK, .one_space = GREE_ONE_SPACE, .zero_space = GREE_ZERO_SPACE};
void GreeClimate::set_model(Model model) {
if (model == GREE_YAN || model == GREE_YX1FF) {
// These models only expose vertical swing; horizontal swing is not encoded on the wire.
this->swing_modes_.erase(climate::CLIMATE_SWING_HORIZONTAL);
this->swing_modes_.erase(climate::CLIMATE_SWING_BOTH);
}
if (model == GREE_YX1FF) {
this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET); // YX1FF 4 speed
}
// Models with a sleep preset
if (model == GREE_YX1FF || model == GREE_YAC1FB9) {
this->presets_.insert(climate::CLIMATE_PRESET_NONE);
this->presets_.insert(climate::CLIMATE_PRESET_SLEEP);
}
static constexpr uint8_t GREE_SWING_AUTO_MASK = 0x40;
#ifdef USE_GREE_YAW1F
// The YAW1F second message is 00 00 00 A0 | 00 00 00 A0.
static constexpr uint8_t GREE_YAW1F_FOLLOWUP_BYTE = 0xA0;
#endif
this->model_ = model;
// The mode, fan and feature mappings use if-else: a switch becomes a lookup table in RAM on ESP8266.
static constexpr uint8_t supported_feature_mask(Model model) {
if (model == GREE_YB1FA)
return GREE_MODE_BIT_TURBO | GREE_MODE_BIT_LIGHT | GREE_MODE_BIT_XFAN;
if (model == GREE_YX1FF)
return GREE_MODE_BIT_LIGHT;
if (model == GREE_GENERIC || model == GREE_YAG || model == GREE_YAW1F)
return 0;
return GREE_MODE_BIT_TURBO | GREE_MODE_BIT_LIGHT | GREE_MODE_BIT_HEALTH | GREE_MODE_BIT_XFAN;
}
void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
assign_bits(this->mode_bits_, bit_mask, enabled);
this->transmit_state();
static constexpr uint8_t default_feature_bits(Model model) {
return supported_feature_mask(model) & GREE_MODE_BIT_LIGHT;
}
#ifdef USE_GREE_FAHRENHEIT
@@ -51,190 +47,292 @@ uint8_t fahrenheit_half_steps(float target_celsius) {
}
#endif
void GreeClimate::transmit_state() {
uint8_t remote_state[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00};
const Model model = this->model_;
// YAW1F uses the YAG byte layout with different framing (see below)
const bool yag_layout = model == GREE_YAG || model == GREE_YAW1F;
struct GreeFrameSpaces {
uint32_t header;
uint32_t message;
};
remote_state[0] = this->fan_speed_() | this->operation_mode_();
#ifdef USE_GREE_FAHRENHEIT
const uint8_t half_steps = this->use_fahrenheit_ ? fahrenheit_half_steps(this->target_temperature) : 0;
remote_state[1] = this->use_fahrenheit_ ? half_steps >> 1 : this->temperature_();
#else
remote_state[1] = this->temperature_();
static GreeFrameSpaces frame_spaces(Model model) {
if (model == GREE_YAC1FB9 || model == GREE_YAW1F)
return {GREE_YAC1FB9_HEADER_SPACE, GREE_YAC1FB9_MESSAGE_SPACE};
return {GREE_HEADER_SPACE, GREE_MESSAGE_SPACE};
}
static void write_frame(remote_base::RemoteTransmitData *data, const GreeState &frame, const GreeFrameSpaces &spaces) {
data->item(GREE_HEADER_MARK, spaces.header);
data->item_bytes<GREE_BIT_TIMINGS>(frame.data(), 4);
// Fixed 010 between the two halves, then the message gap
data->item(GREE_BIT_MARK, GREE_ZERO_SPACE);
data->item(GREE_BIT_MARK, GREE_ONE_SPACE);
data->item(GREE_BIT_MARK, GREE_ZERO_SPACE);
data->item(GREE_BIT_MARK, spaces.message);
data->item_bytes<GREE_BIT_TIMINGS>(frame.data() + 4, 4);
data->mark(GREE_BIT_MARK);
}
#ifdef USE_GREE_RECEIVER
static constexpr uint8_t GREE_MODE_MASK = 0x07;
static constexpr uint8_t GREE_POWER_MASK = 0x08;
static constexpr uint8_t GREE_FAN_MASK = 0x30;
static constexpr uint8_t GREE_SLEEP_MASK = 0x80;
static constexpr uint8_t GREE_TEMP_MASK = 0x0F;
static constexpr uint8_t GREE_TIMER_HALF_HOUR_MASK = 0x10;
static constexpr uint8_t GREE_TIMER_TENS_HOUR_MASK = 0x60;
static constexpr uint8_t GREE_TIMER_HOURS_MASK = 0x0F;
static constexpr uint8_t GREE_BYTE5_FIXED_MASK = 0xB8;
static constexpr uint8_t GREE_BYTE5_FIXED_VALUE = 0x20;
static bool read_frame(remote_base::RemoteReceiveData &data, GreeState &frame, const GreeFrameSpaces &spaces) {
return data.expect_item(GREE_HEADER_MARK, spaces.header) && data.expect_bytes<GREE_BIT_TIMINGS>(frame.data(), 4) &&
data.expect_item(GREE_BIT_MARK, GREE_ZERO_SPACE) && data.expect_item(GREE_BIT_MARK, GREE_ONE_SPACE) &&
data.expect_item(GREE_BIT_MARK, GREE_ZERO_SPACE) && data.expect_item(GREE_BIT_MARK, spaces.message) &&
data.expect_bytes<GREE_BIT_TIMINGS>(frame.data() + 4, 4) && data.expect_mark(GREE_BIT_MARK);
}
#ifdef USE_GREE_YAW1F
static bool is_yaw1f_followup(const GreeState &state) {
return state[3] == GREE_YAW1F_FOLLOWUP_BYTE && state[7] == GREE_YAW1F_FOLLOWUP_BYTE &&
(state[0] | state[1] | state[2] | state[4] | state[5] | state[6]) == 0;
}
#endif
if (model == GREE_YAN) {
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[4] = this->vertical_swing_();
// YAC sends a 0x70 follow-up frame and YAW1F a fixed second message; neither carries climate state.
static bool is_auxiliary_frame(Model model, const GreeState &state) {
#ifdef USE_GREE_YAW1F
if (model == GREE_YAW1F && is_yaw1f_followup(state))
return true;
#endif
return model == GREE_YAC && (state[3] & 0xF0) == 0x70;
}
static bool is_valid_timer(const GreeState &state) {
const uint8_t timer_hours = state[2] & GREE_TIMER_HOURS_MASK;
const uint8_t timer_tens_hours = (state[1] & GREE_TIMER_TENS_HOUR_MASK) >> 5;
const bool timer_half_hour = state[1] & GREE_TIMER_HALF_HOUR_MASK;
if (timer_hours > 9 || timer_tens_hours > 2)
return false;
if (timer_tens_hours < 2)
return true;
return timer_hours < 4 || (timer_hours == 4 && !timer_half_hour);
}
static bool is_valid_vertical_swing(bool automatic, uint8_t position) {
if (automatic) {
return position == GREE_VDIR_SWING || position == GREE_VDIR_SWING_DOWN || position == GREE_VDIR_SWING_MIDDLE ||
position == GREE_VDIR_SWING_UP;
}
return position == GREE_VDIR_MANUAL || (position >= GREE_VDIR_UP && position <= GREE_VDIR_DOWN);
}
if (model == GREE_YX1FF || yag_layout) {
remote_state[2] = 0x60;
remote_state[3] = 0x50;
remote_state[4] = this->vertical_swing_();
static bool is_valid_swing(Model model, const GreeState &state) {
const bool automatic = state[0] & GREE_SWING_AUTO_MASK;
const uint8_t vertical = state[4] & 0x0F;
const uint8_t horizontal = state[4] >> 4;
if (model == GREE_YX1FF) {
// ModelA is set only when powered on.
if (this->mode == climate::CLIMATE_MODE_OFF)
remote_state[2] &= ~0x40;
// YX1FF needs the swing flag as well as its own vane-position value
if (this->vertical_swing_() == GREE_VDIR_SWING) {
remote_state[0] |= (1 << 6);
remote_state[4] = GREE_YX1FF_VANE_SWING;
}
}
}
if (!is_valid_vertical_swing(automatic, vertical))
return false;
if (model == GREE_YX1FF)
return horizontal == (automatic ? GREE_HDIR_SWING : GREE_HDIR_MANUAL);
return horizontal == GREE_HDIR_MANUAL;
}
if (yag_layout) {
remote_state[5] = model == GREE_YAW1F ? 0x80 : 0x40;
if (this->vertical_swing_() == GREE_VDIR_SWING || this->horizontal_swing_() == GREE_HDIR_SWING) {
remote_state[0] |= (1 << 6);
}
}
if (model == GREE_YAC)
remote_state[4] = this->vertical_swing_();
if (model == GREE_YAC || yag_layout) {
remote_state[4] |= (this->horizontal_swing_() << 4);
}
if (model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010
if (this->vertical_swing_() == GREE_VDIR_SWING) {
remote_state[0] |= (1 << 6); // Enable swing by setting bit 6
} else if (this->vertical_swing_() != GREE_VDIR_AUTO) {
remote_state[5] = this->vertical_swing_();
}
}
if (model == GREE_YAN || model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
// Merge the mode bits into remote_state[2]
// Clear the mode bits (bits 4-7) and OR in the current mode_bits_
remote_state[2] = (remote_state[2] & 0x0F) | this->mode_bits_;
}
static climate::ClimateMode decode_operation_mode(uint8_t mode) {
if (mode == GREE_MODE_COOL)
return climate::CLIMATE_MODE_COOL;
if (mode == GREE_MODE_DRY)
return climate::CLIMATE_MODE_DRY;
if (mode == GREE_MODE_FAN)
return climate::CLIMATE_MODE_FAN_ONLY;
if (mode == GREE_MODE_HEAT)
return climate::CLIMATE_MODE_HEAT;
return climate::CLIMATE_MODE_HEAT_COOL;
}
// Use if-else rather than lookup tables to avoid RAM usage on ESP8266.
static climate::ClimateFanMode decode_fan_mode(Model model, uint8_t fan) {
if (fan == GREE_FAN_AUTO)
return climate::CLIMATE_FAN_AUTO;
if (model == GREE_YX1FF) {
if (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO) == climate::CLIMATE_FAN_HIGH) {
remote_state[2] |= GREE_FAN_TURBO_BIT;
}
if (fan == GREE_FAN_1)
return climate::CLIMATE_FAN_QUIET;
if (fan == GREE_FAN_2)
return climate::CLIMATE_FAN_LOW;
return climate::CLIMATE_FAN_MEDIUM;
}
if (fan == GREE_FAN_1)
return climate::CLIMATE_FAN_LOW;
if (fan == GREE_FAN_2)
return climate::CLIMATE_FAN_MEDIUM;
return climate::CLIMATE_FAN_HIGH;
}
#endif // USE_GREE_RECEIVER
if (this->preset_() == GREE_PRESET_SLEEP) {
remote_state[0] |= GREE_PRESET_SLEEP_BIT;
}
#ifdef USE_GREE_FAHRENHEIT
if (this->use_fahrenheit_) {
remote_state[3] |= GREE_FAHRENHEIT_BIT;
if (half_steps & 1)
remote_state[3] |= GREE_TEMP_HALF_DEGREE_BIT;
}
#endif
remote_state[7] = frame_checksum(remote_state);
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
void GreeProtocol::encode(remote_base::RemoteTransmitData *data, const GreeState &state) const {
const GreeFrameSpaces spaces = frame_spaces(this->model_);
data->reserve(this->model_ == GREE_YAW1F ? 280 : 140);
data->set_carrier_frequency(GREE_IR_FREQUENCY);
const bool long_gaps = model == GREE_YAC1FB9 || model == GREE_YAW1F;
const uint32_t header_space = long_gaps ? GREE_YAC1FB9_HEADER_SPACE : GREE_HEADER_SPACE;
const uint32_t message_space = long_gaps ? GREE_YAC1FB9_MESSAGE_SPACE : GREE_MESSAGE_SPACE;
auto encode = [&](const uint8_t *state) {
data->item(GREE_HEADER_MARK, header_space);
data->item_bytes<GREE_BIT_TIMINGS>(state, 4);
// Fixed 010 between the two halves, then the message gap
data->item(GREE_BIT_MARK, GREE_ZERO_SPACE);
data->item(GREE_BIT_MARK, GREE_ONE_SPACE);
data->item(GREE_BIT_MARK, GREE_ZERO_SPACE);
data->item(GREE_BIT_MARK, message_space);
data->item_bytes<GREE_BIT_TIMINGS>(state + 4, 4);
data->mark(GREE_BIT_MARK);
};
encode(remote_state);
write_frame(data, state, spaces);
#ifdef USE_GREE_YAW1F
if (model == GREE_YAW1F) {
// The YAW1F remote follows every command with a second message, 00 00 00 A0 | 00 00 00 A0.
// The command buffer is reused so the payload takes no RAM on ESP8266.
std::fill_n(remote_state, sizeof(remote_state), 0);
remote_state[3] = remote_state[7] = 0xA0;
if (this->model_ == GREE_YAW1F) {
// The YAW1F remote follows every command with a fixed second message.
GreeState followup{};
followup[3] = followup[7] = GREE_YAW1F_FOLLOWUP_BYTE;
data->space(GREE_YAW1F_FOLLOWUP_SPACE);
encode(remote_state);
write_frame(data, followup, spaces);
}
#endif
data->space(0);
transmit.perform();
}
uint8_t GreeClimate::operation_mode_() {
uint8_t operating_mode = GREE_MODE_ON;
#ifdef USE_GREE_RECEIVER
optional<GreeState> GreeProtocol::decode(remote_base::RemoteReceiveData data) const {
const GreeFrameSpaces spaces = frame_spaces(this->model_);
GreeState state{};
if (!read_frame(data, state, spaces) || !GreeProtocol::valid_checksum(state))
return {};
switch (this->mode) {
case climate::CLIMATE_MODE_COOL:
operating_mode |= GREE_MODE_COOL;
break;
case climate::CLIMATE_MODE_DRY:
operating_mode |= GREE_MODE_DRY;
break;
case climate::CLIMATE_MODE_HEAT:
operating_mode |= GREE_MODE_HEAT;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
operating_mode |= GREE_MODE_AUTO;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
operating_mode |= GREE_MODE_FAN;
break;
case climate::CLIMATE_MODE_OFF:
default:
operating_mode = GREE_MODE_OFF;
break;
#ifdef USE_GREE_YAW1F
// Depending on receiver idle settings, YAW1F's ~40 ms second message may arrive in
// the same capture. Validate it completely; never treat it as climate state.
if (this->model_ == GREE_YAW1F && data.is_valid(1)) {
GreeState followup{};
if (!data.expect_space(GREE_YAW1F_FOLLOWUP_SPACE) || !read_frame(data, followup, spaces) ||
!is_yaw1f_followup(followup)) {
ESP_LOGV(TAG, "YAW1F frame dropped: second message invalid");
return {};
}
}
#endif
// A receiver normally appends one final idle space. Reject any extra pulses.
if (data.is_valid(1) || (data.is_valid() && data.peek() > 0)) {
ESP_LOGV(TAG, "Frame dropped: extra pulses follow it (receiver idle may be too long)");
return {};
}
return operating_mode;
return state;
}
#endif // USE_GREE_RECEIVER
uint8_t GreeProtocol::calculate_checksum(const GreeState &state) {
uint8_t sum = 0x0A;
for (uint8_t pos = 0; pos < 4; pos++)
sum += state[pos] & 0x0F;
for (uint8_t pos = 4; pos < GREE_STATE_FRAME_SIZE - 1; pos++)
sum += state[pos] >> 4;
return (sum & 0x0F) << 4;
}
uint8_t GreeClimate::fan_speed_() {
// YX1FF has 4 fan speeds -- we treat low as quiet and turbo as high
if (this->model_ == GREE_YX1FF) {
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
case climate::CLIMATE_FAN_QUIET:
return GREE_FAN_1;
case climate::CLIMATE_FAN_LOW:
return GREE_FAN_2;
case climate::CLIMATE_FAN_MEDIUM:
case climate::CLIMATE_FAN_HIGH:
// HIGH additionally sets the separate Turbo bit in byte 2.
return GREE_FAN_3;
case climate::CLIMATE_FAN_AUTO:
default:
return GREE_FAN_AUTO;
#ifdef USE_GREE_RECEIVER
bool GreeProtocol::valid_checksum(const GreeState &state) {
return (state[GREE_STATE_FRAME_SIZE - 1] & 0xF0) == GreeProtocol::calculate_checksum(state);
}
#endif
GreeState GreeClimateCodec::encode(Model model, const GreeClimateData &data) {
GreeState state{0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00};
const uint8_t vertical_swing = GreeClimateCodec::encode_vertical_swing(data.swing_mode);
const uint8_t horizontal_swing = GreeClimateCodec::encode_horizontal_swing(data.swing_mode);
state[0] =
GreeClimateCodec::encode_fan_mode(model, data.fan_mode) | GreeClimateCodec::encode_operation_mode(data.mode);
const uint8_t target_temperature = clamp<uint8_t>(data.target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX);
state[1] = target_temperature - GREE_TEMP_MIN;
state[2] = data.feature_bits & supported_feature_mask(model);
if (model != GREE_GENERIC)
state[3] = 0x50;
if (model == GREE_YAN)
state[4] = vertical_swing;
if (model == GREE_YB1FA || model == GREE_YX1FF) {
if (data.mode != climate::CLIMATE_MODE_OFF)
state[2] |= GREE_MODEL_A_BIT;
if (model == GREE_YX1FF && data.fan_mode == climate::CLIMATE_FAN_HIGH)
state[2] |= GREE_FAN_TURBO_BIT;
if (vertical_swing == GREE_VDIR_SWING) {
state[0] |= GREE_SWING_AUTO_MASK;
// YX1FF repeats the automatic swing value in both nibbles, while YB1FA only uses the vertical field.
state[4] = model == GREE_YX1FF ? GREE_YX1FF_VANE_SWING : GREE_VDIR_SWING;
}
}
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
case climate::CLIMATE_FAN_LOW:
return GREE_FAN_1;
case climate::CLIMATE_FAN_MEDIUM:
return GREE_FAN_2;
case climate::CLIMATE_FAN_HIGH:
return GREE_FAN_3;
case climate::CLIMATE_FAN_AUTO:
default:
return GREE_FAN_AUTO;
if (model == GREE_YAG || model == GREE_YAW1F) {
state[2] |= 0x60;
state[5] = model == GREE_YAW1F ? 0x80 : 0x40;
if (vertical_swing == GREE_VDIR_SWING || horizontal_swing == GREE_HDIR_SWING)
state[0] |= GREE_SWING_AUTO_MASK;
}
// YAC, like YAG, keeps vertical swing in the low nibble of byte 4; the byte 0 swing flag alone is not enough.
if (model == GREE_YAC || model == GREE_YAG || model == GREE_YAW1F)
state[4] = vertical_swing | (horizontal_swing << 4);
if (model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
state[6] = 0x20;
if (vertical_swing == GREE_VDIR_SWING)
state[0] |= GREE_SWING_AUTO_MASK;
}
if ((model == GREE_YX1FF || model == GREE_YAC1FB9) && data.preset == climate::CLIMATE_PRESET_SLEEP)
state[0] |= GREE_PRESET_SLEEP_BIT;
if (data.fahrenheit) {
state[3] |= GREE_FAHRENHEIT_BIT;
if (data.half_degree)
state[3] |= GREE_TEMP_HALF_DEGREE_BIT;
}
state[GREE_STATE_FRAME_SIZE - 1] = GreeProtocol::calculate_checksum(state);
return state;
}
uint8_t GreeClimate::horizontal_swing_() {
switch (this->swing_mode) {
#ifdef USE_GREE_RECEIVER
optional<GreeClimateData> GreeClimateCodec::decode(Model model, const GreeState &state) {
if (model == GREE_YB1FA || model == GREE_YX1FF)
return GreeClimateCodec::decode_model_a(model, state);
return GreeClimateCodec::decode_legacy(model, state);
}
#endif // USE_GREE_RECEIVER
uint8_t GreeClimateCodec::encode_operation_mode(climate::ClimateMode mode) {
if (mode == climate::CLIMATE_MODE_COOL)
return GREE_MODE_ON | GREE_MODE_COOL;
if (mode == climate::CLIMATE_MODE_DRY)
return GREE_MODE_ON | GREE_MODE_DRY;
if (mode == climate::CLIMATE_MODE_HEAT)
return GREE_MODE_ON | GREE_MODE_HEAT;
if (mode == climate::CLIMATE_MODE_HEAT_COOL)
return GREE_MODE_ON | GREE_MODE_AUTO;
if (mode == climate::CLIMATE_MODE_FAN_ONLY)
return GREE_MODE_ON | GREE_MODE_FAN;
return GREE_MODE_OFF;
}
uint8_t GreeClimateCodec::encode_fan_mode(Model model, climate::ClimateFanMode fan_mode) {
if (model == GREE_YX1FF) {
if (fan_mode == climate::CLIMATE_FAN_QUIET)
return GREE_FAN_1;
if (fan_mode == climate::CLIMATE_FAN_LOW)
return GREE_FAN_2;
if (fan_mode == climate::CLIMATE_FAN_MEDIUM || fan_mode == climate::CLIMATE_FAN_HIGH)
return GREE_FAN_3;
return GREE_FAN_AUTO;
}
if (fan_mode == climate::CLIMATE_FAN_LOW)
return GREE_FAN_1;
if (fan_mode == climate::CLIMATE_FAN_MEDIUM)
return GREE_FAN_2;
if (fan_mode == climate::CLIMATE_FAN_HIGH)
return GREE_FAN_3;
return GREE_FAN_AUTO;
}
uint8_t GreeClimateCodec::encode_horizontal_swing(climate::ClimateSwingMode swing_mode) {
switch (swing_mode) {
case climate::CLIMATE_SWING_HORIZONTAL:
case climate::CLIMATE_SWING_BOTH:
return GREE_HDIR_SWING;
@@ -243,8 +341,8 @@ uint8_t GreeClimate::horizontal_swing_() {
}
}
uint8_t GreeClimate::vertical_swing_() {
switch (this->swing_mode) {
uint8_t GreeClimateCodec::encode_vertical_swing(climate::ClimateSwingMode swing_mode) {
switch (swing_mode) {
case climate::CLIMATE_SWING_VERTICAL:
case climate::CLIMATE_SWING_BOTH:
return GREE_VDIR_SWING;
@@ -253,24 +351,273 @@ uint8_t GreeClimate::vertical_swing_() {
}
}
uint8_t GreeClimate::temperature_() {
return (uint8_t) roundf(clamp<float>(this->target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX)) - GREE_TEMP_MIN;
}
#ifdef USE_GREE_RECEIVER
optional<GreeClimateData> GreeClimateCodec::decode_legacy(Model model, const GreeState &state) {
if (is_auxiliary_frame(model, state))
return {};
uint8_t GreeClimate::preset_() {
// YX1FF and YAC1FB9 have sleep preset
if (this->model_ == GREE_YX1FF || this->model_ == GREE_YAC1FB9) {
switch (this->preset.value_or(climate::CLIMATE_PRESET_NONE)) {
case climate::CLIMATE_PRESET_NONE:
return GREE_PRESET_NONE;
case climate::CLIMATE_PRESET_SLEEP:
return GREE_PRESET_SLEEP;
default:
return GREE_PRESET_NONE;
}
const uint8_t mode = state[0] & GREE_MODE_MASK;
const bool power = state[0] & GREE_POWER_MASK;
const uint8_t fan = state[0] & GREE_FAN_MASK;
const uint8_t temperature = state[1] & GREE_TEMP_MASK;
// Legacy GREE-branded remotes have several model and OEM variants. Decode the fields that are stable across the
// protocol, but do not require the transmitter-side fixed bytes: physical Tadiran/Tosot/GREE remotes may populate
// timer, display-temperature, I-Feel, WiFi, vane-position, or otherwise unused fields differently.
if (mode > GREE_MODE_HEAT || temperature > GREE_TEMP_MAX - GREE_TEMP_MIN)
return {};
const uint8_t vertical = state[4] & 0x0F;
const uint8_t horizontal = (state[4] >> 4) & 0x07;
bool swing_vertical = false;
bool swing_horizontal = false;
switch (model) {
case GREE_YAG:
case GREE_YAW1F:
case GREE_YAC:
// On YAC1FB bit 6 does not reliably identify vertical swing; use vane fields for both axes.
swing_vertical = is_valid_vertical_swing(true, vertical);
swing_horizontal = horizontal == GREE_HDIR_SWING;
break;
case GREE_GENERIC:
case GREE_YAN:
case GREE_YAA:
case GREE_YAC1FB9:
default:
// ESPHome's YAN transmitter stores automatic vertical swing in byte 4, while YAA/YAC1FB9 use bit 6.
// Accept either representation so branded remotes using the standard GREE layout are handled as well.
swing_vertical = (state[0] & GREE_SWING_AUTO_MASK) || is_valid_vertical_swing(true, vertical);
break;
}
return GREE_PRESET_NONE;
GreeClimateData data{
.mode = climate::CLIMATE_MODE_OFF,
.target_temperature = static_cast<uint8_t>(GREE_TEMP_MIN + temperature),
.fan_mode = climate::CLIMATE_FAN_AUTO,
.swing_mode = climate::CLIMATE_SWING_OFF,
.preset = model == GREE_YAC1FB9 && (state[0] & GREE_SLEEP_MASK) ? climate::CLIMATE_PRESET_SLEEP
: climate::CLIMATE_PRESET_NONE,
.feature_bits = static_cast<uint8_t>(state[2] & supported_feature_mask(model)),
.fahrenheit = static_cast<bool>(state[3] & GREE_FAHRENHEIT_BIT),
.half_degree = static_cast<bool>(state[3] & GREE_TEMP_HALF_DEGREE_BIT),
};
if (swing_vertical && swing_horizontal) {
data.swing_mode = climate::CLIMATE_SWING_BOTH;
} else if (swing_vertical) {
data.swing_mode = climate::CLIMATE_SWING_VERTICAL;
} else if (swing_horizontal) {
data.swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
}
if (power)
data.mode = decode_operation_mode(mode);
data.fan_mode = decode_fan_mode(model, fan);
return data;
}
optional<GreeClimateData> GreeClimateCodec::decode_model_a(Model model, const GreeState &state) {
const uint8_t mode = state[0] & GREE_MODE_MASK;
const bool power = state[0] & GREE_POWER_MASK;
const uint8_t fan = state[0] & GREE_FAN_MASK;
const bool swing = state[0] & GREE_SWING_AUTO_MASK;
const bool turbo = state[2] & GREE_FAN_TURBO_BIT;
const uint8_t temperature = state[1] & GREE_TEMP_MASK;
// Validate fields that are not represented by Climate where possible. Supported independent feature state is
// returned separately so the parent can synchronize its feature switches.
if (mode > GREE_MODE_HEAT || temperature > GREE_TEMP_MAX - GREE_TEMP_MIN || !is_valid_timer(state) ||
static_cast<bool>(state[2] & GREE_MODEL_A_BIT) != power ||
(state[3] & ~(GREE_FAHRENHEIT_BIT | GREE_TEMP_HALF_DEGREE_BIT)) != 0x50 || !is_valid_swing(model, state) ||
(state[5] & GREE_BYTE5_FIXED_MASK) != GREE_BYTE5_FIXED_VALUE || state[6] != 0x00 || (state[7] & 0x0F) != 0x00 ||
(model == GREE_YX1FF && turbo && fan != GREE_FAN_3)) {
return {};
}
GreeClimateData data{
.mode = climate::CLIMATE_MODE_OFF,
.target_temperature = static_cast<uint8_t>(GREE_TEMP_MIN + temperature),
.fan_mode = climate::CLIMATE_FAN_AUTO,
.swing_mode = swing ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF,
.preset = model == GREE_YX1FF && state[0] & GREE_SLEEP_MASK ? climate::CLIMATE_PRESET_SLEEP
: climate::CLIMATE_PRESET_NONE,
.feature_bits = static_cast<uint8_t>(state[2] & supported_feature_mask(model)),
.fahrenheit = static_cast<bool>(state[3] & GREE_FAHRENHEIT_BIT),
.half_degree = static_cast<bool>(state[3] & GREE_TEMP_HALF_DEGREE_BIT),
};
if (power)
data.mode = decode_operation_mode(mode);
data.fan_mode = turbo ? climate::CLIMATE_FAN_HIGH : decode_fan_mode(model, fan);
return data;
}
#endif // USE_GREE_RECEIVER
void GreeClimate::set_model(Model model) {
if (model == GREE_YAN || model == GREE_YB1FA || model == GREE_YX1FF) {
// These remotes only expose a vertical swing control.
this->swing_modes_.erase(climate::CLIMATE_SWING_HORIZONTAL);
this->swing_modes_.erase(climate::CLIMATE_SWING_BOTH);
}
if (model == GREE_YX1FF)
this->fan_modes_.insert(climate::CLIMATE_FAN_QUIET);
if (model == GREE_YX1FF || model == GREE_YAC1FB9) {
this->presets_.insert(climate::CLIMATE_PRESET_NONE);
this->presets_.insert(climate::CLIMATE_PRESET_SLEEP);
}
this->model_ = model;
this->feature_bits_ = default_feature_bits(model);
}
void GreeClimate::set_feature_state(GreeFeature feature, bool enabled) {
const uint8_t bit = gree_feature_bit(feature);
if (!(supported_feature_mask(this->model_) & bit)) {
ESP_LOGW(TAG, "GREE model %u does not support feature %u", static_cast<unsigned>(this->model_),
static_cast<unsigned>(feature));
#ifdef USE_GREE_FEATURE_SWITCH
this->publish_feature_state_(feature);
#endif
return;
}
// YB1FA reports Turbo as effective HIGH fan. Keep the Climate and feature entities consistent when Turbo is enabled.
const bool fan_changed = this->model_ == GREE_YB1FA && feature == GREE_FEATURE_TURBO && enabled &&
this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO) != climate::CLIMATE_FAN_HIGH;
if (fan_changed)
this->fan_mode = climate::CLIMATE_FAN_HIGH;
assign_bits(this->feature_bits_, bit, enabled);
this->transmit_state();
#ifdef USE_GREE_FEATURE_SWITCH
this->publish_feature_state_(feature);
#endif
if (fan_changed)
this->publish_state();
}
bool GreeClimate::get_feature_state(GreeFeature feature) const {
return this->feature_bits_ & gree_feature_bit(feature);
}
#ifdef USE_GREE_FEATURE_SWITCH
void GreeClimate::setup() {
climate_ir::ClimateIR::setup();
// Apply every restored switch, then send one frame. Models that receive feature state have restoring disabled,
// so their switches show the current feature bits instead.
bool restored = false;
for (size_t feature = 0; feature < this->feature_switches_.size(); feature++) {
auto *feature_switch = this->feature_switches_[feature];
if (feature_switch == nullptr)
continue;
if (auto initial = feature_switch->get_initial_state_with_restore_mode()) {
// What control() would hand the switch's apply body
assign_bits(this->feature_bits_, gree_feature_bit(static_cast<GreeFeature>(feature)),
*initial != feature_switch->is_inverted());
restored = true;
}
this->publish_feature_state_(static_cast<GreeFeature>(feature));
}
if (restored)
this->transmit_state();
}
void GreeClimate::register_feature_switch(GreeFeature feature, switch_::Switch *feature_switch) {
this->feature_switches_[static_cast<uint8_t>(feature)] = feature_switch;
}
void GreeClimate::publish_feature_state_(GreeFeature feature) {
auto *feature_switch = this->feature_switches_[static_cast<uint8_t>(feature)];
if (feature_switch != nullptr)
feature_switch->publish_state(this->get_feature_state(feature));
}
#ifdef USE_GREE_RECEIVER
void GreeClimate::publish_feature_states_() {
for (size_t feature = 0; feature < this->feature_switches_.size(); feature++)
this->publish_feature_state_(static_cast<GreeFeature>(feature));
}
#endif
#endif // USE_GREE_FEATURE_SWITCH
void GreeClimate::control(const climate::ClimateCall &call) {
// An explicit fan selection exits YB1FA Turbo. Disabling Turbo itself leaves the effective HIGH fan selected.
const bool clear_turbo =
this->model_ == GREE_YB1FA && call.get_fan_mode().has_value() && this->get_feature_state(GREE_FEATURE_TURBO);
if (clear_turbo) {
this->feature_bits_ &= ~GREE_MODE_BIT_TURBO;
#ifdef USE_GREE_FEATURE_SWITCH
this->publish_feature_state_(GREE_FEATURE_TURBO);
#endif
}
climate_ir::ClimateIR::control(call);
}
void GreeClimate::transmit_state() {
GreeClimateData climate_data{
.mode = this->mode,
.target_temperature =
static_cast<uint8_t>(roundf(clamp<float>(this->target_temperature, GREE_TEMP_MIN, GREE_TEMP_MAX))),
.fan_mode = this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO),
.swing_mode = this->swing_mode,
.preset = this->preset.value_or(climate::CLIMATE_PRESET_NONE),
.feature_bits = this->feature_bits_,
};
#ifdef USE_GREE_FAHRENHEIT
if (this->use_fahrenheit_) {
const uint8_t half_steps = fahrenheit_half_steps(this->target_temperature);
climate_data.target_temperature = GREE_TEMP_MIN + (half_steps >> 1);
climate_data.fahrenheit = true;
climate_data.half_degree = half_steps & 1;
}
#endif
const GreeState state = GreeClimateCodec::encode(this->model_, climate_data);
auto transmit = this->transmitter_->transmit();
GreeProtocol(this->model_).encode(transmit.get_data(), state);
transmit.perform();
}
#ifdef USE_GREE_RECEIVER
bool GreeClimate::on_receive(remote_base::RemoteReceiveData data) {
auto state = GreeProtocol(this->model_).decode(data);
if (!state.has_value())
return false;
auto decoded = GreeClimateCodec::decode(this->model_, *state);
if (!decoded.has_value()) {
ESP_LOGD(TAG, "GREE frame rejected for model %u: %02X %02X %02X %02X %02X %02X %02X %02X",
static_cast<unsigned>(this->model_), (*state)[0], (*state)[1], (*state)[2], (*state)[3], (*state)[4],
(*state)[5], (*state)[6], (*state)[7]);
return false;
}
this->mode = decoded->mode;
const int half_steps = 2 * (decoded->target_temperature - GREE_TEMP_MIN) + decoded->half_degree;
float target = GREE_TEMP_MIN + 0.5f * half_steps;
// The panel shows (10 * 32 + 18 * GREE_TEMP_MIN + 9 * k) / 10 F; store that degree so it is sent back unchanged
if (decoded->fahrenheit) {
// The remote truncates the displayed Fahrenheit degree; retain that exact integer
// through subsequent feature-only and fan-only transmissions.
const int fahrenheit_degree = (10 * 32 + 18 * GREE_TEMP_MIN + 9 * half_steps) / 10;
target = fahrenheit_to_celsius(static_cast<float>(fahrenheit_degree));
}
this->target_temperature = target;
this->fan_mode = decoded->fan_mode;
this->swing_mode = decoded->swing_mode;
this->preset = decoded->preset;
this->feature_bits_ = decoded->feature_bits;
#ifdef USE_GREE_FEATURE_SWITCH
this->publish_feature_states_();
#endif
this->publish_state();
return true;
}
#endif // USE_GREE_RECEIVER
} // namespace esphome::gree
+113 -35
View File
@@ -1,7 +1,12 @@
#pragma once
#include <array>
#include "esphome/components/climate_ir/climate_ir.h"
#include "esphome/core/defines.h"
#ifdef USE_GREE_FEATURE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
namespace esphome::gree {
@@ -40,18 +45,15 @@ static constexpr uint32_t GREE_YAC_HEADER_MARK = 6000;
static constexpr uint32_t GREE_YAC_HEADER_SPACE = 3000;
static constexpr uint32_t GREE_YAC_BIT_MARK = 650;
// Timing specific to YAC1FB9
// Also used by YAW1F
// YAW1F shares the longer YAC1FB9 header and inter-block gap.
static constexpr uint32_t GREE_YAC1FB9_HEADER_SPACE = 4500;
static constexpr uint32_t GREE_YAC1FB9_MESSAGE_SPACE = 19980;
// YAW1F: every command is followed by a second 8-byte message after this gap
// Gap between the YAW1F primary command and its fixed second message.
static constexpr uint32_t GREE_YAW1F_FOLLOWUP_SPACE = 40000;
// State Frame size
static constexpr uint8_t GREE_STATE_FRAME_SIZE = 8;
// Only available on YAN
// Vertical air directions. Note that these cannot be set on all heat pumps
static constexpr uint8_t GREE_VDIR_AUTO = 0x00;
static constexpr uint8_t GREE_VDIR_MANUAL = 0x00;
@@ -63,6 +65,9 @@ static constexpr uint8_t GREE_VDIR_MDOWN = 0x05;
static constexpr uint8_t GREE_VDIR_DOWN = 0x06;
// YX1FF vane-position byte while swinging
static constexpr uint8_t GREE_YX1FF_VANE_SWING = 0x11;
static constexpr uint8_t GREE_VDIR_SWING_DOWN = 0x07;
static constexpr uint8_t GREE_VDIR_SWING_MIDDLE = 0x09;
static constexpr uint8_t GREE_VDIR_SWING_UP = 0x0B;
// Only available on YAC/YAG
// Horizontal air directions. Note that these cannot be set on all heat pumps
@@ -75,17 +80,7 @@ static constexpr uint8_t GREE_HDIR_MIDDLE = 0x04;
static constexpr uint8_t GREE_HDIR_MRIGHT = 0x05;
static constexpr uint8_t GREE_HDIR_RIGHT = 0x06;
// Only available on YX1FF
// Turbo (high) fan mode
static constexpr uint8_t GREE_FAN_TURBO_BIT = 0x10;
// Available on YX1FF and YAC1FB9
// Sleep preset mode
static constexpr uint8_t GREE_PRESET_NONE = 0x00;
static constexpr uint8_t GREE_PRESET_SLEEP = 0x01;
static constexpr uint8_t GREE_PRESET_SLEEP_BIT = 0x80;
// Mode bits stored in remote_state[2] bits 4..7 for YAN/YAA/YAC/YAC1FB9.
// Feature bits in byte 2
static constexpr uint8_t GREE_MODE_BIT_TURBO = 0x10;
static constexpr uint8_t GREE_MODE_BIT_LIGHT = 0x20;
static constexpr uint8_t GREE_MODE_BIT_HEALTH = 0x40;
@@ -95,24 +90,94 @@ static constexpr uint8_t GREE_MODE_BIT_XFAN = 0x80;
static constexpr uint8_t GREE_FAHRENHEIT_BIT = 0x08;
static constexpr uint8_t GREE_TEMP_HALF_DEGREE_BIT = 0x04; // Half a degree Celsius added to the temperature
/// Checksum of a state frame: the low nibbles of bytes 0-3 plus the high nibbles of bytes 4-6 plus 0x0A, in the high
/// nibble.
inline uint8_t frame_checksum(const uint8_t *remote_state) {
return (
(((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
0x0F)
<< 4);
}
#ifdef USE_GREE_FAHRENHEIT
/// Return k = 2 * temp_field + half_degree_bit (0..28) for a Celsius target. The unit displays
/// trunc(celsius_to_fahrenheit(GREE_TEMP_MIN + k / 2)), so k is the smallest value that shows the target in F.
uint8_t fahrenheit_half_steps(float target_celsius);
#endif
// Model-specific meanings for the same bit positions on YB1FA/YX1FF.
static constexpr uint8_t GREE_FAN_TURBO_BIT = 0x10;
static constexpr uint8_t GREE_MODEL_A_BIT = 0x40;
// Only available on YX1FF
// Sleep preset mode
static constexpr uint8_t GREE_PRESET_NONE = 0x00;
static constexpr uint8_t GREE_PRESET_SLEEP = 0x01;
static constexpr uint8_t GREE_PRESET_SLEEP_BIT = 0x80;
// Model codes
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG, GREE_YAW1F };
enum Model : uint8_t {
GREE_GENERIC,
GREE_YAN,
GREE_YAA,
GREE_YAC,
GREE_YAC1FB9,
GREE_YB1FA,
GREE_YX1FF,
GREE_YAG,
GREE_YAW1F
};
enum GreeFeature : uint8_t {
GREE_FEATURE_TURBO = 0,
GREE_FEATURE_LIGHT = 1,
GREE_FEATURE_HEALTH = 2,
GREE_FEATURE_XFAN = 3,
};
static constexpr uint8_t gree_feature_bit(GreeFeature feature) { return GREE_MODE_BIT_TURBO << feature; }
using GreeState = std::array<uint8_t, GREE_STATE_FRAME_SIZE>;
struct GreeClimateData {
climate::ClimateMode mode;
uint8_t target_temperature;
climate::ClimateFanMode fan_mode;
climate::ClimateSwingMode swing_mode;
climate::ClimatePreset preset;
uint8_t feature_bits{0};
// The unit's panel shows Fahrenheit; half_degree adds half a degree Celsius to target_temperature
bool fahrenheit{false};
bool half_degree{false};
};
class GreeProtocol {
public:
explicit GreeProtocol(Model model) : model_(model) {}
void encode(remote_base::RemoteTransmitData *data, const GreeState &state) const;
#ifdef USE_GREE_RECEIVER
optional<GreeState> decode(remote_base::RemoteReceiveData data) const;
#endif
static uint8_t calculate_checksum(const GreeState &state);
#ifdef USE_GREE_RECEIVER
static bool valid_checksum(const GreeState &state);
#endif
protected:
Model model_;
};
class GreeClimateCodec {
public:
static GreeState encode(Model model, const GreeClimateData &data);
#ifdef USE_GREE_RECEIVER
/// Decode a frame whose checksum the protocol layer already checked.
static optional<GreeClimateData> decode(Model model, const GreeState &state);
#endif
protected:
static uint8_t encode_operation_mode(climate::ClimateMode mode);
static uint8_t encode_fan_mode(Model model, climate::ClimateFanMode fan_mode);
static uint8_t encode_horizontal_swing(climate::ClimateSwingMode swing_mode);
static uint8_t encode_vertical_swing(climate::ClimateSwingMode swing_mode);
#ifdef USE_GREE_RECEIVER
static optional<GreeClimateData> decode_legacy(Model model, const GreeState &state);
static optional<GreeClimateData> decode_model_a(Model model, const GreeState &state);
#endif
};
class GreeClimate final : public climate_ir::ClimateIR {
public:
@@ -123,25 +188,38 @@ class GreeClimate final : public climate_ir::ClimateIR {
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
#ifdef USE_GREE_FEATURE_SWITCH
void setup() override;
#endif
void set_model(Model model);
#ifdef USE_GREE_FAHRENHEIT
void set_use_fahrenheit(bool use_fahrenheit) { this->use_fahrenheit_ = use_fahrenheit; }
#endif
void set_mode_bit(uint8_t bit_mask, bool enabled);
void set_feature_state(GreeFeature feature, bool enabled);
bool get_feature_state(GreeFeature feature) const;
#ifdef USE_GREE_FEATURE_SWITCH
void register_feature_switch(GreeFeature feature, switch_::Switch *feature_switch);
#endif
protected:
void control(const climate::ClimateCall &call) override;
// Transmit via IR the state of this climate controller.
void transmit_state() override;
#ifdef USE_GREE_RECEIVER
bool on_receive(remote_base::RemoteReceiveData data) override;
#endif
uint8_t operation_mode_();
uint8_t fan_speed_();
uint8_t horizontal_swing_();
uint8_t vertical_swing_();
uint8_t temperature_();
uint8_t preset_();
#ifdef USE_GREE_FEATURE_SWITCH
void publish_feature_state_(GreeFeature feature);
#ifdef USE_GREE_RECEIVER
void publish_feature_states_();
#endif
std::array<switch_::Switch *, 4> feature_switches_{};
#endif
Model model_{};
uint8_t mode_bits_{GREE_MODE_BIT_LIGHT}; // The display light is on until a switch says otherwise
uint8_t feature_bits_{0};
#ifdef USE_GREE_FAHRENHEIT
bool use_fahrenheit_{false};
#endif
+144 -37
View File
@@ -1,7 +1,17 @@
from collections.abc import Callable
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_LIGHT, DEVICE_CLASS_SWITCH, ENTITY_CATEGORY_CONFIG
from esphome.const import (
CONF_INITIAL_STATE,
CONF_LIGHT,
CONF_RESTORE_MODE,
CONF_RESTORE_STATE,
DEVICE_CLASS_SWITCH,
ENTITY_CATEGORY_CONFIG,
)
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -10,77 +20,174 @@ from ..climate import CONF_MODEL, GreeClimate
CODEOWNERS = ["@nagyrobi"]
GreeModeBitSwitch = gree_ns.class_("GreeModeBitSwitch", switch.Switch, cg.Component)
GreeFeature = gree_ns.enum("GreeFeature")
CONF_TURBO = "turbo"
CONF_HEALTH = "health"
CONF_XFAN = "xfan"
CONF_GREE_ID = "gree_id"
# Switch configurations: (config_key, display_name, mode_bit, icon)
# Switch configurations: (config_key, feature, icon, default_restore_mode)
SWITCH_CONFIGS = (
(
CONF_TURBO,
"Gree Turbo Switch",
gree_ns.GREE_MODE_BIT_TURBO,
GreeFeature.GREE_FEATURE_TURBO,
"mdi:car-turbocharger",
"RESTORE_DEFAULT_OFF",
),
# The display light was on before the switches existed, so it stays on by default
(
CONF_LIGHT,
GreeFeature.GREE_FEATURE_LIGHT,
"mdi:led-outline",
"RESTORE_DEFAULT_ON",
),
(
CONF_HEALTH,
GreeFeature.GREE_FEATURE_HEALTH,
"mdi:pine-tree",
"RESTORE_DEFAULT_OFF",
),
(CONF_LIGHT, "Gree Light Switch", gree_ns.GREE_MODE_BIT_LIGHT, "mdi:led-outline"),
(CONF_HEALTH, "Gree Health Switch", gree_ns.GREE_MODE_BIT_HEALTH, "mdi:pine-tree"),
(
CONF_XFAN,
"Gree X-FAN Switch",
gree_ns.GREE_MODE_BIT_XFAN,
GreeFeature.GREE_FEATURE_XFAN,
"mdi:wall-sconce-flat",
"RESTORE_DEFAULT_OFF",
),
)
SUPPORTED_MODELS = {
"yan",
"yaa",
"yac",
"yac1fb9",
ALL_FEATURES = frozenset(key for key, *_ in SWITCH_CONFIGS)
MODEL_FEATURES = {
"yan": ALL_FEATURES,
"yaa": ALL_FEATURES,
"yac": ALL_FEATURES,
"yac1fb9": ALL_FEATURES,
"yb1fa": ALL_FEATURES - {CONF_HEALTH},
"yx1ff": frozenset({CONF_LIGHT}),
}
# These models receive feature state over IR, so the switches follow it instead of restoring
RX_FEATURE_MODELS = frozenset({"yb1fa", "yx1ff"})
_RESTORE_KEYS = (CONF_RESTORE_MODE, CONF_RESTORE_STATE, CONF_INITIAL_STATE)
def _feature_switch_schema(
icon: str, default_restore_mode: str
) -> Callable[[ConfigType], ConfigType]:
schema = switch.switch_schema(
switch.ApplySwitch,
icon=icon,
default_restore_mode=default_restore_mode,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
)
def validator(value: ConfigType) -> ConfigType:
explicit = isinstance(value, dict) and any(
key in value for key in _RESTORE_KEYS
)
config = schema(value)
# Leave an omitted restore mode unset, so final validation can tell it from an explicit one
# and to_code can fill in the default for the model.
if not explicit:
config.pop(CONF_RESTORE_MODE, None)
return config
return validator
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_GREE_ID): cv.use_id(GreeClimate),
**{
cv.Optional(key): switch.switch_schema(
GreeModeBitSwitch,
icon=icon,
# The display light was on before the switches existed, so it stays on by default
default_restore_mode="RESTORE_DEFAULT_ON"
if key == CONF_LIGHT
else "RESTORE_DEFAULT_OFF",
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
)
for key, _, _, icon in SWITCH_CONFIGS
cv.Optional(key): _feature_switch_schema(icon, default_restore_mode)
for key, _, icon, default_restore_mode in SWITCH_CONFIGS
},
}
)
def _validate_model(config: ConfigType) -> None:
climate_conf = fv.get_declaring_config(config[CONF_GREE_ID])
if climate_conf[CONF_MODEL] not in SUPPORTED_MODELS:
def _validate_features_for_model(model: str, config: ConfigType) -> None:
supported_features = MODEL_FEATURES.get(model)
if supported_features is None:
raise cv.Invalid(
"Gree switches are only supported for the "
+ ", ".join(SUPPORTED_MODELS)
+ ", ".join(sorted(MODEL_FEATURES))
+ " models"
)
configured_features = {key for key, *_ in SWITCH_CONFIGS if key in config}
if unsupported := configured_features - supported_features:
suffix = "switch" if len(unsupported) == 1 else "switches"
raise cv.Invalid(
f"Gree model {model} does not support the "
+ ", ".join(sorted(unsupported))
+ f" {suffix}"
)
FINAL_VALIDATE_SCHEMA = _validate_model
def _validate_model(config: ConfigType) -> None:
model = fv.get_declaring_config(config[CONF_GREE_ID])[CONF_MODEL]
_validate_features_for_model(model, config)
if model not in RX_FEATURE_MODELS:
return
for feature, *_ in SWITCH_CONFIGS:
if (feature_conf := config.get(feature)) is None:
continue
if (
CONF_RESTORE_STATE in feature_conf
or str(feature_conf.get(CONF_RESTORE_MODE, "DISABLED")) != "DISABLED"
):
raise cv.Invalid(
f"Gree model {model} receives {feature} state from IR; "
"restore_mode must be DISABLED or omitted",
path=[feature],
)
# The climate keeps one switch per feature, so a second entry would take over the first
_validate_unique = fv.unique_platform_key(
domain=switch.DOMAIN,
platform="gree",
keys=(CONF_GREE_ID,),
message="Only one gree switch entry is allowed per climate ('{gree_id}')",
)
def _final_validate(config: ConfigType) -> None:
_validate_unique(config)
_validate_model(config)
FINAL_VALIDATE_SCHEMA = _final_validate
def _with_default_restore_mode(
feature_conf: ConfigType, default_restore_mode: str, receives_state: bool
) -> ConfigType:
"""Fill in the model's restore mode when none was configured."""
if CONF_RESTORE_MODE in feature_conf or CONF_RESTORE_STATE in feature_conf:
return feature_conf
mode = "DISABLED" if receives_state else default_restore_mode
return {**feature_conf, CONF_RESTORE_MODE: switch.RESTORE_MODES[mode]}
async def to_code(config: ConfigType) -> None:
parent = await cg.get_variable(config[CONF_GREE_ID])
hub = await cg.get_variable(config[CONF_GREE_ID])
receives_state = (
fv.get_declaring_config(config[CONF_GREE_ID], CORE.config)[CONF_MODEL]
in RX_FEATURE_MODELS
)
cg.add_define("USE_GREE_FEATURE_SWITCH")
for conf_key, name, bit_mask, _ in SWITCH_CONFIGS:
if switch_conf := config.get(conf_key):
sw = cg.new_Pvariable(switch_conf[cv.CONF_ID], name, bit_mask)
await switch.register_switch(sw, switch_conf)
await cg.register_component(sw, switch_conf)
await cg.register_parented(sw, parent)
switches = switch.sub_switches(config, parent=hub)
for conf_key, feature, _, default_restore_mode in SWITCH_CONFIGS:
if (feature_conf := config.get(conf_key)) is None:
continue
await switches.create(
_with_default_restore_mode(
feature_conf, default_restore_mode, receives_state
),
lambda sw, feature=feature: hub.register_feature_switch(feature, sw),
apply=f"set_feature_state({feature}, state)",
)
@@ -1,17 +0,0 @@
#include "gree_switch.h"
#include "esphome/core/log.h"
namespace esphome::gree {
ESPHOME_LOG_TAG(TAG, "gree.switch");
void GreeModeBitSwitch::setup() { this->restore(); }
void GreeModeBitSwitch::dump_config() { log_switch(TAG, " ", this->name_, this); }
void GreeModeBitSwitch::write_state(bool state) {
this->parent_->set_mode_bit(this->bit_mask_, state);
this->publish_state(state);
}
} // namespace esphome::gree
@@ -1,22 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/switch/switch.h"
#include "esphome/components/gree/gree.h"
namespace esphome::gree {
class GreeModeBitSwitch final : public switch_::Switch, public Component, public Parented<GreeClimate> {
public:
GreeModeBitSwitch(const char *name, uint8_t bit_mask) : name_(name), bit_mask_(bit_mask) {}
void setup() override;
void dump_config() override;
void write_state(bool state) override;
protected:
const char *name_;
uint8_t bit_mask_;
};
} // namespace esphome::gree
+3 -2
View File
@@ -44,6 +44,7 @@ from .const import (
FAMILY_BK7238,
FAMILY_COMPONENT,
FAMILY_FRIENDLY,
FAMILY_LN882H,
FAMILY_RTL8710B,
FAMILY_RTL8720C,
KEY_BOARD,
@@ -60,8 +61,8 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
# Fault handlers the crash handler wraps (BK7238, BK7231N)
BK_TRAP_HANDLERS = ("bk_trap_udef", "bk_trap_pabt", "bk_trap_dabt")
# Families with a crash handler; the RTL8720C installs it at run time (no wraps).
CRASH_HANDLER_FAMILIES = (FAMILY_BK7231N, FAMILY_BK7238, FAMILY_RTL8720C)
# Families with a crash handler; the RTL8720C and LN882H install it at run time (no wraps).
CRASH_HANDLER_FAMILIES = (FAMILY_BK7231N, FAMILY_BK7238, FAMILY_LN882H, FAMILY_RTL8720C)
CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["preferences"]
+68 -31
View File
@@ -3,11 +3,12 @@
#include "esphome/core/defines.h"
#ifdef USE_LIBRETINY_CRASH_HANDLER
#if !defined(USE_BK72XX) && !defined(USE_LIBRETINY_VARIANT_RTL8720C)
#include "crash_handler.h"
#if !defined(USE_BK72XX) && !defined(USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH)
#error "The LibreTiny crash handler has no fault capture for this chip family"
#endif
#include "crash_handler.h"
#include "core.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
@@ -58,6 +59,18 @@ extern char __ram_code_text_end__[];
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_LIBRETINY_VARIANT_RTL8720C
#ifdef USE_LN882X
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
// Bounds of the code in flash and of the IRAM_ATTR code (linker symbols; the latter from patch_linker.py).
extern char _stext[];
extern char _etext[];
extern char __esphome_sram_text_start[];
extern char __esphome_sram_text_end[];
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_LN882X
namespace esphome::libretiny {
ESPHOME_LOG_TAG(TAG, "libretiny.crash");
@@ -69,7 +82,7 @@ static constexpr uint32_t CRASH_MAGIC_V2 = 0xDEAD0002;
static constexpr uint32_t CRASH_REASON_UNDEFINED = 1;
static constexpr uint32_t CRASH_REASON_PREFETCH_ABORT = 2;
static constexpr uint32_t CRASH_REASON_DATA_ABORT = 3;
// RTL8720C: the Cortex-M exception number (3 to 7) plus this offset.
// RTL8720C and LN882H: the Cortex-M exception number (3 to 7) plus this offset.
static constexpr uint32_t CRASH_REASON_CORTEX_M_OFFSET = 1;
PROGMEM_STRING_TABLE(CrashReasonStrings, "Unknown", "Undefined instruction", "Prefetch abort", "Data abort",
"Hard fault", "MemManage fault", "Bus fault", "Usage fault", "Secure fault");
@@ -92,6 +105,9 @@ struct CrashRecord {
#ifdef USE_BK72XX
// Kept in ITCM, see patch_noinit.py.script.
#define CRASH_RECORD_SECTION ".noinit"
#elif defined(USE_LN882X)
// ln882hki_bsp.template.ld puts .noinit in RETENTION RAM, which startup does not clear.
#define CRASH_RECORD_SECTION ".noinit"
#else
// rtl8710c_ram.ld keeps .ram.bss.noinit* out of the startup zeroing.
#define CRASH_RECORD_SECTION ".ram.bss.noinit.esphome_crash"
@@ -146,6 +162,9 @@ static inline bool is_code_addr(uint32_t val) {
#ifdef USE_BK72XX
return (addr >= (uint32_t) _vector_start && addr < (uint32_t) _data_flash_begin) ||
(addr >= (uint32_t) _itcmcode_ram_begin && addr < (uint32_t) _itcmcode_ram_end);
#elif defined(USE_LN882X)
return (addr >= (uint32_t) _stext && addr < (uint32_t) _etext) ||
(addr >= (uint32_t) __esphome_sram_text_start && addr < (uint32_t) __esphome_sram_text_end);
#else
return (addr >= (uint32_t) __xip_code_text_start__ && addr < (uint32_t) __xip_code_text_end__) ||
(addr >= (uint32_t) __ram_entry_text_start__ && addr < (uint32_t) __ram_code_text_end__);
@@ -257,19 +276,29 @@ extern "C" void __wrap_bk_trap_dabt(arm_registers *regs) {
#endif // USE_BK72XX
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
// The SDK points the fault vectors (3 HardFault to 7 SecureFault) of its RAM vector table (VTOR) at a
// ROM handler that prints a register dump and then spins. crash_handler_install() puts
// esphome_fault_entry() in front of it, as the SDK's own CONFIG_MIIO patch (app_start.c) does.
// The SDK's RAM vector table (VTOR) points the fault vectors at a handler that prints a register dump and then
// spins: a ROM handler on the RTL8720C, CmBacktrace (hal_interrupt.c) on the LN882H. crash_handler_install()
// puts esphome_fault_entry() in front of it, as the RTL8720C SDK's own CONFIG_MIIO patch (app_start.c) does.
static_assert(LT_HW_WATCHDOG, "the crash reboot relies on the hardware watchdog");
// The ROM dump takes about 0.4 s at 115200 baud; without the re-arm the reset waits for the 10 s
// The RTL8720C ROM dump takes about 0.4 s at 115200 baud; without the re-arm the reset waits for the 10 s
// watchdog from arch_init().
static constexpr uint32_t CRASH_REBOOT_WDT_MS = 1000;
static constexpr uint32_t FIRST_FAULT_VECTOR = 3;
static constexpr uint32_t FAULT_VECTOR_COUNT = 5;
static constexpr uint32_t FIRST_FAULT_VECTOR = 3; // HardFault, then MemManage, BusFault, UsageFault
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
static constexpr uint32_t FAULT_VECTOR_COUNT = 5; // and SecureFault (ARMv8-M)
// DTCM, where the app's stacks and heap live (rtl8710c_ram.ld).
static constexpr uint32_t SRAM_START = 0x10000000;
static constexpr uint32_t SRAM_END = 0x1003FA00;
#else
static constexpr uint32_t FAULT_VECTOR_COUNT = 4;
// RAM0, where the stacks and heap live (ln882hki_bsp.template.ld).
static constexpr uint32_t SRAM_START = 0x20000000;
static constexpr uint32_t SRAM_END = 0x20049C00;
#endif
// Architectural System Control Block registers (ARMv8-M).
// Architectural System Control Block registers (ARMv7-M and ARMv8-M).
static constexpr uintptr_t SCB_VTOR = 0xE000ED08;
static constexpr uintptr_t SCB_CFSR = 0xE000ED28;
static constexpr uintptr_t SCB_MMFAR = 0xE000ED34;
@@ -277,26 +306,34 @@ static constexpr uintptr_t SCB_BFAR = 0xE000ED38;
static constexpr uint32_t CFSR_MMARVALID = 1u << 7;
static constexpr uint32_t CFSR_BFARVALID = 1u << 15;
// DTCM, where the app's stacks and heap live (rtl8710c_ram.ld).
static constexpr uint32_t SRAM_START = 0x10000000;
static constexpr uint32_t SRAM_END = 0x1003FA00;
// Exception frame: r0-r3, r12, lr, pc, xPSR (the RTL8720C has no FPU, so never the extended frame).
// Exception frame: r0-r3, r12, lr, pc, xPSR, then the FPU registers when EXC_RETURN says so (LN882H only).
static constexpr uint32_t FRAME_LR = 5;
static constexpr uint32_t FRAME_PC = 6;
static constexpr uint32_t FRAME_XPSR = 7;
static constexpr uint32_t FRAME_WORDS = 8;
static constexpr uint32_t EXTENDED_FRAME_WORDS = 26;
static constexpr uint32_t EXC_RETURN_STANDARD_FRAME = 1u << 4;
static constexpr uint32_t XPSR_STACK_ALIGNED = 1u << 9;
// The ROM handlers fault_entry() continues into, by vector.
static uint32_t s_rom_fault_vectors[FAULT_VECTOR_COUNT]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// The SDK handlers fault_entry() continues into, by vector.
static uint32_t s_sdk_fault_vectors[FAULT_VECTOR_COUNT]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_fault_recorded = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static inline uint32_t read_reg(uintptr_t addr) {
return *(const volatile uint32_t *) addr; // NOLINT(performance-no-int-to-ptr)
}
// Returns the ROM handler to continue into. pc, lr and sp stay 0 when the frame is outside DTCM.
extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *frame, uint32_t vector) {
// Returns the SDK handler to continue into. pc, lr and sp stay 0 when the frame is outside SRAM.
extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *frame, uint32_t vector,
uint32_t exc_return) {
// A later fault, in the recording below or in the SDK handler (CmBacktrace on the LN882H walks past the end
// of RAM), keeps the first record and waits for the watchdog instead of starting the dump again.
if (s_fault_recorded) {
while (true) {
}
}
s_fault_recorded = true;
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
uint32_t cfsr = read_reg(SCB_CFSR);
uint32_t address = 0;
if (cfsr & CFSR_BFARVALID) {
@@ -309,25 +346,24 @@ extern "C" __attribute__((used)) uint32_t esphome_record_fault(const uint32_t *f
s_record.fault_address = address;
auto frame_addr = (uint32_t) frame;
uint32_t frame_words = (exc_return & EXC_RETURN_STANDARD_FRAME) ? FRAME_WORDS : EXTENDED_FRAME_WORDS;
uint32_t sp = 0;
if (frame_addr >= SRAM_START && frame_addr <= SRAM_END - FRAME_WORDS * 4 && (frame_addr & 3) == 0) {
if (frame_addr >= SRAM_START && frame_addr <= SRAM_END - frame_words * 4 && (frame_addr & 3) == 0) {
s_record.pc = frame[FRAME_PC];
s_record.lr = frame[FRAME_LR];
sp = frame_addr + FRAME_WORDS * 4 + ((frame[FRAME_XPSR] & XPSR_STACK_ALIGNED) ? 4 : 0);
sp = frame_addr + frame_words * 4 + ((frame[FRAME_XPSR] & XPSR_STACK_ALIGNED) ? 4 : 0);
} else {
s_record.pc = 0;
s_record.lr = 0;
}
s_record.sp = sp;
finish_record(sp, SRAM_START, SRAM_END);
lt_wdt_enable(CRASH_REBOOT_WDT_MS);
return s_rom_fault_vectors[vector - FIRST_FAULT_VECTOR];
return s_sdk_fault_vectors[vector - FIRST_FAULT_VECTOR];
}
// Fault vector: calls esphome_record_fault(frame, exception number), then jumps to the ROM handler it returns
// with r0-r3 and lr restored. Asm because C would push onto MSP before the frame is found, and the ROM handler
// reads EXC_RETURN from lr.
// Fault vector: calls esphome_record_fault(frame, exception number, EXC_RETURN), then jumps to the SDK handler it
// returns with r0-r3 and lr restored. Asm because C would push onto MSP before the frame is found, and the SDK
// handler reads EXC_RETURN from lr.
extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
__asm volatile("push {r0-r3, r4, lr} \n" // r4 only keeps the stack 8-byte aligned
"tst lr, #4 \n"
@@ -338,8 +374,9 @@ extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
"addeq r0, r0, #24 \n" // MSP: skip the six words pushed above
"mrs r1, ipsr \n"
"ubfx r1, r1, #0, #9 \n" // exception number, 3 to 7
"mov r2, lr \n"
"bl esphome_record_fault \n"
"mov r12, r0 \n" // ROM handler address
"mov r12, r0 \n" // SDK handler address
"pop {r0-r3, r4, lr} \n"
"bx r12 \n");
}
@@ -347,12 +384,12 @@ extern "C" __attribute__((naked, used)) void esphome_fault_entry() {
void crash_handler_install() {
auto *vectors = (uint32_t *) read_reg(SCB_VTOR); // NOLINT(performance-no-int-to-ptr)
for (uint32_t i = 0; i < FAULT_VECTOR_COUNT; i++) {
s_rom_fault_vectors[i] = vectors[FIRST_FAULT_VECTOR + i];
s_sdk_fault_vectors[i] = vectors[FIRST_FAULT_VECTOR + i];
vectors[FIRST_FAULT_VECTOR + i] = (uint32_t) &esphome_fault_entry;
}
}
#endif // USE_LIBRETINY_VARIANT_RTL8720C
#endif // USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
} // namespace esphome::libretiny
+6 -1
View File
@@ -6,6 +6,11 @@
#ifdef USE_LIBRETINY_CRASH_HANDLER
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(USE_LN882X)
// Cortex-M families: the recorder goes in front of the SDK's fault handlers through the RAM vector table.
#define USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
#endif
namespace esphome::libretiny {
/// Log crash data if a crash was detected on previous boot.
@@ -14,7 +19,7 @@ void crash_handler_log();
/// Returns true if crash data was found this boot, reading it first if needed.
bool crash_handler_has_data();
#ifdef USE_LIBRETINY_VARIANT_RTL8720C
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
/// Put the crash recorder in front of the SDK's fault handlers.
void crash_handler_install();
#endif
@@ -156,6 +156,7 @@ COMPONENT_CODEOWNERS = {
# Extra imports per component; the CLI looks up process_stacktrace on the platform package.
COMPONENT_EXTRA_IMPORTS = {
"bk72xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
"ln882x": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
"rtl87xx": "from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401",
}
+1 -1
View File
@@ -28,7 +28,7 @@ namespace esphome {
void arch_init() {
libretiny::setup_preferences();
lt_wdt_enable(10000L);
#if defined(USE_LIBRETINY_CRASH_HANDLER) && defined(USE_LIBRETINY_VARIANT_RTL8720C)
#ifdef USE_LIBRETINY_CRASH_HANDLER_VECTOR_PATCH
libretiny::crash_handler_install();
#endif
#ifdef USE_BK72XX
+1
View File
@@ -29,6 +29,7 @@ from esphome.components.libretiny.const import (
)
from esphome.core import CORE
from esphome.cpp_generator import MockObj
from esphome.stacktrace import process_crash_handler_line as process_stacktrace # noqa: F401
from esphome.types import ConfigType
from .boards import LN882X_BOARD_PINS, LN882X_BOARDS
+3 -1
View File
@@ -102,6 +102,8 @@
#define USE_GRAPH
#define USE_GRAPHICAL_DISPLAY_MENU
#define USE_GREE_FAHRENHEIT
#define USE_GREE_FEATURE_SWITCH
#define USE_GREE_RECEIVER
#define USE_GREE_YAW1F
#define USE_HOERMANN_HCP_IDENTITY
#define USE_HOMEASSISTANT_TIME
@@ -779,7 +781,7 @@
#endif
#ifdef USE_LIBRETINY
#if defined(USE_BK72XX) || defined(USE_LIBRETINY_VARIANT_RTL8720C)
#if defined(USE_BK72XX) || defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(USE_LN882X)
#define USE_LIBRETINY_CRASH_HANDLER
#endif
#define USE_BK72XX_BLE
+1
View File
@@ -67,6 +67,7 @@ STACKTRACE_GATES: Final[dict[str, str]] = {
),
PLATFORM_RP2: _CRASH_HANDLER_GATE,
PLATFORM_BK72XX: _CRASH_HANDLER_GATE,
PLATFORM_LN882X: _CRASH_HANDLER_GATE,
PLATFORM_RTL87XX: _CRASH_HANDLER_GATE,
PLATFORM_NRF52: r"0x[0-9a-fA-F]{3,}\b|Last crash:",
}
+1 -1
View File
@@ -152,7 +152,7 @@ def _addr2line(tool: str, elf: Path, addr: str) -> str:
def process_crash_handler_line(
config: ConfigType, line: str, backtrace_state: bool
) -> bool:
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2, bk72xx and rtl87xx."""
"""Decode a crash handler report with addr2line; the process_stacktrace hook of rp2, bk72xx, rtl87xx and ln882x."""
if CRASH_HANDLER_RE.search(line):
_LOGGER.error("Crash detected - decoding addresses")
return True
@@ -0,0 +1,31 @@
esphome:
name: gree-test
esp32:
board: esp32dev
remote_transmitter:
id: xmitr
pin: GPIO4
carrier_duty_percent: 50%
climate:
- platform: gree
id: gree_yan
name: GREE YAN
model: yan
transmitter_id: xmitr
switch:
- platform: gree
gree_id: gree_yan
light:
id: yan_light
name: YAN Light
turbo:
id: yan_turbo
name: YAN Turbo
health:
id: yan_health
name: YAN Health
restore_mode: ALWAYS_ON
@@ -0,0 +1,29 @@
esphome:
name: gree-test
esp32:
board: esp32dev
remote_transmitter:
id: xmitr
pin: GPIO4
carrier_duty_percent: 50%
remote_receiver:
id: rcvr
pin: GPIO5
climate:
- platform: gree
id: gree_yx1ff
name: GREE YX1FF
model: yx1ff
transmitter_id: xmitr
receiver_id: rcvr
switch:
- platform: gree
gree_id: gree_yx1ff
light:
id: yx1ff_light
name: YX1FF Light
@@ -1,4 +1,4 @@
"""The YAW1F follow-up message is compiled in only for configs that use that model."""
"""GREE code paths are compiled in only for configs that use them."""
from collections.abc import Callable
from pathlib import Path
@@ -20,3 +20,26 @@ def test_other_models_leave_the_define_out(
) -> None:
generate_main(component_config_path("yan.yaml"))
assert "USE_GREE_YAW1F" not in {define.name for define in CORE.defines}
def test_receiver_and_switch_defines_follow_the_config(
generate_main: Callable[[Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("yx1ff_switch.yaml"))
defines = {define.name for define in CORE.defines}
assert {"USE_GREE_RECEIVER", "USE_GREE_FEATURE_SWITCH"} <= defines
# YX1FF receives the light state over IR, so the switch never restores its own
assert (
"yx1ff_light->set_restore_mode(switch_::SWITCH_RESTORE_DISABLED);" in main_cpp
)
def test_transmit_only_config_leaves_the_receiver_out(
generate_main: Callable[[Path], str],
component_config_path: Callable[[str], Path],
) -> None:
generate_main(component_config_path("yan.yaml"))
defines = {define.name for define in CORE.defines}
assert "USE_GREE_RECEIVER" not in defines
assert "USE_GREE_FEATURE_SWITCH" not in defines
+199
View File
@@ -0,0 +1,199 @@
"""Tests for GREE feature switch validation."""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome import config_validation as cv, final_validate as fv
from esphome.components import switch
from esphome.components.gree.switch import (
CONF_GREE_ID,
CONF_HEALTH,
CONF_TURBO,
CONF_XFAN,
_feature_switch_schema,
_validate_model,
_validate_unique,
)
from esphome.const import (
CONF_ID,
CONF_INITIAL_STATE,
CONF_LIGHT,
CONF_MODEL,
CONF_NAME,
CONF_RESTORE_MODE,
CONF_RESTORE_STATE,
CONF_STATE,
)
from esphome.core import ID
from esphome.types import ConfigType
CONF_CLIMATE = "climate"
RESTORE_MODE_VALIDATOR = cv.enum(switch.RESTORE_MODES, upper=True, space="_")
def _switch_config(model: str, features: ConfigType) -> ConfigType:
climate_id = ID("gree_climate", is_declaration=True)
full_config = fv.full_config.get()
full_config[CONF_CLIMATE] = [{CONF_ID: climate_id, CONF_MODEL: model}]
full_config.declare_ids.append((climate_id, [CONF_CLIMATE, 0, CONF_ID]))
return {CONF_GREE_ID: ID("gree_climate", is_declaration=False), **features}
def _validate_switch_config(
model: str, features: set[str], restore_modes: dict[str, str] | None = None
) -> ConfigType:
config = _switch_config(
model,
{
feature: (
{CONF_RESTORE_MODE: RESTORE_MODE_VALIDATOR(restore_modes[feature])}
if restore_modes and feature in restore_modes
else {}
)
for feature in features
},
)
_validate_model(config)
return config
@pytest.mark.parametrize(
("model", "feature"),
[
pytest.param("yx1ff", CONF_TURBO, id="yx1ff-turbo"),
pytest.param("yx1ff", CONF_HEALTH, id="yx1ff-health"),
pytest.param("yb1fa", CONF_HEALTH, id="yb1fa-health"),
pytest.param("yx1ff", CONF_XFAN, id="yx1ff-xfan"),
],
)
def test_unsupported_feature_is_rejected(model: str, feature: str) -> None:
with pytest.raises(
cv.Invalid,
match=rf"Gree model {model} does not support the {feature} switch",
):
_validate_switch_config(model, {feature})
@pytest.mark.parametrize(
("model", "features"),
[
pytest.param("yx1ff", {CONF_LIGHT}, id="yx1ff-light"),
pytest.param(
"yb1fa",
{CONF_TURBO, CONF_LIGHT, CONF_XFAN},
id="yb1fa-all-supported",
),
pytest.param(
"yan",
{CONF_TURBO, CONF_LIGHT, CONF_HEALTH, CONF_XFAN},
id="legacy-all-supported",
),
],
)
def test_supported_features_are_accepted(model: str, features: set[str]) -> None:
_validate_switch_config(model, features)
@pytest.mark.parametrize(
("raw", "expected"),
[
pytest.param({}, None, id="omitted"),
pytest.param(
{CONF_RESTORE_MODE: "RESTORE_DEFAULT_ON"},
"RESTORE_DEFAULT_ON",
id="explicit-schema-default",
),
pytest.param({CONF_RESTORE_MODE: "DISABLED"}, "DISABLED", id="explicit"),
],
)
def test_schema_keeps_only_explicit_restore_modes(
raw: dict[str, str], expected: str | None
) -> None:
validator = _feature_switch_schema("mdi:led-outline", "RESTORE_DEFAULT_ON")
config = validator({CONF_NAME: "Light", **raw})
if expected is None:
assert CONF_RESTORE_MODE not in config
else:
assert str(config[CONF_RESTORE_MODE]) == expected
def test_schema_initial_state_keeps_the_feature_default() -> None:
validator = _feature_switch_schema("mdi:led-outline", "RESTORE_DEFAULT_ON")
config = validator({CONF_NAME: "Light", CONF_INITIAL_STATE: False})
assert CONF_RESTORE_MODE not in config
assert config[CONF_RESTORE_STATE][CONF_STATE] == switch.RESTORE_STATE_KEEP
@pytest.mark.parametrize("model", ["yb1fa", "yx1ff"])
def test_rx_capable_switches_accept_omitted_or_disabled(model: str) -> None:
_validate_switch_config(model, {CONF_LIGHT})
_validate_switch_config(model, {CONF_LIGHT}, {CONF_LIGHT: "DISABLED"})
@pytest.mark.parametrize(
("model", "feature", "restore_mode"),
[
("yx1ff", CONF_LIGHT, "RESTORE_DEFAULT_ON"),
("yx1ff", CONF_LIGHT, "ALWAYS_ON"),
("yx1ff", CONF_LIGHT, "RESTORE_DEFAULT_OFF"),
("yb1fa", CONF_TURBO, "RESTORE_DEFAULT_OFF"),
("yb1fa", CONF_TURBO, "ALWAYS_OFF"),
("yb1fa", CONF_LIGHT, "ALWAYS_ON"),
],
)
def test_rx_capable_switches_reject_explicit_restore_modes(
model: str, feature: str, restore_mode: str
) -> None:
with pytest.raises(cv.Invalid, match="restore_mode must be DISABLED or omitted"):
_validate_switch_config(model, {feature}, {feature: restore_mode})
def test_rx_capable_switches_reject_restore_state() -> None:
with pytest.raises(cv.Invalid, match="restore_mode must be DISABLED or omitted"):
_validate_model(_switch_config("yx1ff", {CONF_LIGHT: {CONF_RESTORE_STATE: {}}}))
def test_legacy_switches_keep_custom_restore_modes() -> None:
config = _validate_switch_config("yan", {CONF_LIGHT}, {CONF_LIGHT: "ALWAYS_OFF"})
assert str(config[CONF_LIGHT][CONF_RESTORE_MODE]) == "ALWAYS_OFF"
def test_omitted_restore_mode_follows_the_model(
generate_main: Callable[[Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("yan_switch.yaml"))
assert (
"yan_light->set_restore_mode(switch_::SWITCH_RESTORE_DEFAULT_ON);" in main_cpp
)
assert (
"yan_turbo->set_restore_mode(switch_::SWITCH_RESTORE_DEFAULT_OFF);" in main_cpp
)
assert "yan_health->set_restore_mode(switch_::SWITCH_ALWAYS_ON);" in main_cpp
assert (
"gree_yan->register_feature_switch(gree::GREE_FEATURE_LIGHT, yan_light);"
in (main_cpp)
)
def test_second_switch_entry_for_one_climate_rejected() -> None:
"""The climate keeps one switch per feature, so a second entry for it would take one over."""
entry = {"platform": "gree", CONF_GREE_ID: "gree_climate"}
fv.full_config.get()["switch"] = [entry, dict(entry)]
with pytest.raises(cv.Invalid, match="Only one gree switch entry"):
_validate_unique(entry)
def test_each_climate_may_have_its_own_switch_entry() -> None:
"""Switch entries of different climates do not compete."""
entries = [
{"platform": "gree", CONF_GREE_ID: "gree_a"},
{"platform": "gree", CONF_GREE_ID: "gree_b"},
]
fv.full_config.get()["switch"] = entries
for entry in entries:
_validate_unique(entry)
@@ -1,4 +1,4 @@
"""The crash handler is built for BK7238, BK7231N (both with an ITCM linker region) and RTL8720C."""
"""The crash handler is built for BK7238, BK7231N (both with an ITCM linker region), RTL8720C and LN882H."""
from __future__ import annotations
@@ -19,7 +19,7 @@ from esphome.core import CORE
("bk7231t.yaml", False, False),
("rtl8720c.yaml", True, False), # installed at run time
("rtl8710b.yaml", False, False),
("ln882x.yaml", False, False),
("ln882x.yaml", True, False), # installed at run time
],
)
def test_crash_handler_family_gate(
+4 -3
View File
@@ -5,12 +5,13 @@ from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# AUTO_LOAD sits on the climate platform, which the unit test build does not load.
manifest.dependencies = manifest.dependencies + ["climate_ir", "climate"]
manifest.dependencies = manifest.dependencies + ["climate_ir", "climate", "switch"]
async def to_code_testing(config: ConfigType) -> None:
# Fahrenheit support is define-gated; emit it so the tests cover that path.
# These paths are define-gated; emit the defines so the tests cover them.
cg.add_define("USE_GREE_FAHRENHEIT")
# The YAW1F follow-up message is compiled in only for configs that use that model
cg.add_define("USE_GREE_FEATURE_SWITCH")
cg.add_define("USE_GREE_RECEIVER")
cg.add_define("USE_GREE_YAW1F")
manifest.to_code = to_code_testing
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include <gtest/gtest.h>
#include "esphome/components/gree/gree.h"
#include "esphome/components/remote_base/remote_base.h"
namespace esphome::gree::testing {
using remote_base::RawTimings;
inline remote_base::RemoteReceiveData receive_data(const RawTimings &raw) {
return remote_base::RemoteReceiveData(raw, 25, remote_base::TOLERANCE_MODE_PERCENTAGE);
}
inline RawTimings encode_signal(Model model, const GreeState &state) {
remote_base::RemoteTransmitData data;
GreeProtocol(model).encode(&data, state);
return data.get_data();
}
inline optional<GreeState> decode_signal(Model model, const RawTimings &raw) {
return GreeProtocol(model).decode(receive_data(raw));
}
inline GreeState with_checksum(GreeState state) {
state[GREE_STATE_FRAME_SIZE - 1] = GreeProtocol::calculate_checksum(state);
return state;
}
/// Hand raw timings to the climate as the remote receiver would.
inline bool receive_raw(GreeClimate &device, const RawTimings &raw) {
remote_base::RemoteReceiverListener *listener = &device;
return listener->on_receive(receive_data(raw));
}
inline bool receive(GreeClimate &device, Model model, const GreeState &state) {
return receive_raw(device, encode_signal(model, state));
}
/// Counts transmissions and keeps the last one's timings instead of driving IR hardware.
class CapturingTransmitter final : public remote_base::RemoteTransmitterBase {
public:
CapturingTransmitter() : RemoteTransmitterBase(nullptr) {}
/// The last transmitted frame, decoded with GreeProtocol.
GreeState frame(Model model) const {
const auto state = decode_signal(model, this->last_raw);
EXPECT_TRUE(state.has_value());
return state.value_or(GreeState{});
}
uint32_t send_count{0};
RawTimings last_raw;
uint32_t last_carrier_frequency{0};
protected:
void send_internal(uint32_t send_times, uint32_t /*send_wait*/) override {
this->send_count += send_times;
this->last_raw = this->temp_.get_data();
this->last_carrier_frequency = this->temp_.get_carrier_frequency();
}
};
} // namespace esphome::gree::testing
+23 -1
View File
@@ -4,13 +4,21 @@ climate:
id: my_gree_ac
model: YAN
transmitter_id: xmitr
# YX1FF, the model use_fahrenheit was verified on; it has no mode switches, so they stay on YAN
# YX1FF, the model use_fahrenheit was verified on
- platform: gree
name: GREE Fahrenheit
id: my_gree_ac_fahrenheit
model: YX1FF
use_fahrenheit: true
transmitter_id: xmitr
receiver_id: rcvr
- platform: gree
name: GREE YB1FA
id: my_gree_yb1fa_ac
model: YB1FA
transmitter_id: xmitr
receiver_id: rcvr
switch:
- platform: gree
@@ -23,3 +31,17 @@ switch:
name: "AC Health"
xfan:
name: "AC X-Fan"
- platform: gree
gree_id: my_gree_yb1fa_ac
light:
name: "YB1FA AC Lights"
turbo:
name: "YB1FA AC Turbo"
xfan:
name: "YB1FA AC X-Fan"
- platform: gree
gree_id: my_gree_ac_fahrenheit
light:
name: "YX1FF AC Lights"
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,185 @@
#include <gtest/gtest.h>
#include "../common.h"
#include "esphome/components/switch/switch.h"
#include "esphome/core/application.h"
#include "esphome/core/preferences.h"
namespace esphome::gree::testing {
namespace {
// Stands in for the hub pointer code generation gives each switch's apply function.
GreeClimate *hub = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
template<GreeFeature F> void apply_feature(switch_::Switch * /*entity*/, bool state) {
hub->set_feature_state(F, state);
}
// The switch class code generation emits for `apply="set_feature_state(<feature>, state)"`.
template<GreeFeature F> using FeatureSwitch = switch_::ApplySwitch<apply_feature<F>>;
void initialize_climate(GreeClimate &device, Model model, CapturingTransmitter &transmitter) {
hub = &device;
device.set_model(model);
device.set_transmitter(&transmitter);
device.mode = climate::CLIMATE_MODE_COOL;
device.target_temperature = 20;
device.fan_mode = climate::CLIMATE_FAN_LOW;
device.swing_mode = climate::CLIMATE_SWING_OFF;
device.preset = climate::CLIMATE_PRESET_NONE;
}
/// Set up the climate as the application would at boot, with nothing saved.
void setup_climate(GreeClimate &device, Model model, CapturingTransmitter &transmitter) {
if (global_preferences != nullptr)
global_preferences->reset();
static char name[] = "test_gree";
static char friendly_name[] = "";
App.pre_setup(name, sizeof(name) - 1, friendly_name, 0);
hub = &device;
device.set_model(model);
device.set_transmitter(&transmitter);
static_cast<Component &>(device).setup();
}
GreeClimateData decode_transmission(const CapturingTransmitter &transmitter) {
auto decoded = GreeClimateCodec::decode(GREE_YB1FA, transmitter.frame(GREE_YB1FA));
EXPECT_TRUE(decoded.has_value());
return decoded.value_or(GreeClimateData{climate::CLIMATE_MODE_OFF, GREE_TEMP_MIN, climate::CLIMATE_FAN_AUTO,
climate::CLIMATE_SWING_OFF, climate::CLIMATE_PRESET_NONE});
}
void receive_yb1fa_turbo(GreeClimate &device) {
ASSERT_TRUE(receive(device, GREE_YB1FA, {0x49, 0x04, 0x70, 0x50, 0x01, 0x21, 0x00, 0x90}));
}
} // namespace
// Code generation disables restoring for the models that receive feature state over IR.
TEST(GreeFeatureSwitch, DisabledRestoreSetupUsesParentStateWithoutTransmit) {
for (const Model model : {GREE_YB1FA, GREE_YX1FF}) {
CapturingTransmitter transmitter;
GreeClimate device;
FeatureSwitch<GREE_FEATURE_LIGHT> light;
light.set_restore_mode(switch_::SWITCH_RESTORE_DISABLED);
device.register_feature_switch(GREE_FEATURE_LIGHT, &light);
setup_climate(device, model, transmitter);
EXPECT_EQ(transmitter.send_count, 0U);
EXPECT_TRUE(light.has_state());
EXPECT_TRUE(light.state);
}
}
TEST(GreeFeatureSwitch, LegacySetupRestoresAllSwitchesInOneTransmission) {
CapturingTransmitter transmitter;
GreeClimate device;
FeatureSwitch<GREE_FEATURE_TURBO> turbo;
turbo.set_restore_mode(switch_::SWITCH_ALWAYS_ON);
device.register_feature_switch(GREE_FEATURE_TURBO, &turbo);
FeatureSwitch<GREE_FEATURE_LIGHT> light;
light.set_restore_mode(switch_::SWITCH_RESTORE_DEFAULT_OFF);
device.register_feature_switch(GREE_FEATURE_LIGHT, &light);
setup_climate(device, GREE_YAN, transmitter);
EXPECT_EQ(transmitter.send_count, 1U);
EXPECT_TRUE(turbo.state);
EXPECT_FALSE(light.state);
EXPECT_TRUE(device.get_feature_state(GREE_FEATURE_TURBO));
EXPECT_FALSE(device.get_feature_state(GREE_FEATURE_LIGHT));
const GreeState transmitted = transmitter.frame(GREE_YAN);
EXPECT_EQ(transmitted[2] & (GREE_MODE_BIT_TURBO | GREE_MODE_BIT_LIGHT), GREE_MODE_BIT_TURBO);
}
TEST(GreeFeatureSwitch, UnsupportedModelFeatureDoesNotTransmitAndRestoresState) {
CapturingTransmitter transmitter;
GreeClimate device;
initialize_climate(device, GREE_YX1FF, transmitter);
FeatureSwitch<GREE_FEATURE_TURBO> turbo;
device.register_feature_switch(GREE_FEATURE_TURBO, &turbo);
turbo.turn_on();
EXPECT_EQ(transmitter.send_count, 0U);
EXPECT_FALSE(turbo.state);
EXPECT_FALSE(device.get_feature_state(GREE_FEATURE_TURBO));
}
TEST(GreeFeatureSwitch, EnablingYB1FATurboSetsEffectiveFanHigh) {
CapturingTransmitter transmitter;
GreeClimate device;
initialize_climate(device, GREE_YB1FA, transmitter);
FeatureSwitch<GREE_FEATURE_TURBO> turbo;
device.register_feature_switch(GREE_FEATURE_TURBO, &turbo);
turbo.turn_on();
EXPECT_EQ(transmitter.send_count, 1U);
EXPECT_TRUE(turbo.state);
EXPECT_TRUE(device.get_feature_state(GREE_FEATURE_TURBO));
ASSERT_TRUE(device.fan_mode.has_value());
EXPECT_EQ(device.fan_mode.value_or(climate::CLIMATE_FAN_ON), climate::CLIMATE_FAN_HIGH);
const auto transmitted = decode_transmission(transmitter);
EXPECT_EQ(transmitted.fan_mode, climate::CLIMATE_FAN_HIGH);
EXPECT_TRUE(transmitted.feature_bits & GREE_MODE_BIT_TURBO);
}
TEST(GreeFeatureSwitch, ReceivingYB1FATurboSetsSwitchAndEffectiveFanHigh) {
CapturingTransmitter transmitter;
GreeClimate device;
initialize_climate(device, GREE_YB1FA, transmitter);
FeatureSwitch<GREE_FEATURE_TURBO> turbo;
device.register_feature_switch(GREE_FEATURE_TURBO, &turbo);
receive_yb1fa_turbo(device);
EXPECT_EQ(transmitter.send_count, 0U);
EXPECT_TRUE(turbo.state);
EXPECT_TRUE(device.get_feature_state(GREE_FEATURE_TURBO));
ASSERT_TRUE(device.fan_mode.has_value());
EXPECT_EQ(device.fan_mode.value_or(climate::CLIMATE_FAN_ON), climate::CLIMATE_FAN_HIGH);
}
TEST(GreeFeatureSwitch, FanChangeAfterEnablingYB1FATurboClearsTurbo) {
CapturingTransmitter transmitter;
GreeClimate device;
initialize_climate(device, GREE_YB1FA, transmitter);
FeatureSwitch<GREE_FEATURE_TURBO> turbo;
device.register_feature_switch(GREE_FEATURE_TURBO, &turbo);
turbo.turn_on();
auto call = device.make_call();
call.set_fan_mode(climate::CLIMATE_FAN_LOW);
call.perform();
EXPECT_EQ(transmitter.send_count, 2U);
EXPECT_FALSE(turbo.state);
EXPECT_FALSE(device.get_feature_state(GREE_FEATURE_TURBO));
const auto transmitted = decode_transmission(transmitter);
EXPECT_EQ(transmitted.fan_mode, climate::CLIMATE_FAN_LOW);
EXPECT_FALSE(transmitted.feature_bits & GREE_MODE_BIT_TURBO);
}
TEST(GreeFeatureSwitch, FanChangeAfterReceivingYB1FATurboClearsTurbo) {
CapturingTransmitter transmitter;
GreeClimate device;
initialize_climate(device, GREE_YB1FA, transmitter);
FeatureSwitch<GREE_FEATURE_TURBO> turbo;
device.register_feature_switch(GREE_FEATURE_TURBO, &turbo);
receive_yb1fa_turbo(device);
auto call = device.make_call();
call.set_fan_mode(climate::CLIMATE_FAN_MEDIUM);
call.perform();
EXPECT_EQ(transmitter.send_count, 1U);
EXPECT_FALSE(turbo.state);
EXPECT_FALSE(device.get_feature_state(GREE_FEATURE_TURBO));
const auto transmitted = decode_transmission(transmitter);
EXPECT_EQ(transmitted.fan_mode, climate::CLIMATE_FAN_MEDIUM);
EXPECT_FALSE(transmitted.feature_bits & GREE_MODE_BIT_TURBO);
}
} // namespace esphome::gree::testing
+2 -2
View File
@@ -1,4 +1,4 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
<<: !include common.yaml
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml
gree: !include common.yaml
+2 -2
View File
@@ -1,4 +1,4 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
<<: !include common.yaml
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml
gree: !include common.yaml
+1 -2
View File
@@ -117,7 +117,6 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
PLATFORM_LN882X,
PLATFORM_NRF52,
PLATFORM_RP2,
ErrorFormat,
@@ -6304,7 +6303,7 @@ def test_run_miniterm_no_stacktrace_analyzer(
"""Platforms without a stacktrace analyzer log an info and stream anyway."""
mock_serial = MockSerial([b"[I][app:100]: Line 1\r\n", MOCK_SERIAL_END])
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_LN882X}
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_HOST}
config = {
CONF_LOGGER: {
CONF_BAUD_RATE: 115200,
+2 -2
View File
@@ -15,7 +15,7 @@ from unittest.mock import Mock
import pytest
from esphome import platform_hooks
from esphome.const import PLATFORM_ESP32, PLATFORM_LN882X, Platform
from esphome.const import PLATFORM_ESP32, PLATFORM_HOST, Platform
def test_no_unregistered_platform_exposes_a_hook() -> None:
@@ -165,7 +165,7 @@ def test_get_stacktrace_handler_reports_missing_analyzer(
caplog: pytest.LogCaptureFixture,
) -> None:
caplog.set_level("INFO", logger="esphome.platform_hooks")
assert platform_hooks.get_stacktrace_handler(PLATFORM_LN882X) is None
assert platform_hooks.get_stacktrace_handler(PLATFORM_HOST) is None
assert "no compatible analyzer" in caplog.text
# A capability gap is ordinary; it must not warn.
assert not any(r.levelno >= logging.WARNING for r in caplog.records)
+7 -1
View File
@@ -18,6 +18,7 @@ from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_HOST,
PLATFORM_LN882X,
PLATFORM_NRF52,
PLATFORM_RP2,
@@ -71,6 +72,10 @@ CRASH_SAMPLES: dict[str, dict[str, list[str]]] = {
"state_markers": ["CRASH DETECTED ON PREVIOUS BOOT"],
"addresses": ["PC: 0x9B0073A4 (fault location)"],
},
PLATFORM_LN882X: {
"state_markers": ["CRASH DETECTED ON PREVIOUS BOOT"],
"addresses": ["PC: 0x1001A2C4 (fault location)"],
},
PLATFORM_NRF52: {
"state_markers": ["Last crash:"],
"addresses": [
@@ -237,6 +242,7 @@ DECODER_PATTERNS: dict[str, list[str]] = {
PLATFORM_RP2: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_BK72XX: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_RTL87XX: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_LN882X: ["CRASH_HANDLER_RE", "CRASH_HANDLER_ADDR_RE"],
PLATFORM_NRF52: ["STACKTRACE_NRF52_PC_LR_RE"],
}
@@ -554,7 +560,7 @@ def test_processor_registry_miss_disables_at_construction(
caplog.set_level("INFO", logger="esphome.platform_hooks")
with patch.object(stacktrace.platform_hooks, "import_module") as mock_import:
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_LN882X)
processor = stacktrace.LogLineProcessor(CONFIG, PLATFORM_HOST)
processor.process_line("PC: 0x40104960")
mock_import.assert_not_called()