mirror of
https://github.com/esphome/esphome.git
synced 2026-10-28 00:42:11 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3278ea5a04 | ||
|
|
c3348789eb | ||
|
|
361a9580da | ||
|
|
43cc2a8e3d | ||
|
|
281c5fcabc | ||
|
|
79c945c76c | ||
|
|
92c54aea77 |
No files matched your search
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,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",
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user