mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[remko_ar715] Add Remko AR-715 IR climate platform (#17173)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
pre-commit-ci-lite[bot]
J. Nick Koston
parent
81c275777d
commit
73d5bb3c97
@@ -472,6 +472,7 @@ esphome/components/rc522/* @glmnet
|
||||
esphome/components/rc522_i2c/* @glmnet
|
||||
esphome/components/rc522_spi/* @glmnet
|
||||
esphome/components/rd03d/* @jasstrong
|
||||
esphome/components/remko_ar715/* @naKruul
|
||||
esphome/components/renogy_inverter_ble/* @emilioaray-dev
|
||||
esphome/components/resampler/speaker/* @kahrendt
|
||||
esphome/components/restart/* @esphome/core
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
CODEOWNERS = ["@naKruul"]
|
||||
DOMAIN = "remko_ar715"
|
||||
@@ -0,0 +1,22 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import climate_ir
|
||||
from esphome.components.climate_ir import CONF_SUPPORTS_HEAT_COOL
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SUPPORTS_HEAT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
|
||||
remko_ar715_ns = cg.esphome_ns.namespace("remko_ar715")
|
||||
RemkoAr715Climate = remko_ar715_ns.class_("RemkoAr715Climate", climate_ir.ClimateIR)
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_schema(RemkoAr715Climate).extend(
|
||||
{
|
||||
cv.Optional(CONF_SUPPORTS_HEAT, default=False): cv.boolean_false,
|
||||
cv.Optional(CONF_SUPPORTS_HEAT_COOL, default=False): cv.boolean_false,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await climate_ir.new_climate_ir(config)
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "remko_ar715.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::remko_ar715 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "remko_ar715.climate");
|
||||
|
||||
// Timings from ir_ClimaButler.cpp (IRremoteESP8266), in microseconds
|
||||
static constexpr uint16_t BIT_MARK = 511;
|
||||
static constexpr uint16_t HDR_SPACE = 3492;
|
||||
static constexpr uint16_t ONE_SPACE = 1540;
|
||||
static constexpr uint16_t ZERO_SPACE = 548;
|
||||
static constexpr uint32_t CARRIER_FREQUENCY = 38000;
|
||||
static constexpr uint8_t BITS = 52;
|
||||
|
||||
static constexpr uint8_t POWER_ON = 0x3;
|
||||
static constexpr uint8_t POWER_OFF = 0x0;
|
||||
static constexpr uint8_t SWING_ON = 0x5;
|
||||
static constexpr uint8_t SWING_OFF = 0x7;
|
||||
|
||||
// Mode/fan byte: high nibble N3, low nibble N2
|
||||
static constexpr uint8_t MODE_FAN_AUTO = 0x80;
|
||||
static constexpr uint8_t MODE_FAN_LOW = 0xA2;
|
||||
static constexpr uint8_t MODE_FAN_MEDIUM = 0x91;
|
||||
static constexpr uint8_t MODE_FAN_HIGH = 0x88;
|
||||
static constexpr uint8_t MODE_DRY = 0xA4;
|
||||
static constexpr uint8_t MODE_FAN_ONLY = 0xA3;
|
||||
|
||||
static uint8_t fan_mode_byte(climate::ClimateFanMode fan_mode) {
|
||||
switch (fan_mode) {
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
return MODE_FAN_HIGH;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
return MODE_FAN_MEDIUM;
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
return MODE_FAN_LOW;
|
||||
default:
|
||||
return MODE_FAN_AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
// 52-bit word, 13 nibbles MSB first:
|
||||
// N12=0x8 (fixed), N11=power, N10..N5=0 (timer, unused), N4=swing,
|
||||
// N3,N2=mode/fan, N1=temp-16, N0=checksum (0xF - sum(N12..N1)) & 0xF
|
||||
static uint64_t build_frame(uint8_t power, uint8_t swing, uint8_t mode_fan, uint8_t temp) {
|
||||
const uint8_t temp_nibble = (temp - 16) & 0xF;
|
||||
const uint8_t sum = 0x8 + power + swing + (mode_fan >> 4) + (mode_fan & 0xF) + temp_nibble;
|
||||
const uint8_t checksum = (0xF - sum) & 0xF;
|
||||
return (uint64_t{0x8} << 48) | (uint64_t{power} << 44) | (uint32_t{swing} << 16) | (uint32_t{mode_fan} << 8) |
|
||||
(temp_nibble << 4) | checksum;
|
||||
}
|
||||
|
||||
static void send_frame(remote_base::RemoteTransmitData *data, uint64_t code) {
|
||||
data->set_carrier_frequency(CARRIER_FREQUENCY);
|
||||
data->reserve(2 + BITS * 2 + 2);
|
||||
data->item(BIT_MARK, HDR_SPACE);
|
||||
for (int i = BITS - 1; i >= 0; i--)
|
||||
data->item(BIT_MARK, (code >> i) & 1 ? ONE_SPACE : ZERO_SPACE);
|
||||
// Stop bit and inter-frame gap
|
||||
data->item(BIT_MARK, HDR_SPACE);
|
||||
}
|
||||
|
||||
void RemkoAr715Climate::transmit_state() {
|
||||
const uint8_t temp = lroundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX));
|
||||
const bool power = this->mode != climate::CLIMATE_MODE_OFF;
|
||||
const bool swing = this->swing_mode == climate::CLIMATE_SWING_VERTICAL;
|
||||
|
||||
// DRY and FAN_ONLY carry no fan speed: mode and fan share the same nibbles
|
||||
uint8_t mode_fan;
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
mode_fan = MODE_DRY;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
mode_fan = MODE_FAN_ONLY;
|
||||
break;
|
||||
default:
|
||||
mode_fan = fan_mode_byte(this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO));
|
||||
break;
|
||||
}
|
||||
|
||||
const uint64_t code = build_frame(power ? POWER_ON : POWER_OFF, swing ? SWING_ON : SWING_OFF, mode_fan, temp);
|
||||
ESP_LOGV(TAG, "Sending power=%s temp=%u swing=%s mode_fan=0x%02X code=0x%05" PRIX32 "%08" PRIX32, ONOFF(power), temp,
|
||||
ONOFF(swing), mode_fan, static_cast<uint32_t>(code >> 32), static_cast<uint32_t>(code));
|
||||
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
send_frame(transmit.get_data(), code);
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
} // namespace esphome::remko_ar715
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/climate_ir/climate_ir.h"
|
||||
|
||||
namespace esphome::remko_ar715 {
|
||||
|
||||
// Remko AR-715 IR remote: 52-bit pulse-distance frame, full state in every command.
|
||||
// IRremoteESP8266 calls it the ClimaButler protocol after a rebrand; Remko is the manufacturer.
|
||||
// Protocol analysis: https://github.com/crankyoldgit/IRremoteESP8266/issues/1812
|
||||
//
|
||||
// Supported devices:
|
||||
// - Remko RKL series (RKL 490, 491, 494, 495) with AR-715 remote
|
||||
// - Fischer ClimaButler RCS-SD43UAI / RCS-SD43UWI
|
||||
// - TROTEC PAC 4600
|
||||
// - Novamatic CL 990 / CL 1590
|
||||
// - Rexair C15000N
|
||||
// - freecom RCS-SD43UAI / RCS-SD43UWI
|
||||
|
||||
static constexpr float TEMP_MIN = 16.0f;
|
||||
static constexpr float TEMP_MAX = 30.0f;
|
||||
|
||||
class RemkoAr715Climate final : public climate_ir::ClimateIR {
|
||||
public:
|
||||
RemkoAr715Climate()
|
||||
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX,
|
||||
1.0f, // temperature step
|
||||
true, // supports dry
|
||||
true, // supports fan only
|
||||
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH},
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {}
|
||||
|
||||
protected:
|
||||
void transmit_state() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::remko_ar715
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The harness walks dependencies from the component manifest, not from the
|
||||
# climate platform, so declare the platform's auto-load here.
|
||||
manifest.auto_load = ["climate_ir", "climate"]
|
||||
@@ -0,0 +1,3 @@
|
||||
climate:
|
||||
- platform: remko_ar715
|
||||
name: Test RemkoAr715
|
||||
@@ -0,0 +1,106 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/components/remko_ar715/remko_ar715.h"
|
||||
|
||||
namespace esphome::remko_ar715::testing {
|
||||
|
||||
static constexpr int32_t BIT_MARK = 511;
|
||||
static constexpr int32_t HDR_SPACE = 3492;
|
||||
static constexpr int32_t ONE_SPACE = 1540;
|
||||
static constexpr int32_t ZERO_SPACE = 548;
|
||||
static constexpr int BITS = 52;
|
||||
|
||||
/// The waveform the component must produce for a code: header, 52 bits MSB first, stop bit
|
||||
static remote_base::RawTimings waveform(uint64_t code) {
|
||||
remote_base::RawTimings raw;
|
||||
raw.push_back(BIT_MARK);
|
||||
raw.push_back(-HDR_SPACE);
|
||||
for (int i = BITS - 1; i >= 0; i--) {
|
||||
raw.push_back(BIT_MARK);
|
||||
raw.push_back((code >> i) & 1 ? -ONE_SPACE : -ZERO_SPACE);
|
||||
}
|
||||
raw.push_back(BIT_MARK);
|
||||
raw.push_back(-HDR_SPACE);
|
||||
return raw;
|
||||
}
|
||||
|
||||
/// Decodes a frame back into its code so a mismatch is readable
|
||||
static uint64_t code_of(const remote_base::RawTimings &raw) {
|
||||
uint64_t code = 0;
|
||||
for (int bit = 0; bit < BITS; bit++) {
|
||||
const size_t space = 3 + 2 * bit;
|
||||
code = (code << 1) | (space < raw.size() && -raw[space] > 1000 ? 1 : 0);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
class CapturingTransmitter : public remote_base::RemoteTransmitterBase {
|
||||
public:
|
||||
CapturingTransmitter() : RemoteTransmitterBase(nullptr) {}
|
||||
std::vector<remote_base::RawTimings> frames;
|
||||
|
||||
protected:
|
||||
void send_internal(uint32_t send_times, uint32_t send_wait) override {
|
||||
this->frames.push_back(this->temp_.get_data());
|
||||
}
|
||||
};
|
||||
|
||||
struct Fixture {
|
||||
CapturingTransmitter transmitter;
|
||||
RemkoAr715Climate climate;
|
||||
|
||||
Fixture() {
|
||||
this->climate.set_transmitter(&this->transmitter);
|
||||
this->climate.setup();
|
||||
}
|
||||
|
||||
void command(climate::ClimateMode mode, float temperature, climate::ClimateFanMode fan,
|
||||
climate::ClimateSwingMode swing) {
|
||||
auto call = this->climate.make_call();
|
||||
call.set_mode(mode);
|
||||
call.set_target_temperature(temperature);
|
||||
call.set_fan_mode(fan);
|
||||
call.set_swing_mode(swing);
|
||||
call.perform();
|
||||
}
|
||||
|
||||
const remote_base::RawTimings &last_frame() const { return this->transmitter.frames.back(); }
|
||||
};
|
||||
|
||||
static void expect_code(const remote_base::RawTimings &actual, uint64_t code) {
|
||||
EXPECT_EQ(actual, waveform(code)) << std::hex << "actual 0x" << code_of(actual) << "\nexpected 0x" << code;
|
||||
}
|
||||
|
||||
// Codes captured from the AR-715 remote in IRremoteESP8266 issue 1812
|
||||
TEST(RemkoAr715, EncodesCapturedFrames) {
|
||||
Fixture f;
|
||||
|
||||
f.command(climate::CLIMATE_MODE_COOL, 16, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_SWING_VERTICAL);
|
||||
expect_code(f.last_frame(), 0x830000005880FULL);
|
||||
|
||||
// Power off keeps the rest of the state in the frame
|
||||
f.command(climate::CLIMATE_MODE_OFF, 16, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_SWING_VERTICAL);
|
||||
expect_code(f.last_frame(), 0x8000000058802ULL);
|
||||
|
||||
f.command(climate::CLIMATE_MODE_COOL, 25, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_SWING_OFF);
|
||||
expect_code(f.last_frame(), 0x830000007919AULL);
|
||||
|
||||
f.command(climate::CLIMATE_MODE_COOL, 20, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_SWING_OFF);
|
||||
expect_code(f.last_frame(), 0x830000007914FULL);
|
||||
}
|
||||
|
||||
// Dry and fan-only replace the fan nibbles; the fan speed is not sent
|
||||
TEST(RemkoAr715, DryAndFanOnlyIgnoreTheFanSpeed) {
|
||||
Fixture f;
|
||||
|
||||
f.command(climate::CLIMATE_MODE_DRY, 22, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_SWING_OFF);
|
||||
expect_code(f.last_frame(), 0x830000007A469ULL);
|
||||
|
||||
f.command(climate::CLIMATE_MODE_FAN_ONLY, 22, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_SWING_OFF);
|
||||
expect_code(f.last_frame(), 0x830000007A36AULL);
|
||||
}
|
||||
|
||||
} // namespace esphome::remko_ar715::testing
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
remko_ar715: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
remko_ar715: !include common.yaml
|
||||
Reference in New Issue
Block a user