Compare commits

..
7 changed files with 142 additions and 124 deletions

No files matched your search

@@ -95,4 +95,19 @@ void ClimateIR::dump_config() {
YESNO(this->modes_.count(climate::CLIMATE_MODE_HEAT_COOL)));
}
int find_ir_code_index(const IRCode *table, size_t count, uint8_t key, bool match_code) {
for (size_t i = 0; i < count; i++) {
if (progmem_read_byte(match_code ? &table[i].code : &table[i].value) == key)
return static_cast<int>(i);
}
return -1;
}
uint8_t lookup_ir_code(const IRCode *table, size_t count, uint8_t key, bool match_code, uint8_t fallback) {
const int index = find_ir_code_index(table, count, key, match_code);
if (index < 0)
return fallback;
return progmem_read_byte(match_code ? &table[index].value : &table[index].code);
}
} // namespace esphome::climate_ir
@@ -6,9 +6,42 @@
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/hal.h"
#include "esphome/core/optional.h"
namespace esphome::climate_ir {
/// One entry of a PROGMEM table that maps a climate enum value to the unit's code.
struct IRCode {
uint8_t value; // a climate enum; they are all uint8_t
uint8_t code;
};
/// Index of the entry whose value (or, with match_code, code) is key, or -1. Reads the table from PROGMEM.
int find_ir_code_index(const IRCode *table, size_t count, uint8_t key, bool match_code);
/// The entry whose value (or, with match_code, code) is key, read from PROGMEM.
template<size_t N>
ESPHOME_ALWAYS_INLINE optional<IRCode> find_ir_code(const IRCode (&table)[N], uint8_t key, bool match_code) {
const int index = find_ir_code_index(table, N, key, match_code);
if (index < 0)
return {};
return IRCode{progmem_read_byte(&table[index].value), progmem_read_byte(&table[index].code)};
}
/// The code for value (or, with match_code, the value for code), or fallback when the table has no such entry.
uint8_t lookup_ir_code(const IRCode *table, size_t count, uint8_t key, bool match_code, uint8_t fallback);
/// The code for value, or fallback when the table has no such value.
template<size_t N> uint8_t to_ir_code(const IRCode (&table)[N], uint8_t value, uint8_t fallback) {
return lookup_ir_code(table, N, value, false, fallback);
}
/// The value for code, or fallback when the table has no such code; pass the current state to keep it.
template<typename T, size_t N> T from_ir_code(const IRCode (&table)[N], uint8_t code, T fallback) {
static_assert(sizeof(T) == sizeof(uint8_t), "IRCode values are one byte");
return static_cast<T>(lookup_ir_code(table, N, code, true, static_cast<uint8_t>(fallback)));
}
/* A base for climate which works by sending (and receiving) IR codes
To send IR codes implement
+12 -25
View File
@@ -50,34 +50,25 @@ static constexpr uint8_t STATE_TEMPLATE[STATE_MESSAGE_LENGTH] PROGMEM = {
static constexpr uint8_t OFF_FRAME[STATE_MESSAGE_LENGTH_UTIL] PROGMEM = {0x00, 0x08, 0x08, BYTE3_POWER_OFF,
255 - BYTE3_POWER_OFF};
struct Code {
uint8_t value; // a climate enum, all of which are uint8_t
uint8_t code;
};
static constexpr Code MODES[] PROGMEM = {
using climate_ir::find_ir_code;
using climate_ir::from_ir_code;
using climate_ir::IRCode;
using climate_ir::to_ir_code;
// A received mode that is not listed is ignored
static constexpr IRCode MODES[] PROGMEM = {
{climate::CLIMATE_MODE_HEAT_COOL, BYTE7_MODE_AUTO},
{climate::CLIMATE_MODE_COOL, 0x80},
{climate::CLIMATE_MODE_DRY, 0x40},
{climate::CLIMATE_MODE_FAN_ONLY, 0xC0},
{climate::CLIMATE_MODE_HEAT, 0x20},
};
static constexpr Code FANS[] PROGMEM = {
static constexpr IRCode FANS[] PROGMEM = {
{climate::CLIMATE_FAN_AUTO, BYTE8_FAN_AUTO}, {climate::CLIMATE_FAN_HIGH, 0x80},
{climate::CLIMATE_FAN_MEDIUM, 0x40}, {climate::CLIMATE_FAN_LOW, 0xC0},
{climate::CLIMATE_FAN_QUIET, 0x20},
};
// The entry whose value (or code) matches key. Entries are copied out of PROGMEM.
template<size_t N> static optional<Code> find_entry(const Code (&table)[N], uint8_t key, bool match_code) {
for (const Code &slot : table) {
Code entry;
progmem_memcpy(&entry, &slot, sizeof(entry));
if ((match_code ? entry.code : entry.value) == key)
return entry;
}
return {};
}
// The temperature code is the 4-bit reversal of (F - TEMP_MIN) / TEMP_STEP; the mapping is its own inverse.
static uint8_t reverse_nibble(uint8_t value) { return reverse_bits(value) >> 4; }
@@ -151,14 +142,11 @@ void FriedrichClimate::transmit_state() {
temperature += temperature % 2;
this->target_temperature = temperature;
const auto mode = find_entry(MODES, this->mode, false);
const auto fan = find_entry(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), false);
uint8_t frame[STATE_MESSAGE_LENGTH];
progmem_memcpy(frame, STATE_TEMPLATE, sizeof(frame));
frame[6] |= reverse_nibble((temperature - TEMP_MIN) / TEMP_STEP);
frame[7] = mode.has_value() ? mode->code : BYTE7_MODE_AUTO;
frame[8] = fan.has_value() ? fan->code : BYTE8_FAN_AUTO;
frame[7] = to_ir_code(MODES, this->mode, BYTE7_MODE_AUTO);
frame[8] = to_ir_code(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), BYTE8_FAN_AUTO);
if (this->swing_mode != climate::CLIMATE_SWING_OFF)
frame[8] |= BYTE8_FAN_SWING;
frame[STATE_MESSAGE_LENGTH - 1] = checksum_state(frame);
@@ -209,7 +197,7 @@ bool FriedrichClimate::on_receive(remote_base::RemoteReceiveData src) {
return false;
// Ignore frames with a mode this component does not expose (e.g. MIN_HEAT, COIL_DRY)
const auto mode = find_entry(MODES, d[7], true);
const auto mode = find_ir_code(MODES, d[7], true);
if (!mode.has_value()) {
ESP_LOGV(TAG, "Ignoring unknown mode 0x%02X", d[7]);
return false;
@@ -224,8 +212,7 @@ bool FriedrichClimate::on_receive(remote_base::RemoteReceiveData src) {
this->target_temperature = TEMP_MIN + temp_index * TEMP_STEP;
this->swing_mode = (d[8] & BYTE8_FAN_SWING) ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
const auto fan = find_entry(FANS, d[8] & ~BYTE8_FAN_SWING, true);
this->fan_mode = fan.has_value() ? static_cast<climate::ClimateFanMode>(fan->value) : climate::CLIMATE_FAN_AUTO;
this->fan_mode = from_ir_code(FANS, d[8] & ~BYTE8_FAN_SWING, climate::CLIMATE_FAN_AUTO);
this->publish_state();
return true;
+7 -11
View File
@@ -21,19 +21,16 @@ constexpr uint8_t MISTRAL_POWER_ON = 0x20;
constexpr uint8_t MISTRAL_TEMP_OFFSET = 0x9F;
constexpr uint8_t MISTRAL_MODE_AUTO = 0x10;
// The tables live in flash, so entries are copied out before use
struct ModeCode {
climate::ClimateMode mode;
uint8_t code;
};
static const ModeCode MODES[] PROGMEM = {
// OFF sends the auto mode code and only clears the power byte
static constexpr climate_ir::IRCode MODES[] PROGMEM = {
{climate::CLIMATE_MODE_HEAT_COOL, MISTRAL_MODE_AUTO},
{climate::CLIMATE_MODE_COOL, 0xC0},
{climate::CLIMATE_MODE_DRY, 0x40},
{climate::CLIMATE_MODE_FAN_ONLY, 0x80},
{climate::CLIMATE_MODE_HEAT, 0x60},
{climate::CLIMATE_MODE_HEAT_COOL, MISTRAL_MODE_AUTO},
};
// Each fan speed sets two bytes; the table lives in flash, so entries are copied out before use
struct FanCode {
climate::ClimateFanMode fan;
uint8_t byte1;
@@ -78,7 +75,6 @@ static bool is_valid_frame(const uint8_t *frame) {
}
void MistralIR::transmit_state() {
const auto mode = find_entry(MODES, [this](const ModeCode &m) { return m.mode == this->mode; });
const climate::ClimateFanMode fan_mode = this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO);
const FanCode fan =
find_entry(FANS, [fan_mode](const FanCode &f) { return f.fan == fan_mode; }).value_or(load_entry(FANS[0]));
@@ -88,7 +84,7 @@ void MistralIR::transmit_state() {
progmem_memcpy(frame, FRAME_TEMPLATE, sizeof(frame));
frame[1] = fan.byte1;
frame[3] = this->mode == climate::CLIMATE_MODE_OFF ? MISTRAL_POWER_OFF : MISTRAL_POWER_ON;
frame[4] = mode.has_value() ? mode->code : MISTRAL_MODE_AUTO;
frame[4] = climate_ir::to_ir_code(MODES, this->mode, MISTRAL_MODE_AUTO);
frame[5] = reverse_bits(static_cast<uint8_t>(MISTRAL_TEMP_OFFSET - temp));
frame[6] = fan.byte6;
frame[11] = compute_checksum(frame);
@@ -116,12 +112,12 @@ bool MistralIR::on_receive(remote_base::RemoteReceiveData data) {
return false;
// Decode everything before touching the state so an unknown code leaves it untouched
const auto mode = find_entry(MODES, [frame](const ModeCode &m) { return m.code == frame[4]; });
const auto mode = climate_ir::find_ir_code(MODES, frame[4], true);
const auto fan = find_entry(FANS, [frame](const FanCode &f) { return f.byte1 == frame[1] && f.byte6 == frame[6]; });
if (!mode.has_value() || !fan.has_value())
return false;
this->mode = mode->mode;
this->mode = static_cast<climate::ClimateMode>(mode->value);
this->target_temperature =
clamp<float>(MISTRAL_TEMP_OFFSET - reverse_bits(frame[5]), MISTRAL_TEMP_MIN, MISTRAL_TEMP_MAX);
this->fan_mode = fan->fan;
+15 -33
View File
@@ -47,46 +47,28 @@ static bool is_powered_on(const uint8_t *state) {
return get_field(state, POWER_1) == POWER_ON && get_field(state, POWER_2) == POWER_ON;
}
struct Code {
uint8_t value; // a climate enum, all of which are uint8_t
uint8_t code;
};
// The first entry of each table is the fallback in both directions
static constexpr Code MODES[] PROGMEM = {
using climate_ir::from_ir_code;
using climate_ir::IRCode;
using climate_ir::to_ir_code;
// Codes not listed fall back to auto (mode and fan) and to swing off
static constexpr IRCode MODES[] PROGMEM = {
{climate::CLIMATE_MODE_HEAT_COOL, 0}, {climate::CLIMATE_MODE_COOL, 1}, {climate::CLIMATE_MODE_DRY, 2},
{climate::CLIMATE_MODE_FAN_ONLY, 3}, {climate::CLIMATE_MODE_HEAT, 4},
};
static constexpr Code FANS[] PROGMEM = {
static constexpr IRCode FANS[] PROGMEM = {
{climate::CLIMATE_FAN_AUTO, 0},
{climate::CLIMATE_FAN_LOW, 2},
{climate::CLIMATE_FAN_MEDIUM, 4},
{climate::CLIMATE_FAN_HIGH, 5},
};
static constexpr Code SWINGS[] PROGMEM = {
static constexpr IRCode SWINGS[] PROGMEM = {
{climate::CLIMATE_SWING_OFF, 0b111},
{climate::CLIMATE_SWING_VERTICAL, 0b010},
{climate::CLIMATE_SWING_HORIZONTAL, 0b011},
{climate::CLIMATE_SWING_BOTH, 0b100},
};
// Returns the entry whose value (or code) matches key, else the first entry. Entries are copied out of PROGMEM.
static Code find_entry(const Code *table, size_t count, uint8_t key, bool match_code) {
Code entry{};
for (size_t i = 0; i < count; i++) {
progmem_memcpy(&entry, &table[i], sizeof(entry));
if ((match_code ? entry.code : entry.value) == key)
return entry;
}
progmem_memcpy(&entry, table, sizeof(entry));
return entry;
}
template<size_t N> static uint8_t to_code(const Code (&table)[N], uint8_t value) {
return find_entry(table, N, value, false).code;
}
template<typename T, size_t N> static T from_code(const Code (&table)[N], uint8_t code) {
return static_cast<T>(find_entry(table, N, code, true).value);
}
// Inverted count of the one bits in a section, skipping the two nibbles that hold the checksum itself.
// See https://github.com/crankyoldgit/IRremoteESP8266/issues/1538#issuecomment-894645947
static uint8_t section_checksum(const uint8_t *section) {
@@ -123,13 +105,13 @@ void SamsungClimate::transmit_state() {
if (on) {
progmem_memcpy(state, RESET_STATE, SAMSUNG_AC_STATE_LENGTH);
set_field(state, MODE, to_code(MODES, this->mode));
set_field(state, MODE, to_ir_code(MODES, this->mode, 0));
} // else keep the last mode so the power off frame still carries a valid state
const uint8_t temp = clamp<uint8_t>(this->target_temperature, SAMSUNG_AC_TEMP_MIN, SAMSUNG_AC_TEMP_MAX);
set_field(state, TEMP, temp - SAMSUNG_AC_TEMP_MIN);
set_field(state, SWING, to_code(SWINGS, this->swing_mode));
set_field(state, FAN, to_code(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)));
set_field(state, SWING, to_ir_code(SWINGS, this->swing_mode, 0b111));
set_field(state, FAN, to_ir_code(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), 0));
set_field(state, POWER_1, on ? POWER_ON : 0);
set_field(state, POWER_2, on ? POWER_ON : 0);
@@ -212,11 +194,11 @@ bool SamsungClimate::on_receive(remote_base::RemoteReceiveData data) {
memcpy(state, frame, SAMSUNG_AC_SECTION_LENGTH);
memcpy(state + SAMSUNG_AC_SECTION_LENGTH, frame + length - SAMSUNG_AC_SECTION_LENGTH, SAMSUNG_AC_SECTION_LENGTH);
this->swing_mode = from_code<climate::ClimateSwingMode>(SWINGS, get_field(state, SWING));
this->swing_mode = from_ir_code(SWINGS, get_field(state, SWING), climate::CLIMATE_SWING_OFF);
this->target_temperature = get_field(state, TEMP) + SAMSUNG_AC_TEMP_MIN;
this->fan_mode = from_code<climate::ClimateFanMode>(FANS, get_field(state, FAN));
this->mode =
is_powered_on(state) ? from_code<climate::ClimateMode>(MODES, get_field(state, MODE)) : climate::CLIMATE_MODE_OFF;
this->fan_mode = from_ir_code(FANS, get_field(state, FAN), climate::CLIMATE_FAN_AUTO);
this->mode = is_powered_on(state) ? from_ir_code(MODES, get_field(state, MODE), climate::CLIMATE_MODE_HEAT_COOL)
: climate::CLIMATE_MODE_OFF;
ESP_LOGD(TAG, "Received %u byte frame, power %s", length, ONOFF(is_powered_on(state)));
this->publish_state();
@@ -6,70 +6,32 @@ namespace esphome::siemens_ira211 {
using namespace remote_base;
struct PresetMapping {
climate::ClimatePreset preset;
IRA211Mode mode;
// Presets map to IRA211Mode and fan modes to IRA211Fan. Values the other side does not know fall back to comfort
// and auto.
static constexpr climate_ir::IRCode PRESETS[] PROGMEM = {
{climate::CLIMATE_PRESET_COMFORT, static_cast<uint8_t>(IRA211Mode::IRA211_MODE_COMFORT)},
{climate::CLIMATE_PRESET_ECO, static_cast<uint8_t>(IRA211Mode::IRA211_MODE_TIMER)},
};
static constexpr climate_ir::IRCode FANS[] PROGMEM = {
{climate::CLIMATE_FAN_AUTO, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_AUTO)},
{climate::CLIMATE_FAN_LOW, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_LOW)},
{climate::CLIMATE_FAN_MEDIUM, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_MEDIUM)},
{climate::CLIMATE_FAN_HIGH, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_HIGH)},
};
static const PresetMapping PRESETS[] PROGMEM = {
{climate::CLIMATE_PRESET_COMFORT, IRA211Mode::IRA211_MODE_COMFORT},
{climate::CLIMATE_PRESET_ECO, IRA211Mode::IRA211_MODE_TIMER},
};
struct FanMapping {
climate::ClimateFanMode fan_mode;
IRA211Fan fan;
};
static const FanMapping FANS[] PROGMEM = {
{climate::CLIMATE_FAN_AUTO, IRA211Fan::IRA211_FAN_AUTO},
{climate::CLIMATE_FAN_LOW, IRA211Fan::IRA211_FAN_LOW},
{climate::CLIMATE_FAN_MEDIUM, IRA211Fan::IRA211_FAN_MEDIUM},
{climate::CLIMATE_FAN_HIGH, IRA211Fan::IRA211_FAN_HIGH},
};
// The tables live in flash, so entries are copied out before use
template<typename T> static T load_entry(const T &slot) {
T entry;
progmem_memcpy(&entry, &slot, sizeof(T));
return entry;
}
// The first entry of each table is the fallback for values the other side does not know
static IRA211Mode to_ira211_mode(climate::ClimatePreset preset) {
for (const auto &slot : PRESETS) {
const auto entry = load_entry(slot);
if (entry.preset == preset)
return entry.mode;
}
return load_entry(PRESETS[0]).mode;
return static_cast<IRA211Mode>(
climate_ir::to_ir_code(PRESETS, preset, static_cast<uint8_t>(IRA211Mode::IRA211_MODE_COMFORT)));
}
static climate::ClimatePreset to_climate_preset(IRA211Mode mode) {
for (const auto &slot : PRESETS) {
const auto entry = load_entry(slot);
if (entry.mode == mode)
return entry.preset;
}
return load_entry(PRESETS[0]).preset;
return climate_ir::from_ir_code(PRESETS, static_cast<uint8_t>(mode), climate::CLIMATE_PRESET_COMFORT);
}
static IRA211Fan to_ira211_fan(climate::ClimateFanMode fan_mode) {
for (const auto &slot : FANS) {
const auto entry = load_entry(slot);
if (entry.fan_mode == fan_mode)
return entry.fan;
}
return load_entry(FANS[0]).fan;
return static_cast<IRA211Fan>(
climate_ir::to_ir_code(FANS, fan_mode, static_cast<uint8_t>(IRA211Fan::IRA211_FAN_AUTO)));
}
static climate::ClimateFanMode to_climate_fan_mode(IRA211Fan fan) {
for (const auto &slot : FANS) {
const auto entry = load_entry(slot);
if (entry.fan == fan)
return entry.fan_mode;
}
return load_entry(FANS[0]).fan_mode;
return climate_ir::from_ir_code(FANS, static_cast<uint8_t>(fan), climate::CLIMATE_FAN_AUTO);
}
void SiemensIRA211Climate::setup() {
@@ -0,0 +1,43 @@
#include <gtest/gtest.h>
#include "esphome/components/climate_ir/climate_ir.h"
namespace esphome::climate_ir::testing {
static constexpr IRCode FANS[] PROGMEM = {
{climate::CLIMATE_FAN_AUTO, 0x10},
{climate::CLIMATE_FAN_LOW, 0x20},
{climate::CLIMATE_FAN_HIGH, 0x30},
};
TEST(ClimateIRCodeTest, FindByValue) {
const auto entry = find_ir_code(FANS, climate::CLIMATE_FAN_LOW, false);
ASSERT_TRUE(entry.has_value());
EXPECT_EQ(entry->value, climate::CLIMATE_FAN_LOW);
EXPECT_EQ(entry->code, 0x20);
}
TEST(ClimateIRCodeTest, FindByCode) {
const auto entry = find_ir_code(FANS, 0x30, true);
ASSERT_TRUE(entry.has_value());
EXPECT_EQ(entry->value, climate::CLIMATE_FAN_HIGH);
}
TEST(ClimateIRCodeTest, FindMissing) {
EXPECT_FALSE(find_ir_code(FANS, climate::CLIMATE_FAN_MEDIUM, false).has_value());
// A code that is also a listed value must not match when looking up codes
EXPECT_FALSE(find_ir_code(FANS, climate::CLIMATE_FAN_LOW, true).has_value());
}
TEST(ClimateIRCodeTest, ToCode) {
EXPECT_EQ(to_ir_code(FANS, climate::CLIMATE_FAN_HIGH, 0x00), 0x30);
EXPECT_EQ(to_ir_code(FANS, climate::CLIMATE_FAN_MEDIUM, 0x00), 0x00);
}
TEST(ClimateIRCodeTest, FromCode) {
EXPECT_EQ(from_ir_code(FANS, 0x20, climate::CLIMATE_FAN_AUTO), climate::CLIMATE_FAN_LOW);
EXPECT_EQ(from_ir_code(FANS, 0x99, climate::CLIMATE_FAN_AUTO), climate::CLIMATE_FAN_AUTO);
// The current state as the fallback leaves it unchanged on an unknown code
EXPECT_EQ(from_ir_code(FANS, 0x99, climate::CLIMATE_FAN_MEDIUM), climate::CLIMATE_FAN_MEDIUM);
}
} // namespace esphome::climate_ir::testing