From d7576f67e85701d2ec35786b1c4c25432b7e31d1 Mon Sep 17 00:00:00 2001 From: hagak <9702199+hagak@users.noreply.github.com> Date: Wed, 19 Oct 2022 03:29:22 -0400 Subject: [PATCH] Added component Daikin BRC to support ceiling cassette heatpumps (#3743) --- CODEOWNERS | 1 + esphome/components/daikin_brc/__init__.py | 1 + esphome/components/daikin_brc/climate.py | 24 ++ esphome/components/daikin_brc/daikin_brc.cpp | 273 +++++++++++++++++++ esphome/components/daikin_brc/daikin_brc.h | 82 ++++++ tests/test1.yaml | 3 + 6 files changed, 384 insertions(+) create mode 100644 esphome/components/daikin_brc/__init__.py create mode 100644 esphome/components/daikin_brc/climate.py create mode 100644 esphome/components/daikin_brc/daikin_brc.cpp create mode 100644 esphome/components/daikin_brc/daikin_brc.h diff --git a/CODEOWNERS b/CODEOWNERS index 688c58b2a4..78624ecdbe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -58,6 +58,7 @@ esphome/components/cse7761/* @berfenger esphome/components/ct_clamp/* @jesserockz esphome/components/current_based/* @djwmarcx esphome/components/dac7678/* @NickB1 +esphome/components/daikin_brc/* @hagak esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core esphome/components/debug/* @OttoWinter diff --git a/esphome/components/daikin_brc/__init__.py b/esphome/components/daikin_brc/__init__.py new file mode 100644 index 0000000000..69002c015f --- /dev/null +++ b/esphome/components/daikin_brc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@hagak"] diff --git a/esphome/components/daikin_brc/climate.py b/esphome/components/daikin_brc/climate.py new file mode 100644 index 0000000000..3468b6533c --- /dev/null +++ b/esphome/components/daikin_brc/climate.py @@ -0,0 +1,24 @@ +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"] + +daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc") +DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) + +CONF_USE_FAHRENHEIT = "use_fahrenheit" + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DaikinBrcClimate), + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) + cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/daikin_brc/daikin_brc.cpp b/esphome/components/daikin_brc/daikin_brc.cpp new file mode 100644 index 0000000000..6683d70f80 --- /dev/null +++ b/esphome/components/daikin_brc/daikin_brc.cpp @@ -0,0 +1,273 @@ +#include "daikin_brc.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace daikin_brc { + +static const char *const TAG = "daikin_brc.climate"; + +void DaikinBrcClimate::control(const climate::ClimateCall &call) { + this->mode_button_ = 0x00; + if (call.get_mode().has_value()) { + // Need to determine if this is call due to Mode button pressed so that we can set the Mode button byte + this->mode_button_ = DAIKIN_BRC_IR_MODE_BUTTON; + } + ClimateIR::control(call); +} + +void DaikinBrcClimate::transmit_state() { + uint8_t remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE] = {0x11, 0xDA, 0x17, 0x18, 0x04, 0x00, 0x1E, 0x11, + 0xDA, 0x17, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x00}; + + remote_state[12] = this->alt_mode_(); + remote_state[13] = this->mode_button_; + remote_state[14] = this->operation_mode_(); + remote_state[17] = this->temperature_(); + remote_state[18] = this->fan_speed_swing_(); + + // Calculate checksum + for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1; i++) { + remote_state[DAIKIN_BRC_TRANSMIT_FRAME_SIZE - 1] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(DAIKIN_BRC_IR_FREQUENCY); + + data->mark(DAIKIN_BRC_HEADER_MARK); + data->space(DAIKIN_BRC_HEADER_SPACE); + for (int i = 0; i < DAIKIN_BRC_PREAMBLE_SIZE; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BRC_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE); + } + } + data->mark(DAIKIN_BRC_BIT_MARK); + data->space(DAIKIN_BRC_MESSAGE_SPACE); + data->mark(DAIKIN_BRC_HEADER_MARK); + data->space(DAIKIN_BRC_HEADER_SPACE); + + for (int i = DAIKIN_BRC_PREAMBLE_SIZE; i < DAIKIN_BRC_TRANSMIT_FRAME_SIZE; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BRC_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_BRC_ONE_SPACE : DAIKIN_BRC_ZERO_SPACE); + } + } + + data->mark(DAIKIN_BRC_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DaikinBrcClimate::alt_mode_() { + uint8_t alt_mode = 0x00; + switch (this->mode) { + case climate::CLIMATE_MODE_DRY: + alt_mode = 0x23; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + alt_mode = 0x63; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_HEAT: + default: + alt_mode = 0x73; + break; + } + return alt_mode; +} + +uint8_t DaikinBrcClimate::operation_mode_() { + uint8_t operating_mode = DAIKIN_BRC_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DAIKIN_BRC_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DAIKIN_BRC_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DAIKIN_BRC_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= DAIKIN_BRC_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DAIKIN_BRC_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DAIKIN_BRC_MODE_OFF; + break; + } + + return operating_mode; +} + +uint8_t DaikinBrcClimate::fan_speed_swing_() { + uint16_t fan_speed; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DAIKIN_BRC_FAN_1; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DAIKIN_BRC_FAN_2; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DAIKIN_BRC_FAN_3; + break; + default: + fan_speed = DAIKIN_BRC_FAN_1; + } + + // If swing is enabled switch first 4 bits to 1111 + switch (this->swing_mode) { + case climate::CLIMATE_SWING_BOTH: + fan_speed |= DAIKIN_BRC_IR_SWING_ON; + break; + default: + fan_speed |= DAIKIN_BRC_IR_SWING_OFF; + break; + } + return fan_speed; +} + +uint8_t DaikinBrcClimate::temperature_() { + // Force special temperatures depending on the mode + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_DRY: + if (this->fahrenheit_) { + return DAIKIN_BRC_IR_DRY_FAN_TEMP_F; + } + return DAIKIN_BRC_IR_DRY_FAN_TEMP_C; + case climate::CLIMATE_MODE_HEAT_COOL: + default: + uint8_t temperature; + // Temperature in remote is in F + if (this->fahrenheit_) { + temperature = (uint8_t) roundf( + clamp(((this->target_temperature * 1.8) + 32), DAIKIN_BRC_TEMP_MIN_F, DAIKIN_BRC_TEMP_MAX_F)); + } else { + temperature = ((uint8_t) roundf(this->target_temperature) - 9) << 1; + } + return temperature; + } +} + +bool DaikinBrcClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DAIKIN_BRC_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DAIKIN_BRC_STATE_FRAME_SIZE - 1] != checksum) { + ESP_LOGCONFIG(TAG, "Bad CheckSum %x", checksum); + return false; + } + + uint8_t mode = frame[7]; + if (mode & DAIKIN_BRC_MODE_ON) { + switch (mode & 0xF0) { + case DAIKIN_BRC_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DAIKIN_BRC_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DAIKIN_BRC_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DAIKIN_BRC_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case DAIKIN_BRC_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + + uint8_t temperature = frame[10]; + float temperature_c; + if (this->fahrenheit_) { + temperature_c = clamp(((temperature - 32) / 1.8), DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C); + } else { + temperature_c = (temperature >> 1) + 9; + } + + this->target_temperature = temperature_c; + + uint8_t fan_mode = frame[11]; + uint8_t swing_mode = frame[11]; + switch (swing_mode & 0xF) { + case DAIKIN_BRC_IR_SWING_ON: + this->swing_mode = climate::CLIMATE_SWING_BOTH; + break; + case DAIKIN_BRC_IR_SWING_OFF: + this->swing_mode = climate::CLIMATE_SWING_OFF; + break; + } + + switch (fan_mode & 0xF0) { + case DAIKIN_BRC_FAN_1: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DAIKIN_BRC_FAN_2: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DAIKIN_BRC_FAN_3: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + } + this->publish_state(); + return true; +} + +bool DaikinBrcClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DAIKIN_BRC_STATE_FRAME_SIZE] = {}; + if (!data.expect_item(DAIKIN_BRC_HEADER_MARK, DAIKIN_BRC_HEADER_SPACE)) { + return false; + } + for (uint8_t pos = 0; pos < DAIKIN_BRC_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(DAIKIN_BRC_BIT_MARK, DAIKIN_BRC_ZERO_SPACE)) { + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != 0x11) + return false; + } else if (pos == 1) { + // frame header + if (byte != 0xDA) + return false; + } else if (pos == 2) { + // frame header + if (byte != 0x17) + return false; + } else if (pos == 3) { + // frame header + if (byte != 0x18) + return false; + } else if (pos == 4) { + // frame type + if (byte != 0x00) + return false; + } + } + return this->parse_state_frame_(state_frame); +} + +} // namespace daikin_brc +} // namespace esphome diff --git a/esphome/components/daikin_brc/daikin_brc.h b/esphome/components/daikin_brc/daikin_brc.h new file mode 100644 index 0000000000..bdc6384809 --- /dev/null +++ b/esphome/components/daikin_brc/daikin_brc.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace daikin_brc { + +// Values for Daikin BRC4CXXX IR Controllers +// Temperature +const uint8_t DAIKIN_BRC_TEMP_MIN_F = 60; // fahrenheit +const uint8_t DAIKIN_BRC_TEMP_MAX_F = 90; // fahrenheit +const float DAIKIN_BRC_TEMP_MIN_C = (DAIKIN_BRC_TEMP_MIN_F - 32) / 1.8; // fahrenheit +const float DAIKIN_BRC_TEMP_MAX_C = (DAIKIN_BRC_TEMP_MAX_F - 32) / 1.8; // fahrenheit + +// Modes +const uint8_t DAIKIN_BRC_MODE_AUTO = 0x30; +const uint8_t DAIKIN_BRC_MODE_COOL = 0x20; +const uint8_t DAIKIN_BRC_MODE_HEAT = 0x10; +const uint8_t DAIKIN_BRC_MODE_DRY = 0x70; +const uint8_t DAIKIN_BRC_MODE_FAN = 0x00; +const uint8_t DAIKIN_BRC_MODE_OFF = 0x00; +const uint8_t DAIKIN_BRC_MODE_ON = 0x01; + +// Fan Speed +const uint8_t DAIKIN_BRC_FAN_1 = 0x10; +const uint8_t DAIKIN_BRC_FAN_2 = 0x30; +const uint8_t DAIKIN_BRC_FAN_3 = 0x50; +const uint8_t DAIKIN_BRC_FAN_AUTO = 0xA0; + +// IR Transmission +const uint32_t DAIKIN_BRC_IR_FREQUENCY = 38000; +const uint32_t DAIKIN_BRC_HEADER_MARK = 5070; +const uint32_t DAIKIN_BRC_HEADER_SPACE = 2140; +const uint32_t DAIKIN_BRC_BIT_MARK = 370; +const uint32_t DAIKIN_BRC_ONE_SPACE = 1780; +const uint32_t DAIKIN_BRC_ZERO_SPACE = 710; +const uint32_t DAIKIN_BRC_MESSAGE_SPACE = 29410; + +const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_F = 72; // Dry/Fan mode is always 17 Celsius. +const uint8_t DAIKIN_BRC_IR_DRY_FAN_TEMP_C = (17 - 9) * 2; // Dry/Fan mode is always 17 Celsius. +const uint8_t DAIKIN_BRC_IR_SWING_ON = 0x5; +const uint8_t DAIKIN_BRC_IR_SWING_OFF = 0x6; +const uint8_t DAIKIN_BRC_IR_MODE_BUTTON = 0x4; // This is set after a mode action + +// State Frame size +const uint8_t DAIKIN_BRC_STATE_FRAME_SIZE = 15; +// Preamble size +const uint8_t DAIKIN_BRC_PREAMBLE_SIZE = 7; +// Transmit Frame size - includes a preamble +const uint8_t DAIKIN_BRC_TRANSMIT_FRAME_SIZE = DAIKIN_BRC_PREAMBLE_SIZE + DAIKIN_BRC_STATE_FRAME_SIZE; + +class DaikinBrcClimate : public climate_ir::ClimateIR { + public: + DaikinBrcClimate() + : climate_ir::ClimateIR(DAIKIN_BRC_TEMP_MIN_C, DAIKIN_BRC_TEMP_MAX_C, 0.5f, true, true, + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH}) {} + + /// Set use of Fahrenheit units + void set_fahrenheit(bool value) { + this->fahrenheit_ = value; + this->temperature_step_ = value ? 0.5f : 1.0f; + } + + protected: + uint8_t mode_button_ = 0x00; + // Capture if the MODE was changed + void control(const climate::ClimateCall &call) override; + // Transmit via IR the state of this climate controller. + void transmit_state() override; + uint8_t alt_mode_(); + uint8_t operation_mode_(); + uint8_t fan_speed_swing_(); + uint8_t temperature_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); + bool fahrenheit_{false}; +}; + +} // namespace daikin_brc +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 82cdac4623..b286c37deb 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1862,6 +1862,9 @@ climate: name: Fujitsu General Climate - platform: daikin name: Daikin Climate + - platform: daikin_brc + name: Daikin BRC Climate + use_fahrenheit: true - platform: delonghi name: Delonghi Climate - platform: yashima