From 4e1f6518e829bcd80422cdf76b9d359603f23ae9 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 10 May 2022 19:22:22 +1000 Subject: [PATCH] Delonghi Penguino PAC W120HP ir support (#3124) --- CODEOWNERS | 1 + esphome/components/delonghi/__init__.py | 1 + esphome/components/delonghi/climate.py | 20 +++ esphome/components/delonghi/delonghi.cpp | 186 +++++++++++++++++++++++ esphome/components/delonghi/delonghi.h | 64 ++++++++ tests/test1.yaml | 2 + 6 files changed, 274 insertions(+) create mode 100644 esphome/components/delonghi/__init__.py create mode 100644 esphome/components/delonghi/climate.py create mode 100644 esphome/components/delonghi/delonghi.cpp create mode 100644 esphome/components/delonghi/delonghi.h diff --git a/CODEOWNERS b/CODEOWNERS index 3a511275e1..16b9008379 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -55,6 +55,7 @@ esphome/components/current_based/* @djwmarcx esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core esphome/components/debug/* @OttoWinter +esphome/components/delonghi/* @grob6000 esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee diff --git a/esphome/components/delonghi/__init__.py b/esphome/components/delonghi/__init__.py new file mode 100644 index 0000000000..0a81eb2da7 --- /dev/null +++ b/esphome/components/delonghi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@grob6000"] diff --git a/esphome/components/delonghi/climate.py b/esphome/components/delonghi/climate.py new file mode 100644 index 0000000000..614706defe --- /dev/null +++ b/esphome/components/delonghi/climate.py @@ -0,0 +1,20 @@ +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"] + +delonghi_ns = cg.esphome_ns.namespace("delonghi") +DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DelonghiClimate), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/delonghi/delonghi.cpp b/esphome/components/delonghi/delonghi.cpp new file mode 100644 index 0000000000..9bc0b5753d --- /dev/null +++ b/esphome/components/delonghi/delonghi.cpp @@ -0,0 +1,186 @@ +#include "delonghi.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace delonghi { + +static const char *const TAG = "delonghi.climate"; + +void DelonghiClimate::transmit_state() { + uint8_t remote_state[DELONGHI_STATE_FRAME_SIZE] = {0}; + remote_state[0] = DELONGHI_ADDRESS; + remote_state[1] = this->temperature_(); + remote_state[1] |= (this->fan_speed_()) << 5; + remote_state[2] = this->operation_mode_(); + // Calculate checksum + for (int i = 0; i < DELONGHI_STATE_FRAME_SIZE - 1; i++) { + remote_state[DELONGHI_STATE_FRAME_SIZE - 1] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(DELONGHI_IR_FREQUENCY); + + data->mark(DELONGHI_HEADER_MARK); + data->space(DELONGHI_HEADER_SPACE); + for (unsigned char b : remote_state) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DELONGHI_BIT_MARK); + bool bit = b & mask; + data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE); + } + } + data->mark(DELONGHI_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DelonghiClimate::operation_mode_() { + uint8_t operating_mode = DELONGHI_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DELONGHI_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DELONGHI_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DELONGHI_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= DELONGHI_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DELONGHI_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DELONGHI_MODE_OFF; + break; + } + return operating_mode; +} + +uint16_t DelonghiClimate::fan_speed_() { + uint16_t fan_speed; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DELONGHI_FAN_LOW; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DELONGHI_FAN_MEDIUM; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DELONGHI_FAN_HIGH; + break; + case climate::CLIMATE_FAN_AUTO: + default: + fan_speed = DELONGHI_FAN_AUTO; + } + return fan_speed; +} + +uint8_t DelonghiClimate::temperature_() { + // Force special temperatures depending on the mode + uint8_t temperature = 0b0001; + switch (this->mode) { + case climate::CLIMATE_MODE_HEAT: + temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_DRY: + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_OFF: + default: + temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_COOL; + } + if (temperature > 0x0F) { + temperature = 0x0F; // clamp maximum + } + return temperature; +} + +bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) { + return false; + } + uint8_t mode = frame[2] & 0x0F; + if (mode & DELONGHI_MODE_ON) { + switch (mode & 0x0E) { + case DELONGHI_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DELONGHI_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DELONGHI_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DELONGHI_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case DELONGHI_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + uint8_t temperature = frame[1] & 0x0F; + if (this->mode == climate::CLIMATE_MODE_HEAT) { + this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_HEAT; + } else { + this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_COOL; + } + uint8_t fan_mode = frame[1] >> 5; + switch (fan_mode) { + case DELONGHI_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DELONGHI_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DELONGHI_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case DELONGHI_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + this->publish_state(); + return true; +} + +bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {}; + if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) { + return false; + } + for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) { + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != DELONGHI_ADDRESS) { + return false; + } + } + } + return this->parse_state_frame_(state_frame); +} + +} // namespace delonghi +} // namespace esphome diff --git a/esphome/components/delonghi/delonghi.h b/esphome/components/delonghi/delonghi.h new file mode 100644 index 0000000000..d310a58aee --- /dev/null +++ b/esphome/components/delonghi/delonghi.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace delonghi { + +// Values for DELONGHI ARC43XXX IR Controllers +const uint8_t DELONGHI_ADDRESS = 83; + +// Temperature +const uint8_t DELONGHI_TEMP_MIN = 13; // Celsius +const uint8_t DELONGHI_TEMP_MAX = 32; // Celsius +const uint8_t DELONGHI_TEMP_OFFSET_COOL = 17; // Celsius +const uint8_t DELONGHI_TEMP_OFFSET_HEAT = 12; // Celsius + +// Modes +const uint8_t DELONGHI_MODE_AUTO = 0b1000; +const uint8_t DELONGHI_MODE_COOL = 0b0000; +const uint8_t DELONGHI_MODE_HEAT = 0b0110; +const uint8_t DELONGHI_MODE_DRY = 0b0010; +const uint8_t DELONGHI_MODE_FAN = 0b0100; +const uint8_t DELONGHI_MODE_OFF = 0b0000; +const uint8_t DELONGHI_MODE_ON = 0b0001; + +// Fan Speed +const uint8_t DELONGHI_FAN_AUTO = 0b00; +const uint8_t DELONGHI_FAN_HIGH = 0b01; +const uint8_t DELONGHI_FAN_MEDIUM = 0b10; +const uint8_t DELONGHI_FAN_LOW = 0b11; + +// IR Transmission - similar to NEC1 +const uint32_t DELONGHI_IR_FREQUENCY = 38000; +const uint32_t DELONGHI_HEADER_MARK = 9000; +const uint32_t DELONGHI_HEADER_SPACE = 4500; +const uint32_t DELONGHI_BIT_MARK = 465; +const uint32_t DELONGHI_ONE_SPACE = 1750; +const uint32_t DELONGHI_ZERO_SPACE = 670; + +// State Frame size +const uint8_t DELONGHI_STATE_FRAME_SIZE = 8; + +class DelonghiClimate : public climate_ir::ClimateIR { + public: + DelonghiClimate() + : climate_ir::ClimateIR(DELONGHI_TEMP_MIN, DELONGHI_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; + uint8_t operation_mode_(); + uint16_t fan_speed_(); + uint8_t temperature_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); +}; + +} // namespace delonghi +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index aba37976aa..deaf1c237e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1811,6 +1811,8 @@ climate: name: Fujitsu General Climate - platform: daikin name: Daikin Climate + - platform: delonghi + name: Delonghi Climate - platform: yashima name: Yashima Climate - platform: mitsubishi