diff --git a/esphome/components/whirlpool/__init__.py b/esphome/components/whirlpool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py new file mode 100644 index 0000000000..1083b86618 --- /dev/null +++ b/esphome/components/whirlpool/climate.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID, CONF_MODEL + +AUTO_LOAD = ['climate_ir'] + +whirlpool_ns = cg.esphome_ns.namespace('whirlpool') +WhirlpoolClimate = whirlpool_ns.class_('WhirlpoolClimate', climate_ir.ClimateIR) + +Model = whirlpool_ns.enum('Model') +MODELS = { + 'DG11J1-3A': Model.MODEL_DG11J1_3A, + 'DG11J1-91': Model.MODEL_DG11J1_91, +} + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(WhirlpoolClimate), + cv.Optional(CONF_MODEL, default='DG11J1-3A'): cv.enum(MODELS, upper=True) +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) + cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp new file mode 100644 index 0000000000..0956f816ce --- /dev/null +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -0,0 +1,289 @@ +#include "whirlpool.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace whirlpool { + +static const char *TAG = "whirlpool.climate"; + +const uint16_t WHIRLPOOL_HEADER_MARK = 9000; +const uint16_t WHIRLPOOL_HEADER_SPACE = 4494; +const uint16_t WHIRLPOOL_BIT_MARK = 572; +const uint16_t WHIRLPOOL_ONE_SPACE = 1659; +const uint16_t WHIRLPOOL_ZERO_SPACE = 553; +const uint32_t WHIRLPOOL_GAP = 7960; + +const uint32_t WHIRLPOOL_CARRIER_FREQUENCY = 38000; + +const uint8_t WHIRLPOOL_STATE_LENGTH = 21; + +const uint8_t WHIRLPOOL_HEAT = 0; +const uint8_t WHIRLPOOL_DRY = 3; +const uint8_t WHIRLPOOL_COOL = 2; +const uint8_t WHIRLPOOL_FAN = 4; +const uint8_t WHIRLPOOL_AUTO = 1; + +const uint8_t WHIRLPOOL_FAN_AUTO = 0; +const uint8_t WHIRLPOOL_FAN_HIGH = 1; +const uint8_t WHIRLPOOL_FAN_MED = 2; +const uint8_t WHIRLPOOL_FAN_LOW = 3; + +const uint8_t WHIRLPOOL_SWING_MASK = 128; + +const uint8_t WHIRLPOOL_POWER = 0x04; + +void WhirlpoolClimate::transmit_state() { + uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; + remote_state[0] = 0x83; + remote_state[1] = 0x06; + remote_state[6] = 0x80; + // MODEL DG11J191 + remote_state[18] = 0x08; + + auto powered_on = this->mode != climate::CLIMATE_MODE_OFF; + if (powered_on != this->powered_on_assumed_) { + // Set power toggle command + remote_state[2] = 4; + remote_state[15] = 1; + this->powered_on_assumed_ = powered_on; + } + switch (this->mode) { + case climate::CLIMATE_MODE_AUTO: + // set fan auto + // set temp auto temp + // set sleep false + remote_state[3] = WHIRLPOOL_AUTO; + remote_state[15] = 0x17; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[3] = WHIRLPOOL_HEAT; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_COOL: + remote_state[3] = WHIRLPOOL_COOL; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_DRY: + remote_state[3] = WHIRLPOOL_DRY; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[3] = WHIRLPOOL_FAN; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_OFF: + default: + break; + } + + // Temperature + auto temp = (uint8_t) roundf(clamp(this->target_temperature, this->temperature_min_(), this->temperature_max_())); + remote_state[3] |= (uint8_t)(temp - this->temperature_min_()) << 4; + + // Fan speed + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state[2] |= WHIRLPOOL_FAN_HIGH; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state[2] |= WHIRLPOOL_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state[2] |= WHIRLPOOL_FAN_LOW; + break; + default: + break; + } + + // Swing + ESP_LOGV(TAG, "send swing %s", this->send_swing_cmd_ ? "true" : "false"); + if (this->send_swing_cmd_) { + if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) { + remote_state[2] |= 128; + remote_state[8] |= 64; + } + } + + // Checksum + for (uint8_t i = 2; i < 12; i++) + remote_state[13] ^= remote_state[i]; + for (uint8_t i = 14; i < 20; i++) + remote_state[20] ^= remote_state[i]; + + ESP_LOGV(TAG, + "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %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], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17], + remote_state[18], remote_state[19], remote_state[20]); + + // Send code + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + + // Header + data->mark(WHIRLPOOL_HEADER_MARK); + data->space(WHIRLPOOL_HEADER_SPACE); + // Data + auto bytes_sent = 0; + for (uint8_t i : remote_state) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(WHIRLPOOL_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? WHIRLPOOL_ONE_SPACE : WHIRLPOOL_ZERO_SPACE); + } + bytes_sent++; + if (bytes_sent == 6 || bytes_sent == 14) { + // Divider + data->mark(WHIRLPOOL_BIT_MARK); + data->space(WHIRLPOOL_GAP); + } + } + // Footer + data->mark(WHIRLPOOL_BIT_MARK); + + transmit.perform(); +} + +bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(WHIRLPOOL_HEADER_MARK, WHIRLPOOL_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; + // Read all bytes. + for (int i = 0; i < WHIRLPOOL_STATE_LENGTH; i++) { + // Read bit + if (i == 6 || i == 14) { + if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP)) + return false; + } + for (int j = 0; j < 8; j++) { + if (data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_ONE_SPACE)) + remote_state[i] |= 1 << j; + + else if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_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(WHIRLPOOL_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + uint8_t checksum13 = 0; + uint8_t checksum20 = 0; + // Calculate checksum and compare with signal value. + for (uint8_t i = 2; i < 12; i++) + checksum13 ^= remote_state[i]; + for (uint8_t i = 14; i < 20; i++) + checksum20 ^= remote_state[i]; + + if (checksum13 != remote_state[13] || checksum20 != remote_state[20]) { + ESP_LOGVV(TAG, "Checksum fail"); + return false; + } + + ESP_LOGV( + TAG, + "Received: %02X %02X %02X %02X %02X %02X %02X %02X %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], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17], + remote_state[18], remote_state[19], remote_state[20]); + + // verify header remote code + if (remote_state[0] != 0x83 || remote_state[1] != 0x06) + return false; + + // powr on/off button + ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & WHIRLPOOL_POWER)); + + if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) { + auto powered_on = this->mode != climate::CLIMATE_MODE_OFF; + + if (powered_on) { + this->mode = climate::CLIMATE_MODE_OFF; + this->powered_on_assumed_ = false; + } else { + this->powered_on_assumed_ = true; + } + } + + // Set received mode + if (powered_on_assumed_) { + auto mode = remote_state[3] & 0x7; + ESP_LOGV(TAG, "Mode: %02X", mode); + switch (mode) { + case WHIRLPOOL_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case WHIRLPOOL_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case WHIRLPOOL_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case WHIRLPOOL_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case WHIRLPOOL_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + } + } + + // Set received temp + int temp = remote_state[3] & 0xF0; + ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); + temp = (uint8_t) temp >> 4; + temp += static_cast(this->temperature_min_()); + ESP_LOGVV(TAG, "Temperature Climate: %u", temp); + this->target_temperature = temp; + + // Set received fan speed + auto fan = remote_state[2] & 0x03; + ESP_LOGVV(TAG, "Fan: %02X", fan); + switch (fan) { + case WHIRLPOOL_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case WHIRLPOOL_FAN_MED: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case WHIRLPOOL_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case WHIRLPOOL_FAN_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + + // Set received swing status + if ((remote_state[2] & WHIRLPOOL_SWING_MASK) == WHIRLPOOL_SWING_MASK && remote_state[8] == 0x40) { + ESP_LOGVV(TAG, "Swing toggle pressed "); + if (this->swing_mode == climate::CLIMATE_SWING_OFF) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + } + + this->publish_state(); + return true; +} + +} // namespace whirlpool +} // namespace esphome diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h new file mode 100644 index 0000000000..44116b340c --- /dev/null +++ b/esphome/components/whirlpool/whirlpool.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace whirlpool { + +/// Simple enum to represent models. +enum Model { + MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32 + MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30 +}; + +// Temperature +const float WHIRLPOOL_DG11J1_3A_TEMP_MAX = 32.0; +const float WHIRLPOOL_DG11J1_3A_TEMP_MIN = 18.0; +const float WHIRLPOOL_DG11J1_91_TEMP_MAX = 30.0; +const float WHIRLPOOL_DG11J1_91_TEMP_MIN = 16.0; + +class WhirlpoolClimate : public climate_ir::ClimateIR { + public: + WhirlpoolClimate() + : climate_ir::ClimateIR(temperature_min_(), temperature_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}) {} + + void setup() override { + climate_ir::ClimateIR::setup(); + + this->powered_on_assumed_ = this->mode != climate::CLIMATE_MODE_OFF; + } + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + climate_ir::ClimateIR::control(call); + } + + void set_model(Model model) { this->model_ = model; } + + 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; + + // used to track when to send the power toggle command + bool powered_on_assumed_; + + bool send_swing_cmd_{false}; + Model model_; + + float temperature_min_() { + return (model_ == MODEL_DG11J1_3A) ? WHIRLPOOL_DG11J1_3A_TEMP_MIN : WHIRLPOOL_DG11J1_91_TEMP_MIN; + } + float temperature_max_() { + return (model_ == MODEL_DG11J1_3A) ? WHIRLPOOL_DG11J1_3A_TEMP_MAX : WHIRLPOOL_DG11J1_91_TEMP_MAX; + } +}; + +} // namespace whirlpool +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 8019148e07..dcc6700b45 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1228,6 +1228,8 @@ climate: name: Yashima Climate - platform: mitsubishi name: Mitsubishi + - platform: whirlpool + name: Whirlpool Climate switch: - platform: gpio