From 4118a289a69e12093fbe9282a8d196364b05451b Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 8 Sep 2019 22:14:39 -0300 Subject: [PATCH] Add coolix receiver (#645) * add coolix receiver a * lint - added comments * Lint * target temp neve be nan --- esphome/components/coolix/climate.py | 7 +- esphome/components/coolix/coolix.cpp | 160 ++++++++++++++++++++------- esphome/components/coolix/coolix.h | 10 +- 3 files changed, 135 insertions(+), 42 deletions(-) diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 750a97d087..ed88f6ad6a 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,6 +1,6 @@ import esphome.codegen as cg 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 AUTO_LOAD = ['sensor'] @@ -9,12 +9,14 @@ coolix_ns = cg.esphome_ns.namespace('coolix') CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component) CONF_TRANSMITTER_ID = 'transmitter_id' +CONF_RECEIVER_ID = 'receiver_id' CONF_SUPPORTS_HEAT = 'supports_heat' CONF_SUPPORTS_COOL = 'supports_cool' CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(CoolixClimate), 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_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), @@ -31,6 +33,9 @@ def to_code(config): if CONF_SENSOR in config: sens = yield cg.get_variable(config[CONF_SENSOR]) 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]) cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index ffc67adeb3..4da307a737 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -7,15 +7,24 @@ namespace coolix { static const char *TAG = "coolix.climate"; 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. const uint32_t COOLIX_DEFAULT_STATE = 0xB2BFC8; const uint32_t COOLIX_DEFAULT_STATE_AUTO_24_FAN = 0xB21F48; -const uint8_t COOLIX_COOL = 0b00; -const uint8_t COOLIX_DRY = 0b01; -const uint8_t COOLIX_AUTO = 0b10; -const uint8_t COOLIX_HEAT = 0b11; -const uint8_t COOLIX_FAN = 4; // Synthetic. -const uint32_t COOLIX_MODE_MASK = 0b000000000000000000001100; // 0xC +const uint8_t COOLIX_COOL = 0b0000; +const uint8_t COOLIX_DRY_FAN = 0b0100; +const uint8_t COOLIX_AUTO = 0b1000; +const uint8_t COOLIX_HEAT = 0b1100; +const uint32_t COOLIX_MODE_MASK = 0b1100; +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 const uint8_t COOLIX_TEMP_MIN = 17; // Celsius @@ -41,22 +50,13 @@ const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { }; // Constants -// Pulse parms are *50-100 for the Mark and *50+100 for the space -// First MARK is the one after the long gap -// pulse parameters in usec -const uint16_t COOLIX_TICK = 560; // Approximately 21 cycles at 38kHz -const uint16_t COOLIX_BIT_MARK_TICKS = 1; -const uint16_t COOLIX_BIT_MARK = COOLIX_BIT_MARK_TICKS * COOLIX_TICK; -const uint16_t COOLIX_ONE_SPACE_TICKS = 3; -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; +static const uint32_t BIT_MARK_US = 660; +static const uint32_t HEADER_MARK_US = 560 * 8; +static const uint32_t HEADER_SPACE_US = 560 * 8; +static const uint32_t BIT_ONE_SPACE_US = 1500; +static const uint32_t BIT_ZERO_SPACE_US = 450; +static const uint32_t FOOTER_MARK_US = BIT_MARK_US; +static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US; const uint16_t COOLIX_BITS = 24; @@ -90,10 +90,13 @@ void CoolixClimate::setup() { restore->apply(this); } else { // 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 - 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) { @@ -111,10 +114,10 @@ void CoolixClimate::transmit_state_() { switch (this->mode) { 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; 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; case climate::CLIMATE_MODE_AUTO: remote_state = COOLIX_DEFAULT_STATE_AUTO_24_FAN; @@ -127,10 +130,10 @@ void CoolixClimate::transmit_state_() { if (this->mode != climate::CLIMATE_MODE_OFF) { 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_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 data = transmit.get_data(); @@ -139,32 +142,113 @@ void CoolixClimate::transmit_state_() { uint16_t repeat = 1; for (uint16_t r = 0; r <= repeat; r++) { // Header - data->mark(COOLIX_HEADER_MARK); - data->space(COOLIX_HEADER_SPACE); + data->mark(HEADER_MARK_US); + data->space(HEADER_SPACE_US); // 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. for (uint16_t i = 8; i <= COOLIX_BITS; i += 8) { // 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 for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(COOLIX_BIT_MARK); - data->space((segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE); + data->mark(BIT_MARK_US); + data->space((byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); } // Inverted for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(COOLIX_BIT_MARK); - data->space(!(segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE); + data->mark(BIT_MARK_US); + data->space(!(byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); } } // Footer - data->mark(COOLIX_BIT_MARK); - data->space(COOLIX_MIN_GAP); // Pause before repeating + data->mark(BIT_MARK_US); + data->space(FOOTER_SPACE_US); // Pause before repeating } 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 esphome diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index 0d52018d2a..392728c654 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -10,7 +10,9 @@ namespace esphome { namespace coolix { -class CoolixClimate : public climate::Climate, public Component { +using namespace remote_base; + +class CoolixClimate : public climate::Climate, public Component, public RemoteReceiverListener { public: void setup() override; 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. void transmit_state_(); - bool supports_cool_{true}; - bool supports_heat_{true}; + bool on_receive(RemoteReceiveData data) override; + + bool supports_cool_; + bool supports_heat_; remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr};