mirror of
https://github.com/esphome/esphome.git
synced 2024-11-29 18:24:13 +01:00
Add coolix receiver (#645)
* add coolix receiver a * lint - added comments * Lint * target temp neve be nan
This commit is contained in:
parent
1d5f8d5a52
commit
4118a289a6
3 changed files with 135 additions and 42 deletions
|
@ -1,6 +1,6 @@
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.components import climate, remote_transmitter, sensor
|
from esphome.components import climate, remote_transmitter, remote_receiver, sensor
|
||||||
from esphome.const import CONF_ID, CONF_SENSOR
|
from esphome.const import CONF_ID, CONF_SENSOR
|
||||||
|
|
||||||
AUTO_LOAD = ['sensor']
|
AUTO_LOAD = ['sensor']
|
||||||
|
@ -9,12 +9,14 @@ coolix_ns = cg.esphome_ns.namespace('coolix')
|
||||||
CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component)
|
CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component)
|
||||||
|
|
||||||
CONF_TRANSMITTER_ID = 'transmitter_id'
|
CONF_TRANSMITTER_ID = 'transmitter_id'
|
||||||
|
CONF_RECEIVER_ID = 'receiver_id'
|
||||||
CONF_SUPPORTS_HEAT = 'supports_heat'
|
CONF_SUPPORTS_HEAT = 'supports_heat'
|
||||||
CONF_SUPPORTS_COOL = 'supports_cool'
|
CONF_SUPPORTS_COOL = 'supports_cool'
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({
|
CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_id(CoolixClimate),
|
cv.GenerateID(): cv.declare_id(CoolixClimate),
|
||||||
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent),
|
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent),
|
||||||
|
cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent),
|
||||||
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
|
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
|
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
|
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
|
||||||
|
@ -31,6 +33,9 @@ def to_code(config):
|
||||||
if CONF_SENSOR in config:
|
if CONF_SENSOR in config:
|
||||||
sens = yield cg.get_variable(config[CONF_SENSOR])
|
sens = yield cg.get_variable(config[CONF_SENSOR])
|
||||||
cg.add(var.set_sensor(sens))
|
cg.add(var.set_sensor(sens))
|
||||||
|
if CONF_RECEIVER_ID in config:
|
||||||
|
receiver = yield cg.get_variable(config[CONF_RECEIVER_ID])
|
||||||
|
cg.add(receiver.register_listener(var))
|
||||||
|
|
||||||
transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
|
transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
|
||||||
cg.add(var.set_transmitter(transmitter))
|
cg.add(var.set_transmitter(transmitter))
|
||||||
|
|
|
@ -7,15 +7,24 @@ namespace coolix {
|
||||||
static const char *TAG = "coolix.climate";
|
static const char *TAG = "coolix.climate";
|
||||||
|
|
||||||
const uint32_t COOLIX_OFF = 0xB27BE0;
|
const uint32_t COOLIX_OFF = 0xB27BE0;
|
||||||
|
const uint32_t COOLIX_SWING = 0xB26BE0;
|
||||||
|
const uint32_t COOLIX_LED = 0xB5F5A5;
|
||||||
|
const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6;
|
||||||
|
|
||||||
// On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore.
|
// On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore.
|
||||||
const uint32_t COOLIX_DEFAULT_STATE = 0xB2BFC8;
|
const uint32_t COOLIX_DEFAULT_STATE = 0xB2BFC8;
|
||||||
const uint32_t COOLIX_DEFAULT_STATE_AUTO_24_FAN = 0xB21F48;
|
const uint32_t COOLIX_DEFAULT_STATE_AUTO_24_FAN = 0xB21F48;
|
||||||
const uint8_t COOLIX_COOL = 0b00;
|
const uint8_t COOLIX_COOL = 0b0000;
|
||||||
const uint8_t COOLIX_DRY = 0b01;
|
const uint8_t COOLIX_DRY_FAN = 0b0100;
|
||||||
const uint8_t COOLIX_AUTO = 0b10;
|
const uint8_t COOLIX_AUTO = 0b1000;
|
||||||
const uint8_t COOLIX_HEAT = 0b11;
|
const uint8_t COOLIX_HEAT = 0b1100;
|
||||||
const uint8_t COOLIX_FAN = 4; // Synthetic.
|
const uint32_t COOLIX_MODE_MASK = 0b1100;
|
||||||
const uint32_t COOLIX_MODE_MASK = 0b000000000000000000001100; // 0xC
|
const uint32_t COOLIX_FAN_MASK = 0xF000;
|
||||||
|
const uint32_t COOLIX_FAN_DRY = 0x1000;
|
||||||
|
const uint32_t COOLIX_FAN_AUTO = 0xB000;
|
||||||
|
const uint32_t COOLIX_FAN_MIN = 0x9000;
|
||||||
|
const uint32_t COOLIX_FAN_MED = 0x5000;
|
||||||
|
const uint32_t COOLIX_FAN_MAX = 0x3000;
|
||||||
|
|
||||||
// Temperature
|
// Temperature
|
||||||
const uint8_t COOLIX_TEMP_MIN = 17; // Celsius
|
const uint8_t COOLIX_TEMP_MIN = 17; // Celsius
|
||||||
|
@ -41,22 +50,13 @@ const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
// Pulse parms are *50-100 for the Mark and *50+100 for the space
|
static const uint32_t BIT_MARK_US = 660;
|
||||||
// First MARK is the one after the long gap
|
static const uint32_t HEADER_MARK_US = 560 * 8;
|
||||||
// pulse parameters in usec
|
static const uint32_t HEADER_SPACE_US = 560 * 8;
|
||||||
const uint16_t COOLIX_TICK = 560; // Approximately 21 cycles at 38kHz
|
static const uint32_t BIT_ONE_SPACE_US = 1500;
|
||||||
const uint16_t COOLIX_BIT_MARK_TICKS = 1;
|
static const uint32_t BIT_ZERO_SPACE_US = 450;
|
||||||
const uint16_t COOLIX_BIT_MARK = COOLIX_BIT_MARK_TICKS * COOLIX_TICK;
|
static const uint32_t FOOTER_MARK_US = BIT_MARK_US;
|
||||||
const uint16_t COOLIX_ONE_SPACE_TICKS = 3;
|
static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US;
|
||||||
const uint16_t COOLIX_ONE_SPACE = COOLIX_ONE_SPACE_TICKS * COOLIX_TICK;
|
|
||||||
const uint16_t COOLIX_ZERO_SPACE_TICKS = 1;
|
|
||||||
const uint16_t COOLIX_ZERO_SPACE = COOLIX_ZERO_SPACE_TICKS * COOLIX_TICK;
|
|
||||||
const uint16_t COOLIX_HEADER_MARK_TICKS = 8;
|
|
||||||
const uint16_t COOLIX_HEADER_MARK = COOLIX_HEADER_MARK_TICKS * COOLIX_TICK;
|
|
||||||
const uint16_t COOLIX_HEADER_SPACE_TICKS = 8;
|
|
||||||
const uint16_t COOLIX_HEADER_SPACE = COOLIX_HEADER_SPACE_TICKS * COOLIX_TICK;
|
|
||||||
const uint16_t COOLIX_MIN_GAP_TICKS = COOLIX_HEADER_MARK_TICKS + COOLIX_ZERO_SPACE_TICKS;
|
|
||||||
const uint16_t COOLIX_MIN_GAP = COOLIX_MIN_GAP_TICKS * COOLIX_TICK;
|
|
||||||
|
|
||||||
const uint16_t COOLIX_BITS = 24;
|
const uint16_t COOLIX_BITS = 24;
|
||||||
|
|
||||||
|
@ -90,10 +90,13 @@ void CoolixClimate::setup() {
|
||||||
restore->apply(this);
|
restore->apply(this);
|
||||||
} else {
|
} else {
|
||||||
// restore from defaults
|
// restore from defaults
|
||||||
this->mode = climate::CLIMATE_MODE_AUTO;
|
this->mode = climate::CLIMATE_MODE_OFF;
|
||||||
// initialize target temperature to some value so that it's not NAN
|
// initialize target temperature to some value so that it's not NAN
|
||||||
this->target_temperature = roundf(this->current_temperature);
|
this->target_temperature = (uint8_t) roundf(clamp(this->current_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX));
|
||||||
}
|
}
|
||||||
|
// never send nan as temperature. HA will disable the user to change the temperature.
|
||||||
|
if (isnan(this->target_temperature))
|
||||||
|
this->target_temperature = 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CoolixClimate::control(const climate::ClimateCall &call) {
|
void CoolixClimate::control(const climate::ClimateCall &call) {
|
||||||
|
@ -111,10 +114,10 @@ void CoolixClimate::transmit_state_() {
|
||||||
|
|
||||||
switch (this->mode) {
|
switch (this->mode) {
|
||||||
case climate::CLIMATE_MODE_COOL:
|
case climate::CLIMATE_MODE_COOL:
|
||||||
remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | (COOLIX_COOL << 2);
|
remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_COOL;
|
||||||
break;
|
break;
|
||||||
case climate::CLIMATE_MODE_HEAT:
|
case climate::CLIMATE_MODE_HEAT:
|
||||||
remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | (COOLIX_HEAT << 2);
|
remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_HEAT;
|
||||||
break;
|
break;
|
||||||
case climate::CLIMATE_MODE_AUTO:
|
case climate::CLIMATE_MODE_AUTO:
|
||||||
remote_state = COOLIX_DEFAULT_STATE_AUTO_24_FAN;
|
remote_state = COOLIX_DEFAULT_STATE_AUTO_24_FAN;
|
||||||
|
@ -127,10 +130,10 @@ void CoolixClimate::transmit_state_() {
|
||||||
if (this->mode != climate::CLIMATE_MODE_OFF) {
|
if (this->mode != climate::CLIMATE_MODE_OFF) {
|
||||||
auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX));
|
auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX));
|
||||||
remote_state &= ~COOLIX_TEMP_MASK; // Clear the old temp.
|
remote_state &= ~COOLIX_TEMP_MASK; // Clear the old temp.
|
||||||
remote_state |= (COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4);
|
remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Sending coolix code: %u", remote_state);
|
ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state);
|
||||||
|
|
||||||
auto transmit = this->transmitter_->transmit();
|
auto transmit = this->transmitter_->transmit();
|
||||||
auto data = transmit.get_data();
|
auto data = transmit.get_data();
|
||||||
|
@ -139,32 +142,113 @@ void CoolixClimate::transmit_state_() {
|
||||||
uint16_t repeat = 1;
|
uint16_t repeat = 1;
|
||||||
for (uint16_t r = 0; r <= repeat; r++) {
|
for (uint16_t r = 0; r <= repeat; r++) {
|
||||||
// Header
|
// Header
|
||||||
data->mark(COOLIX_HEADER_MARK);
|
data->mark(HEADER_MARK_US);
|
||||||
data->space(COOLIX_HEADER_SPACE);
|
data->space(HEADER_SPACE_US);
|
||||||
// Data
|
// Data
|
||||||
// Break data into byte segments, starting at the Most Significant
|
// Break data into bytes, starting at the Most Significant
|
||||||
// Byte. Each byte then being sent normal, then followed inverted.
|
// Byte. Each byte then being sent normal, then followed inverted.
|
||||||
for (uint16_t i = 8; i <= COOLIX_BITS; i += 8) {
|
for (uint16_t i = 8; i <= COOLIX_BITS; i += 8) {
|
||||||
// Grab a bytes worth of data.
|
// Grab a bytes worth of data.
|
||||||
uint8_t segment = (remote_state >> (COOLIX_BITS - i)) & 0xFF;
|
uint8_t byte = (remote_state >> (COOLIX_BITS - i)) & 0xFF;
|
||||||
// Normal
|
// Normal
|
||||||
for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) {
|
for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) {
|
||||||
data->mark(COOLIX_BIT_MARK);
|
data->mark(BIT_MARK_US);
|
||||||
data->space((segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE);
|
data->space((byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US);
|
||||||
}
|
}
|
||||||
// Inverted
|
// Inverted
|
||||||
for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) {
|
for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) {
|
||||||
data->mark(COOLIX_BIT_MARK);
|
data->mark(BIT_MARK_US);
|
||||||
data->space(!(segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE);
|
data->space(!(byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Footer
|
// Footer
|
||||||
data->mark(COOLIX_BIT_MARK);
|
data->mark(BIT_MARK_US);
|
||||||
data->space(COOLIX_MIN_GAP); // Pause before repeating
|
data->space(FOOTER_SPACE_US); // Pause before repeating
|
||||||
}
|
}
|
||||||
|
|
||||||
transmit.perform();
|
transmit.perform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||||
|
// Decoded remote state y 3 bytes long code.
|
||||||
|
uint32_t remote_state = 0;
|
||||||
|
// The protocol sends the data twice, read here
|
||||||
|
uint32_t loop_read;
|
||||||
|
for (uint16_t loop = 1; loop <= 2; loop++) {
|
||||||
|
if (!data.expect_item(HEADER_MARK_US, HEADER_SPACE_US))
|
||||||
|
return false;
|
||||||
|
loop_read = 0;
|
||||||
|
for (uint8_t a_byte = 0; a_byte < 3; a_byte++) {
|
||||||
|
uint8_t byte = 0;
|
||||||
|
for (int8_t a_bit = 7; a_bit >= 0; a_bit--) {
|
||||||
|
if (data.expect_item(BIT_MARK_US, BIT_ONE_SPACE_US))
|
||||||
|
byte |= 1 << a_bit;
|
||||||
|
else if (!data.expect_item(BIT_MARK_US, BIT_ZERO_SPACE_US))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Need to see this segment inverted
|
||||||
|
for (int8_t a_bit = 7; a_bit >= 0; a_bit--) {
|
||||||
|
bool bit = byte & (1 << a_bit);
|
||||||
|
if (!data.expect_item(BIT_MARK_US, bit ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Receiving MSB first: reorder bytes
|
||||||
|
loop_read |= byte << ((2 - a_byte) * 8);
|
||||||
|
}
|
||||||
|
// Footer Mark
|
||||||
|
if (!data.expect_mark(BIT_MARK_US))
|
||||||
|
return false;
|
||||||
|
if (loop == 1) {
|
||||||
|
// Back up state on first loop
|
||||||
|
remote_state = loop_read;
|
||||||
|
if (!data.expect_space(FOOTER_SPACE_US))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "Decoded 0x%02X", remote_state);
|
||||||
|
if (remote_state != loop_read || (remote_state & 0xFF0000) != 0xB20000)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (remote_state == COOLIX_OFF) {
|
||||||
|
this->mode = climate::CLIMATE_MODE_OFF;
|
||||||
|
} else {
|
||||||
|
if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT)
|
||||||
|
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||||
|
else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO)
|
||||||
|
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||||
|
else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) {
|
||||||
|
// climate::CLIMATE_MODE_DRY;
|
||||||
|
if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_DRY)
|
||||||
|
ESP_LOGV(TAG, "Not supported DRY mode. Reporting AUTO");
|
||||||
|
else
|
||||||
|
ESP_LOGV(TAG, "Not supported FAN Auto mode. Reporting AUTO");
|
||||||
|
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||||
|
} else
|
||||||
|
this->mode = climate::CLIMATE_MODE_COOL;
|
||||||
|
|
||||||
|
// Fan Speed
|
||||||
|
// When climate::CLIMATE_MODE_DRY is implemented replace following line with this:
|
||||||
|
// if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_DRY)
|
||||||
|
if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO)
|
||||||
|
ESP_LOGV(TAG, "Not supported FAN speed AUTO");
|
||||||
|
else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN)
|
||||||
|
ESP_LOGV(TAG, "Not supported FAN speed MIN");
|
||||||
|
else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED)
|
||||||
|
ESP_LOGV(TAG, "Not supported FAN speed MED");
|
||||||
|
else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX)
|
||||||
|
ESP_LOGV(TAG, "Not supported FAN speed MAX");
|
||||||
|
|
||||||
|
// Temperature
|
||||||
|
uint8_t temperature_code = (remote_state & COOLIX_TEMP_MASK) >> 4;
|
||||||
|
for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++)
|
||||||
|
if (COOLIX_TEMP_MAP[i] == temperature_code)
|
||||||
|
this->target_temperature = i + COOLIX_TEMP_MIN;
|
||||||
|
}
|
||||||
|
this->publish_state();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace coolix
|
} // namespace coolix
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
@ -10,7 +10,9 @@
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace coolix {
|
namespace coolix {
|
||||||
|
|
||||||
class CoolixClimate : public climate::Climate, public Component {
|
using namespace remote_base;
|
||||||
|
|
||||||
|
class CoolixClimate : public climate::Climate, public Component, public RemoteReceiverListener {
|
||||||
public:
|
public:
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
|
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
|
||||||
|
@ -29,8 +31,10 @@ class CoolixClimate : public climate::Climate, public Component {
|
||||||
/// Transmit via IR the state of this climate controller.
|
/// Transmit via IR the state of this climate controller.
|
||||||
void transmit_state_();
|
void transmit_state_();
|
||||||
|
|
||||||
bool supports_cool_{true};
|
bool on_receive(RemoteReceiveData data) override;
|
||||||
bool supports_heat_{true};
|
|
||||||
|
bool supports_cool_;
|
||||||
|
bool supports_heat_;
|
||||||
|
|
||||||
remote_transmitter::RemoteTransmitterComponent *transmitter_;
|
remote_transmitter::RemoteTransmitterComponent *transmitter_;
|
||||||
sensor::Sensor *sensor_{nullptr};
|
sensor::Sensor *sensor_{nullptr};
|
||||||
|
|
Loading…
Reference in a new issue