mirror of
https://github.com/esphome/esphome.git
synced 2024-11-28 09:44:12 +01:00
Climate component for Ballu air conditioners with remote model YKR-K/002E (#1939)
This commit is contained in:
parent
3b940b1c04
commit
964ab65497
5 changed files with 292 additions and 0 deletions
|
@ -19,6 +19,7 @@ esphome/components/api/* @OttoWinter
|
||||||
esphome/components/async_tcp/* @OttoWinter
|
esphome/components/async_tcp/* @OttoWinter
|
||||||
esphome/components/atc_mithermometer/* @ahpohl
|
esphome/components/atc_mithermometer/* @ahpohl
|
||||||
esphome/components/b_parasite/* @rbaron
|
esphome/components/b_parasite/* @rbaron
|
||||||
|
esphome/components/ballu/* @bazuchan
|
||||||
esphome/components/bang_bang/* @OttoWinter
|
esphome/components/bang_bang/* @OttoWinter
|
||||||
esphome/components/binary_sensor/* @esphome/core
|
esphome/components/binary_sensor/* @esphome/core
|
||||||
esphome/components/ble_client/* @buxtronix
|
esphome/components/ble_client/* @buxtronix
|
||||||
|
|
0
esphome/components/ballu/__init__.py
Normal file
0
esphome/components/ballu/__init__.py
Normal file
239
esphome/components/ballu/ballu.cpp
Normal file
239
esphome/components/ballu/ballu.cpp
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
#include "ballu.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace ballu {
|
||||||
|
|
||||||
|
static const char *const TAG = "ballu.climate";
|
||||||
|
|
||||||
|
const uint16_t BALLU_HEADER_MARK = 9000;
|
||||||
|
const uint16_t BALLU_HEADER_SPACE = 4500;
|
||||||
|
const uint16_t BALLU_BIT_MARK = 575;
|
||||||
|
const uint16_t BALLU_ONE_SPACE = 1675;
|
||||||
|
const uint16_t BALLU_ZERO_SPACE = 550;
|
||||||
|
|
||||||
|
const uint32_t BALLU_CARRIER_FREQUENCY = 38000;
|
||||||
|
|
||||||
|
const uint8_t BALLU_STATE_LENGTH = 13;
|
||||||
|
|
||||||
|
const uint8_t BALLU_AUTO = 0;
|
||||||
|
const uint8_t BALLU_COOL = 0x20;
|
||||||
|
const uint8_t BALLU_DRY = 0x40;
|
||||||
|
const uint8_t BALLU_HEAT = 0x80;
|
||||||
|
const uint8_t BALLU_FAN = 0xc0;
|
||||||
|
|
||||||
|
const uint8_t BALLU_FAN_AUTO = 0xa0;
|
||||||
|
const uint8_t BALLU_FAN_HIGH = 0x20;
|
||||||
|
const uint8_t BALLU_FAN_MED = 0x40;
|
||||||
|
const uint8_t BALLU_FAN_LOW = 0x60;
|
||||||
|
|
||||||
|
const uint8_t BALLU_SWING_VER = 0x07;
|
||||||
|
const uint8_t BALLU_SWING_HOR = 0xe0;
|
||||||
|
const uint8_t BALLU_POWER = 0x20;
|
||||||
|
|
||||||
|
void BalluClimate::transmit_state() {
|
||||||
|
uint8_t remote_state[BALLU_STATE_LENGTH] = {0};
|
||||||
|
|
||||||
|
auto temp = (uint8_t) roundf(clamp(this->target_temperature, YKR_K_002E_TEMP_MIN, YKR_K_002E_TEMP_MAX));
|
||||||
|
auto swing_ver =
|
||||||
|
((this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || (this->swing_mode == climate::CLIMATE_SWING_BOTH));
|
||||||
|
auto swing_hor =
|
||||||
|
((this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (this->swing_mode == climate::CLIMATE_SWING_BOTH));
|
||||||
|
|
||||||
|
remote_state[0] = 0xc3;
|
||||||
|
remote_state[1] = ((temp - 8) << 3) | (swing_ver ? 0 : BALLU_SWING_VER);
|
||||||
|
remote_state[2] = swing_hor ? 0 : BALLU_SWING_HOR;
|
||||||
|
remote_state[9] = (this->mode == climate::CLIMATE_MODE_OFF) ? 0 : BALLU_POWER;
|
||||||
|
remote_state[11] = 0x1e;
|
||||||
|
|
||||||
|
// Fan speed
|
||||||
|
switch (this->fan_mode.value()) {
|
||||||
|
case climate::CLIMATE_FAN_HIGH:
|
||||||
|
remote_state[4] |= BALLU_FAN_HIGH;
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_FAN_MEDIUM:
|
||||||
|
remote_state[4] |= BALLU_FAN_MED;
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_FAN_LOW:
|
||||||
|
remote_state[4] |= BALLU_FAN_LOW;
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_FAN_AUTO:
|
||||||
|
remote_state[4] |= BALLU_FAN_AUTO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode
|
||||||
|
switch (this->mode) {
|
||||||
|
case climate::CLIMATE_MODE_AUTO:
|
||||||
|
remote_state[6] |= BALLU_AUTO;
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_MODE_HEAT:
|
||||||
|
remote_state[6] |= BALLU_HEAT;
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_MODE_COOL:
|
||||||
|
remote_state[6] |= BALLU_COOL;
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_MODE_DRY:
|
||||||
|
remote_state[6] |= BALLU_DRY;
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||||
|
remote_state[6] |= BALLU_FAN;
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_MODE_OFF:
|
||||||
|
remote_state[6] |= BALLU_AUTO;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checksum
|
||||||
|
for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++)
|
||||||
|
remote_state[12] += remote_state[i];
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0],
|
||||||
|
remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6],
|
||||||
|
remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12]);
|
||||||
|
|
||||||
|
// Send code
|
||||||
|
auto transmit = this->transmitter_->transmit();
|
||||||
|
auto data = transmit.get_data();
|
||||||
|
|
||||||
|
data->set_carrier_frequency(38000);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
data->mark(BALLU_HEADER_MARK);
|
||||||
|
data->space(BALLU_HEADER_SPACE);
|
||||||
|
// Data
|
||||||
|
for (uint8_t i : remote_state) {
|
||||||
|
for (uint8_t j = 0; j < 8; j++) {
|
||||||
|
data->mark(BALLU_BIT_MARK);
|
||||||
|
bool bit = i & (1 << j);
|
||||||
|
data->space(bit ? BALLU_ONE_SPACE : BALLU_ZERO_SPACE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Footer
|
||||||
|
data->mark(BALLU_BIT_MARK);
|
||||||
|
|
||||||
|
transmit.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||||
|
// Validate header
|
||||||
|
if (!data.expect_item(BALLU_HEADER_MARK, BALLU_HEADER_SPACE)) {
|
||||||
|
ESP_LOGV(TAG, "Header fail");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t remote_state[BALLU_STATE_LENGTH] = {0};
|
||||||
|
// Read all bytes.
|
||||||
|
for (int i = 0; i < BALLU_STATE_LENGTH; i++) {
|
||||||
|
// Read bit
|
||||||
|
for (int j = 0; j < 8; j++) {
|
||||||
|
if (data.expect_item(BALLU_BIT_MARK, BALLU_ONE_SPACE))
|
||||||
|
remote_state[i] |= 1 << j;
|
||||||
|
|
||||||
|
else if (!data.expect_item(BALLU_BIT_MARK, BALLU_ZERO_SPACE)) {
|
||||||
|
ESP_LOGV(TAG, "Byte %d bit %d fail", i, j);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGVV(TAG, "Byte %d %02X", i, remote_state[i]);
|
||||||
|
}
|
||||||
|
// Validate footer
|
||||||
|
if (!data.expect_mark(BALLU_BIT_MARK)) {
|
||||||
|
ESP_LOGV(TAG, "Footer fail");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t checksum = 0;
|
||||||
|
// Calculate checksum and compare with signal value.
|
||||||
|
for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++)
|
||||||
|
checksum += remote_state[i];
|
||||||
|
|
||||||
|
if (checksum != remote_state[BALLU_STATE_LENGTH - 1]) {
|
||||||
|
ESP_LOGVV(TAG, "Checksum fail");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0],
|
||||||
|
remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6],
|
||||||
|
remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12]);
|
||||||
|
|
||||||
|
// verify header remote code
|
||||||
|
if (remote_state[0] != 0xc3)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// powr on/off button
|
||||||
|
ESP_LOGV(TAG, "Power: %02X", (remote_state[9] & BALLU_POWER));
|
||||||
|
|
||||||
|
if ((remote_state[9] & BALLU_POWER) != BALLU_POWER) {
|
||||||
|
this->mode = climate::CLIMATE_MODE_OFF;
|
||||||
|
} else {
|
||||||
|
auto mode = remote_state[6] & 0xe0;
|
||||||
|
ESP_LOGV(TAG, "Mode: %02X", mode);
|
||||||
|
switch (mode) {
|
||||||
|
case BALLU_HEAT:
|
||||||
|
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||||
|
break;
|
||||||
|
case BALLU_COOL:
|
||||||
|
this->mode = climate::CLIMATE_MODE_COOL;
|
||||||
|
break;
|
||||||
|
case BALLU_DRY:
|
||||||
|
this->mode = climate::CLIMATE_MODE_DRY;
|
||||||
|
break;
|
||||||
|
case BALLU_FAN:
|
||||||
|
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||||
|
break;
|
||||||
|
case BALLU_AUTO:
|
||||||
|
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set received temp
|
||||||
|
int temp = remote_state[1] & 0xf8;
|
||||||
|
ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
|
||||||
|
temp = ((uint8_t) temp >> 3) + 8;
|
||||||
|
ESP_LOGVV(TAG, "Temperature Climate: %u", temp);
|
||||||
|
this->target_temperature = temp;
|
||||||
|
|
||||||
|
// Set received fan speed
|
||||||
|
auto fan = remote_state[4] & 0xe0;
|
||||||
|
ESP_LOGVV(TAG, "Fan: %02X", fan);
|
||||||
|
switch (fan) {
|
||||||
|
case BALLU_FAN_HIGH:
|
||||||
|
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||||
|
break;
|
||||||
|
case BALLU_FAN_MED:
|
||||||
|
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||||
|
break;
|
||||||
|
case BALLU_FAN_LOW:
|
||||||
|
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||||
|
break;
|
||||||
|
case BALLU_FAN_AUTO:
|
||||||
|
default:
|
||||||
|
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set received swing status
|
||||||
|
ESP_LOGVV(TAG, "Swing status: %02X %02X", remote_state[1] & BALLU_SWING_VER, remote_state[2] & BALLU_SWING_HOR);
|
||||||
|
if (((remote_state[1] & BALLU_SWING_VER) != BALLU_SWING_VER) &&
|
||||||
|
((remote_state[2] & BALLU_SWING_HOR) != BALLU_SWING_HOR)) {
|
||||||
|
this->swing_mode = climate::CLIMATE_SWING_BOTH;
|
||||||
|
} else if ((remote_state[1] & BALLU_SWING_VER) != BALLU_SWING_VER) {
|
||||||
|
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||||
|
} else if ((remote_state[2] & BALLU_SWING_HOR) != BALLU_SWING_HOR) {
|
||||||
|
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
||||||
|
} else {
|
||||||
|
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->publish_state();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ballu
|
||||||
|
} // namespace esphome
|
31
esphome/components/ballu/ballu.h
Normal file
31
esphome/components/ballu/ballu.h
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/climate_ir/climate_ir.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace ballu {
|
||||||
|
|
||||||
|
// Support for Ballu air conditioners with YKR-K/002E remote
|
||||||
|
|
||||||
|
// Temperature
|
||||||
|
const float YKR_K_002E_TEMP_MIN = 16.0;
|
||||||
|
const float YKR_K_002E_TEMP_MAX = 32.0;
|
||||||
|
|
||||||
|
class BalluClimate : public climate_ir::ClimateIR {
|
||||||
|
public:
|
||||||
|
BalluClimate()
|
||||||
|
: climate_ir::ClimateIR(YKR_K_002E_TEMP_MIN, YKR_K_002E_TEMP_MAX, 1.0f, true, true,
|
||||||
|
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
||||||
|
climate::CLIMATE_FAN_HIGH},
|
||||||
|
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL,
|
||||||
|
climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
/// Transmit via IR the state of this climate controller.
|
||||||
|
void transmit_state() override;
|
||||||
|
/// Handle received IR Buffer
|
||||||
|
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ballu
|
||||||
|
} // namespace esphome
|
21
esphome/components/ballu/climate.py
Normal file
21
esphome/components/ballu/climate.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import climate_ir
|
||||||
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
|
AUTO_LOAD = ["climate_ir"]
|
||||||
|
CODEOWNERS = ["@bazuchan"]
|
||||||
|
|
||||||
|
ballu_ns = cg.esphome_ns.namespace("ballu")
|
||||||
|
BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(BalluClimate),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await climate_ir.register_climate_ir(var, config)
|
Loading…
Reference in a new issue