mirror of
https://github.com/esphome/esphome.git
synced 2026-09-27 06:50:22 +00:00
[fujitsu_general] Read the mode, fan speed and swing fields with their real width (#18297)
Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
J. Nick Koston
parent
a4bebc3083
commit
ca46954335
@@ -2,12 +2,6 @@
|
||||
|
||||
namespace esphome::fujitsu_general {
|
||||
|
||||
// bytes' bits are reversed for fujitsu, so nibbles are ordered 1, 0, 3, 2, 5, 4, etc...
|
||||
|
||||
#define SET_NIBBLE(message, nibble, value) \
|
||||
((message)[(nibble) / 2] |= ((value) &0b00001111) << (((nibble) % 2) ? 0 : 4))
|
||||
#define GET_NIBBLE(message, nibble) (((message)[(nibble) / 2] >> (((nibble) % 2) ? 0 : 4)) & 0b00001111)
|
||||
|
||||
static const char *const TAG = "fujitsu_general.climate";
|
||||
|
||||
// Common header
|
||||
@@ -36,32 +30,29 @@ constexpr uint8_t FUJITSU_GENERAL_STATE_HEADER_BYTE1 = 0x30;
|
||||
// State footer
|
||||
constexpr uint8_t FUJITSU_GENERAL_STATE_FOOTER_BYTE0 = 0x20;
|
||||
|
||||
// Temperature
|
||||
constexpr uint8_t FUJITSU_GENERAL_TEMPERATURE_NIBBLE = 16;
|
||||
|
||||
// Power on
|
||||
constexpr uint8_t FUJITSU_GENERAL_POWER_ON_NIBBLE = 17;
|
||||
constexpr uint8_t FUJITSU_GENERAL_POWER_OFF = 0x00;
|
||||
constexpr uint8_t FUJITSU_GENERAL_POWER_ON = 0x01;
|
||||
|
||||
// Mode
|
||||
constexpr uint8_t FUJITSU_GENERAL_MODE_NIBBLE = 19;
|
||||
// Bit 3 is the clean flag, which is also 10 degree heat on the ARRAH2E and ARREW4E remotes.
|
||||
constexpr uint8_t FUJITSU_GENERAL_MODE_MASK = 0b0111;
|
||||
constexpr uint8_t FUJITSU_GENERAL_CLEAN_BIT = 0b1000;
|
||||
constexpr uint8_t FUJITSU_GENERAL_MODE_AUTO = 0x00;
|
||||
constexpr uint8_t FUJITSU_GENERAL_MODE_COOL = 0x01;
|
||||
constexpr uint8_t FUJITSU_GENERAL_MODE_DRY = 0x02;
|
||||
constexpr uint8_t FUJITSU_GENERAL_MODE_FAN = 0x03;
|
||||
constexpr uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04;
|
||||
// const uint8_t FUJITSU_GENERAL_MODE_10C = 0x0B;
|
||||
|
||||
// Swing
|
||||
constexpr uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 20;
|
||||
constexpr uint8_t FUJITSU_GENERAL_SWING_MASK = 0b0011;
|
||||
constexpr uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00;
|
||||
constexpr uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01;
|
||||
constexpr uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02;
|
||||
constexpr uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03;
|
||||
|
||||
// Fan
|
||||
constexpr uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 21;
|
||||
constexpr uint8_t FUJITSU_GENERAL_FAN_MASK = 0b0111;
|
||||
constexpr uint8_t FUJITSU_GENERAL_FAN_AUTO = 0x00;
|
||||
constexpr uint8_t FUJITSU_GENERAL_FAN_HIGH = 0x01;
|
||||
constexpr uint8_t FUJITSU_GENERAL_FAN_MEDIUM = 0x02;
|
||||
@@ -111,68 +102,67 @@ void FujitsuGeneralClimate::transmit_state() {
|
||||
uint8_t temperature_clamped =
|
||||
(uint8_t) roundf(clamp<float>(this->target_temperature, FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX));
|
||||
uint8_t temperature_offset = temperature_clamped - FUJITSU_GENERAL_TEMP_MIN;
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_TEMPERATURE_NIBBLE, temperature_offset);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_TEMPERATURE_NIBBLE, temperature_offset);
|
||||
|
||||
// Set power on
|
||||
if (!this->power_) {
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_POWER_ON_NIBBLE, FUJITSU_GENERAL_POWER_ON);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_POWER_ON_NIBBLE, FUJITSU_GENERAL_POWER_ON);
|
||||
}
|
||||
|
||||
// Set mode
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_COOL);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_COOL);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_HEAT);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_HEAT);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_DRY);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_DRY);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_FAN);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_FAN);
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
default:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_AUTO);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_AUTO);
|
||||
break;
|
||||
// TODO: CLIMATE_MODE_10C is missing from esphome
|
||||
}
|
||||
|
||||
// Set fan
|
||||
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_HIGH);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_MEDIUM);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_MEDIUM);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_QUIET:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT);
|
||||
break;
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
default:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO);
|
||||
break;
|
||||
}
|
||||
|
||||
// Set swing
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_VERTICAL);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_VERTICAL);
|
||||
break;
|
||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_HORIZONTAL);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_HORIZONTAL);
|
||||
break;
|
||||
case climate::CLIMATE_SWING_BOTH:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_BOTH);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_BOTH);
|
||||
break;
|
||||
case climate::CLIMATE_SWING_OFF:
|
||||
default:
|
||||
SET_NIBBLE(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_NONE);
|
||||
set_nibble(remote_state, FUJITSU_GENERAL_SWING_NIBBLE, FUJITSU_GENERAL_SWING_NONE);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -243,6 +233,52 @@ uint8_t FujitsuGeneralClimate::checksum_state_(uint8_t const *message) {
|
||||
|
||||
uint8_t FujitsuGeneralClimate::checksum_util_(uint8_t const *message) { return 255 - message[5]; }
|
||||
|
||||
// These decoders use if chains rather than switches: on ESP8266 the compiler turns a dense switch
|
||||
// into a lookup table in .rodata, which lives in RAM there.
|
||||
climate::ClimateMode decode_mode(uint8_t mode_field, climate::ClimateMode current_mode) {
|
||||
const uint8_t mode = mode_field & FUJITSU_GENERAL_MODE_MASK;
|
||||
if (mode == FUJITSU_GENERAL_MODE_COOL)
|
||||
return climate::CLIMATE_MODE_COOL;
|
||||
if (mode == FUJITSU_GENERAL_MODE_HEAT)
|
||||
return climate::CLIMATE_MODE_HEAT;
|
||||
if (mode == FUJITSU_GENERAL_MODE_DRY)
|
||||
return climate::CLIMATE_MODE_DRY;
|
||||
if (mode == FUJITSU_GENERAL_MODE_FAN)
|
||||
return climate::CLIMATE_MODE_FAN_ONLY;
|
||||
if (mode == FUJITSU_GENERAL_MODE_AUTO)
|
||||
return climate::CLIMATE_MODE_HEAT_COOL;
|
||||
// A state frame means the unit is on, so never keep OFF.
|
||||
ESP_LOGW(TAG, "Received unassigned mode %X, keeping the current mode", mode);
|
||||
return current_mode == climate::CLIMATE_MODE_OFF ? climate::CLIMATE_MODE_HEAT_COOL : current_mode;
|
||||
}
|
||||
|
||||
optional<climate::ClimateFanMode> decode_fan_mode(uint8_t fan_field, optional<climate::ClimateFanMode> current_mode) {
|
||||
const uint8_t fan = fan_field & FUJITSU_GENERAL_FAN_MASK;
|
||||
if (fan == FUJITSU_GENERAL_FAN_HIGH)
|
||||
return climate::CLIMATE_FAN_HIGH;
|
||||
if (fan == FUJITSU_GENERAL_FAN_MEDIUM)
|
||||
return climate::CLIMATE_FAN_MEDIUM;
|
||||
if (fan == FUJITSU_GENERAL_FAN_LOW)
|
||||
return climate::CLIMATE_FAN_LOW;
|
||||
if (fan == FUJITSU_GENERAL_FAN_SILENT)
|
||||
return climate::CLIMATE_FAN_QUIET;
|
||||
if (fan == FUJITSU_GENERAL_FAN_AUTO)
|
||||
return climate::CLIMATE_FAN_AUTO;
|
||||
ESP_LOGW(TAG, "Received unassigned fan speed %X, keeping the current fan mode", fan);
|
||||
return current_mode;
|
||||
}
|
||||
|
||||
climate::ClimateSwingMode decode_swing_mode(uint8_t swing_field) {
|
||||
const uint8_t swing = swing_field & FUJITSU_GENERAL_SWING_MASK;
|
||||
if (swing == FUJITSU_GENERAL_SWING_VERTICAL)
|
||||
return climate::CLIMATE_SWING_VERTICAL;
|
||||
if (swing == FUJITSU_GENERAL_SWING_HORIZONTAL)
|
||||
return climate::CLIMATE_SWING_HORIZONTAL;
|
||||
if (swing == FUJITSU_GENERAL_SWING_BOTH)
|
||||
return climate::CLIMATE_SWING_BOTH;
|
||||
return climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
|
||||
bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
ESP_LOGV(TAG, "Received IR message");
|
||||
|
||||
@@ -316,70 +352,25 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
}
|
||||
|
||||
if (recv_message_type == FUJITSU_GENERAL_MESSAGE_TYPE_STATE) {
|
||||
const uint8_t recv_tempertature = GET_NIBBLE(recv_message, FUJITSU_GENERAL_TEMPERATURE_NIBBLE);
|
||||
const uint8_t recv_tempertature = get_nibble(recv_message, FUJITSU_GENERAL_TEMPERATURE_NIBBLE);
|
||||
const uint8_t offset_temperature = recv_tempertature + FUJITSU_GENERAL_TEMP_MIN;
|
||||
this->target_temperature = offset_temperature;
|
||||
ESP_LOGV(TAG, "Received temperature %d", offset_temperature);
|
||||
|
||||
const uint8_t recv_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_MODE_NIBBLE);
|
||||
const uint8_t recv_mode = get_nibble(recv_message, FUJITSU_GENERAL_MODE_NIBBLE);
|
||||
ESP_LOGV(TAG, "Received mode %X", recv_mode);
|
||||
switch (recv_mode) {
|
||||
case FUJITSU_GENERAL_MODE_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
case FUJITSU_GENERAL_MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
case FUJITSU_GENERAL_MODE_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
case FUJITSU_GENERAL_MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
case FUJITSU_GENERAL_MODE_AUTO:
|
||||
default:
|
||||
// TODO: CLIMATE_MODE_10C is missing from esphome
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
if ((recv_mode & FUJITSU_GENERAL_CLEAN_BIT) != 0) {
|
||||
ESP_LOGW(TAG, "Received a frame with the clean / 10 degree heat bit set, which is not supported");
|
||||
}
|
||||
this->mode = decode_mode(recv_mode, this->mode);
|
||||
|
||||
const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE);
|
||||
const uint8_t recv_fan_mode = get_nibble(recv_message, FUJITSU_GENERAL_FAN_NIBBLE);
|
||||
ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode);
|
||||
switch (recv_fan_mode) {
|
||||
case FUJITSU_GENERAL_FAN_SILENT:
|
||||
this->fan_mode = climate::CLIMATE_FAN_QUIET;
|
||||
break;
|
||||
case FUJITSU_GENERAL_FAN_LOW:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
case FUJITSU_GENERAL_FAN_MEDIUM:
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
case FUJITSU_GENERAL_FAN_HIGH:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
case FUJITSU_GENERAL_FAN_AUTO:
|
||||
default:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
}
|
||||
this->fan_mode = decode_fan_mode(recv_fan_mode, this->fan_mode);
|
||||
|
||||
const uint8_t recv_swing_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_SWING_NIBBLE);
|
||||
const uint8_t recv_swing_mode = get_nibble(recv_message, FUJITSU_GENERAL_SWING_NIBBLE);
|
||||
ESP_LOGV(TAG, "Received swing mode %X", recv_swing_mode);
|
||||
switch (recv_swing_mode) {
|
||||
case FUJITSU_GENERAL_SWING_VERTICAL:
|
||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
break;
|
||||
case FUJITSU_GENERAL_SWING_HORIZONTAL:
|
||||
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
||||
break;
|
||||
case FUJITSU_GENERAL_SWING_BOTH:
|
||||
this->swing_mode = climate::CLIMATE_SWING_BOTH;
|
||||
break;
|
||||
case FUJITSU_GENERAL_SWING_NONE:
|
||||
default:
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
this->swing_mode = decode_swing_mode(recv_swing_mode);
|
||||
|
||||
this->power_ = true;
|
||||
}
|
||||
|
||||
@@ -43,9 +43,36 @@ constexpr uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius
|
||||
* heat 30 swing vert 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00101000 00000000 00000000 00000000 00000100 00011101
|
||||
* heat 30 noswing 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011
|
||||
* ```
|
||||
*
|
||||
* The column markers show which bits varied in these captures, not field widths.
|
||||
*/
|
||||
// clang-format on
|
||||
|
||||
// Bits are reversed within each byte, so an odd nibble index is the low half of its byte.
|
||||
constexpr uint8_t get_nibble(const uint8_t *message, uint8_t nibble) {
|
||||
return (message[nibble / 2] >> ((nibble % 2) ? 0 : 4)) & 0b00001111;
|
||||
}
|
||||
|
||||
/// Write a nibble into a zero-initialised frame.
|
||||
constexpr void set_nibble(uint8_t *message, uint8_t nibble, uint8_t value) {
|
||||
message[nibble / 2] |= (value & 0b00001111) << ((nibble % 2) ? 0 : 4);
|
||||
}
|
||||
|
||||
// Nibble indices of the state frame fields.
|
||||
constexpr uint8_t FUJITSU_GENERAL_TEMPERATURE_NIBBLE = 16;
|
||||
constexpr uint8_t FUJITSU_GENERAL_POWER_ON_NIBBLE = 17;
|
||||
constexpr uint8_t FUJITSU_GENERAL_MODE_NIBBLE = 19;
|
||||
constexpr uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 20;
|
||||
constexpr uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 21;
|
||||
|
||||
/// Unassigned values keep the current mode, except that OFF becomes HEAT_COOL.
|
||||
climate::ClimateMode decode_mode(uint8_t mode_field, climate::ClimateMode current_mode);
|
||||
|
||||
/// Unassigned values keep the current fan mode.
|
||||
optional<climate::ClimateFanMode> decode_fan_mode(uint8_t fan_field, optional<climate::ClimateFanMode> current_mode);
|
||||
|
||||
climate::ClimateSwingMode decode_swing_mode(uint8_t swing_field);
|
||||
|
||||
class FujitsuGeneralClimate final : public climate_ir::ClimateIR {
|
||||
public:
|
||||
FujitsuGeneralClimate()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# This component's AUTO_LOAD = ["climate_ir"] sits on the climate platform manifest, while its
|
||||
# own __init__.py is empty. The unit test build resolves the bare `fujitsu_general` domain, so
|
||||
# it never sees that manifest. And climate_ir itself doesn't declare `climate` even though
|
||||
# ClimateIR derives from climate::Climate. Pull both in so the test can include the header.
|
||||
manifest.dependencies = manifest.dependencies + ["climate_ir", "climate"]
|
||||
@@ -0,0 +1,254 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "esphome/components/fujitsu_general/fujitsu_general.h"
|
||||
|
||||
namespace esphome::fujitsu_general::testing {
|
||||
|
||||
// The mode field of a received frame is three bits wide. The fourth bit of the same nibble belongs
|
||||
// to the clean feature, so it has to be ignored when reading the mode.
|
||||
|
||||
TEST(FujitsuGeneralDecodeModeTest, DecodesTheAssignedModes) {
|
||||
EXPECT_EQ(decode_mode(0x00, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT_COOL);
|
||||
EXPECT_EQ(decode_mode(0x01, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_COOL);
|
||||
EXPECT_EQ(decode_mode(0x02, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_DRY);
|
||||
EXPECT_EQ(decode_mode(0x03, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_FAN_ONLY);
|
||||
EXPECT_EQ(decode_mode(0x04, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT);
|
||||
}
|
||||
|
||||
TEST(FujitsuGeneralDecodeModeTest, IgnoresTheCleanBit) {
|
||||
// 0x0B is fan mode with the clean bit set. It used to be read as one value and reported as
|
||||
// heat/cool, which is the bug this covers.
|
||||
EXPECT_EQ(decode_mode(0x0B, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_FAN_ONLY);
|
||||
|
||||
EXPECT_EQ(decode_mode(0x08, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT_COOL);
|
||||
EXPECT_EQ(decode_mode(0x09, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_COOL);
|
||||
EXPECT_EQ(decode_mode(0x0A, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_DRY);
|
||||
EXPECT_EQ(decode_mode(0x0C, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT);
|
||||
}
|
||||
|
||||
TEST(FujitsuGeneralDecodeModeTest, KeepsTheCurrentModeForUnassignedValues) {
|
||||
// 0x5 to 0x7 fit in the field but the protocol does not use them.
|
||||
EXPECT_EQ(decode_mode(0x05, climate::CLIMATE_MODE_COOL), climate::CLIMATE_MODE_COOL);
|
||||
EXPECT_EQ(decode_mode(0x06, climate::CLIMATE_MODE_HEAT), climate::CLIMATE_MODE_HEAT);
|
||||
EXPECT_EQ(decode_mode(0x07, climate::CLIMATE_MODE_DRY), climate::CLIMATE_MODE_DRY);
|
||||
|
||||
// The same three with the clean bit set. Without the mask these would not reach this branch.
|
||||
EXPECT_EQ(decode_mode(0x0D, climate::CLIMATE_MODE_COOL), climate::CLIMATE_MODE_COOL);
|
||||
EXPECT_EQ(decode_mode(0x0E, climate::CLIMATE_MODE_HEAT), climate::CLIMATE_MODE_HEAT);
|
||||
EXPECT_EQ(decode_mode(0x0F, climate::CLIMATE_MODE_FAN_ONLY), climate::CLIMATE_MODE_FAN_ONLY);
|
||||
}
|
||||
|
||||
TEST(FujitsuGeneralDecodeModeTest, NeverReportsOffForAStateFrame) {
|
||||
// A state frame describes a running unit, so keeping an off current mode would publish it as off
|
||||
// and turn the next transmission into a power off command. Automatic is the least specific mode
|
||||
// available, which is what the field's unassigned values decoded to before they were masked.
|
||||
for (uint8_t field = 0x05; field <= 0x07; field++) {
|
||||
SCOPED_TRACE(static_cast<int>(field));
|
||||
EXPECT_EQ(decode_mode(field, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT_COOL);
|
||||
EXPECT_EQ(decode_mode(field | 0b1000, climate::CLIMATE_MODE_OFF), climate::CLIMATE_MODE_HEAT_COOL);
|
||||
}
|
||||
}
|
||||
|
||||
// The fan speed field is three bits wide as well, and used to fold every value it did not
|
||||
// recognise into the automatic speed.
|
||||
|
||||
TEST(FujitsuGeneralDecodeFanModeTest, DecodesTheAssignedSpeeds) {
|
||||
EXPECT_EQ(decode_fan_mode(0x00, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_AUTO);
|
||||
EXPECT_EQ(decode_fan_mode(0x01, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_HIGH);
|
||||
EXPECT_EQ(decode_fan_mode(0x02, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_MEDIUM);
|
||||
EXPECT_EQ(decode_fan_mode(0x03, climate::CLIMATE_FAN_AUTO), climate::CLIMATE_FAN_LOW);
|
||||
EXPECT_EQ(decode_fan_mode(0x04, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_QUIET);
|
||||
}
|
||||
|
||||
TEST(FujitsuGeneralDecodeFanModeTest, IgnoresTheFourthBit) {
|
||||
EXPECT_EQ(decode_fan_mode(0x08, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_AUTO);
|
||||
EXPECT_EQ(decode_fan_mode(0x09, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_HIGH);
|
||||
EXPECT_EQ(decode_fan_mode(0x0A, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_MEDIUM);
|
||||
EXPECT_EQ(decode_fan_mode(0x0B, climate::CLIMATE_FAN_AUTO), climate::CLIMATE_FAN_LOW);
|
||||
EXPECT_EQ(decode_fan_mode(0x0C, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_QUIET);
|
||||
}
|
||||
|
||||
TEST(FujitsuGeneralDecodeFanModeTest, KeepsTheCurrentFanModeForUnassignedValues) {
|
||||
EXPECT_EQ(decode_fan_mode(0x05, climate::CLIMATE_FAN_HIGH), climate::CLIMATE_FAN_HIGH);
|
||||
EXPECT_EQ(decode_fan_mode(0x06, climate::CLIMATE_FAN_MEDIUM), climate::CLIMATE_FAN_MEDIUM);
|
||||
EXPECT_EQ(decode_fan_mode(0x07, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_LOW);
|
||||
EXPECT_EQ(decode_fan_mode(0x0D, climate::CLIMATE_FAN_HIGH), climate::CLIMATE_FAN_HIGH);
|
||||
EXPECT_EQ(decode_fan_mode(0x0E, climate::CLIMATE_FAN_HIGH), climate::CLIMATE_FAN_HIGH);
|
||||
EXPECT_EQ(decode_fan_mode(0x0F, climate::CLIMATE_FAN_LOW), climate::CLIMATE_FAN_LOW);
|
||||
}
|
||||
|
||||
TEST(FujitsuGeneralDecodeFanModeTest, LeavesAnUnsetFanModeUnset) {
|
||||
EXPECT_FALSE(decode_fan_mode(0x05, {}).has_value());
|
||||
}
|
||||
|
||||
// The swing field is only two bits wide. The two bits above it are reserved, and were read as part
|
||||
// of the value.
|
||||
|
||||
TEST(FujitsuGeneralDecodeSwingModeTest, DecodesTheAssignedValues) {
|
||||
EXPECT_EQ(decode_swing_mode(0x00), climate::CLIMATE_SWING_OFF);
|
||||
EXPECT_EQ(decode_swing_mode(0x01), climate::CLIMATE_SWING_VERTICAL);
|
||||
EXPECT_EQ(decode_swing_mode(0x02), climate::CLIMATE_SWING_HORIZONTAL);
|
||||
EXPECT_EQ(decode_swing_mode(0x03), climate::CLIMATE_SWING_BOTH);
|
||||
}
|
||||
|
||||
TEST(FujitsuGeneralDecodeSwingModeTest, IgnoresTheReservedBits) {
|
||||
// Without the mask everything from 0x04 up fell through to the default branch and reported swing
|
||||
// off. All twelve are covered, so the field's whole input space is asserted.
|
||||
const climate::ClimateSwingMode expected[] = {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
|
||||
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH};
|
||||
for (uint8_t field = 0x04; field <= 0x0F; field++) {
|
||||
SCOPED_TRACE(static_cast<int>(field));
|
||||
EXPECT_EQ(decode_swing_mode(field), expected[field & 0b0011]);
|
||||
}
|
||||
}
|
||||
|
||||
// Every state frame annotated in fujitsu_general.h, as the bytes those rows spell out. None of them
|
||||
// sets the fourth bit of the mode or fan field, or either bit above the swing field, so the masks
|
||||
// must leave all of them decoding exactly as they did before this change.
|
||||
|
||||
namespace {
|
||||
|
||||
struct CapturedFrame {
|
||||
const char *label;
|
||||
uint8_t bytes[16];
|
||||
uint8_t temperature;
|
||||
bool turn_on;
|
||||
climate::ClimateMode mode;
|
||||
climate::ClimateFanMode fan_mode;
|
||||
climate::ClimateSwingMode swing_mode;
|
||||
};
|
||||
|
||||
constexpr CapturedFrame CAPTURED_FRAMES[] = {
|
||||
{"auto auto 18",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x8F},
|
||||
18,
|
||||
true,
|
||||
climate::CLIMATE_MODE_HEAT_COOL,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"auto auto 19",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x7F},
|
||||
19,
|
||||
true,
|
||||
climate::CLIMATE_MODE_HEAT_COOL,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"auto auto 30 (temperatures)",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCF},
|
||||
30,
|
||||
true,
|
||||
climate::CLIMATE_MODE_HEAT_COOL,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"on at 16",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x20, 0xAB},
|
||||
16,
|
||||
true,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"down to 16",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x20, 0xAC},
|
||||
16,
|
||||
false,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"auto auto 30 (mode options)",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCF},
|
||||
30,
|
||||
true,
|
||||
climate::CLIMATE_MODE_HEAT_COOL,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"cool auto 30",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCE},
|
||||
30,
|
||||
true,
|
||||
climate::CLIMATE_MODE_COOL,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"dry auto 30",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x02, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCD},
|
||||
30,
|
||||
true,
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"fan (auto) (30)",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x03, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCC},
|
||||
30,
|
||||
true,
|
||||
climate::CLIMATE_MODE_FAN_ONLY,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"heat auto 30",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x04, 0x00, 0x00, 0x00, 0x00, 0x20, 0xCB},
|
||||
30,
|
||||
true,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"heat 30 high",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE1, 0x04, 0x01, 0x00, 0x00, 0x00, 0x20, 0xCA},
|
||||
30,
|
||||
true,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"heat 30 med",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x02, 0x00, 0x00, 0x00, 0x20, 0xCA},
|
||||
30,
|
||||
false,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"heat 30 low",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x03, 0x00, 0x00, 0x00, 0x20, 0xC9},
|
||||
30,
|
||||
false,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_FAN_LOW,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"heat 30 quiet",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x04, 0x00, 0x00, 0x00, 0x20, 0xC8},
|
||||
30,
|
||||
false,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_FAN_QUIET,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
{"heat 30 swing vert",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x14, 0x00, 0x00, 0x00, 0x20, 0xB8},
|
||||
30,
|
||||
false,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_FAN_QUIET,
|
||||
climate::CLIMATE_SWING_VERTICAL},
|
||||
{"heat 30 noswing",
|
||||
{0x14, 0x63, 0x00, 0x10, 0x10, 0xFE, 0x09, 0x30, 0xE0, 0x04, 0x04, 0x00, 0x00, 0x00, 0x20, 0xC8},
|
||||
30,
|
||||
false,
|
||||
climate::CLIMATE_MODE_HEAT,
|
||||
climate::CLIMATE_FAN_QUIET,
|
||||
climate::CLIMATE_SWING_OFF},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(FujitsuGeneralCaptureTest, DecodesEveryCapturedFrame) {
|
||||
for (const auto &frame : CAPTURED_FRAMES) {
|
||||
SCOPED_TRACE(frame.label);
|
||||
// Read through the component's own nibble helper and field indices, so this also fails if the
|
||||
// frame layout the header records ever stops matching what on_receive() reads.
|
||||
EXPECT_EQ(get_nibble(frame.bytes, FUJITSU_GENERAL_TEMPERATURE_NIBBLE) + FUJITSU_GENERAL_TEMP_MIN,
|
||||
frame.temperature);
|
||||
// The turn on flag is only written by transmit_state(), so this pins the frame layout rather
|
||||
// than a decode path.
|
||||
EXPECT_EQ(get_nibble(frame.bytes, FUJITSU_GENERAL_POWER_ON_NIBBLE) != 0, frame.turn_on);
|
||||
EXPECT_EQ(decode_mode(get_nibble(frame.bytes, FUJITSU_GENERAL_MODE_NIBBLE), climate::CLIMATE_MODE_OFF), frame.mode);
|
||||
EXPECT_EQ(decode_fan_mode(get_nibble(frame.bytes, FUJITSU_GENERAL_FAN_NIBBLE), climate::CLIMATE_FAN_ON),
|
||||
frame.fan_mode);
|
||||
EXPECT_EQ(decode_swing_mode(get_nibble(frame.bytes, FUJITSU_GENERAL_SWING_NIBBLE)), frame.swing_mode);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::fujitsu_general::testing
|
||||
Reference in New Issue
Block a user