From 9a70bfa47187011ce9eccf7d2939bd067f53d8d6 Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Mon, 10 Jan 2022 02:47:19 +0400 Subject: [PATCH] New Midea IR component, improvements and fixes (#2847) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/climate_ir/climate_ir.cpp | 16 +- esphome/components/climate_ir/climate_ir.h | 4 +- esphome/components/coolix/coolix.cpp | 159 ++++---------- esphome/components/coolix/coolix.h | 6 +- esphome/components/midea/ir_transmitter.h | 8 +- esphome/components/midea_ir/__init__.py | 0 esphome/components/midea_ir/climate.py | 25 +++ esphome/components/midea_ir/midea_data.h | 92 ++++++++ esphome/components/midea_ir/midea_ir.cpp | 201 ++++++++++++++++++ esphome/components/midea_ir/midea_ir.h | 47 ++++ esphome/components/remote_base/__init__.py | 39 ++++ .../remote_base/coolix_protocol.cpp | 84 ++++++++ .../components/remote_base/coolix_protocol.h | 30 +++ .../components/remote_base/midea_protocol.cpp | 104 ++++----- .../components/remote_base/midea_protocol.h | 62 +++--- esphome/core/helpers.cpp | 4 +- esphome/core/helpers.h | 9 +- tests/test1.yaml | 3 + 19 files changed, 664 insertions(+), 230 deletions(-) create mode 100644 esphome/components/midea_ir/__init__.py create mode 100644 esphome/components/midea_ir/climate.py create mode 100644 esphome/components/midea_ir/midea_data.h create mode 100644 esphome/components/midea_ir/midea_ir.cpp create mode 100644 esphome/components/midea_ir/midea_ir.h create mode 100644 esphome/components/remote_base/coolix_protocol.cpp create mode 100644 esphome/components/remote_base/coolix_protocol.h diff --git a/CODEOWNERS b/CODEOWNERS index deecd094dc..5f9a579827 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -102,6 +102,7 @@ esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/midea/* @dudanov +esphome/components/midea_ir/* @dudanov esphome/components/mitsubishi/* @RubyBailey esphome/components/modbus_controller/* @martgras esphome/components/modbus_controller/binary_sensor/* @martgras diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index b47d9b0141..76adfb42bb 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -10,21 +10,22 @@ climate::ClimateTraits ClimateIR::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); - if (supports_cool_) + if (this->supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); - if (supports_heat_) + if (this->supports_heat_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); - if (supports_dry_) + if (this->supports_dry_) traits.add_supported_mode(climate::CLIMATE_MODE_DRY); - if (supports_fan_only_) + if (this->supports_fan_only_) traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); traits.set_supports_two_point_target_temperature(false); traits.set_visual_min_temperature(this->minimum_temperature_); traits.set_visual_max_temperature(this->maximum_temperature_); traits.set_visual_temperature_step(this->temperature_step_); - traits.set_supported_fan_modes(fan_modes_); - traits.set_supported_swing_modes(swing_modes_); + traits.set_supported_fan_modes(this->fan_modes_); + traits.set_supported_swing_modes(this->swing_modes_); + traits.set_supported_presets(this->presets_); return traits; } @@ -50,6 +51,7 @@ void ClimateIR::setup() { roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); this->fan_mode = climate::CLIMATE_FAN_AUTO; this->swing_mode = climate::CLIMATE_SWING_OFF; + this->preset = climate::CLIMATE_PRESET_NONE; } // Never send nan to HA if (std::isnan(this->target_temperature)) @@ -65,6 +67,8 @@ void ClimateIR::control(const climate::ClimateCall &call) { this->fan_mode = *call.get_fan_mode(); if (call.get_swing_mode().has_value()) this->swing_mode = *call.get_swing_mode(); + if (call.get_preset().has_value()) + this->preset = *call.get_preset(); this->transmit_state(); this->publish_state(); } diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 677021da29..5be4fc06f5 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -22,7 +22,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, - std::set swing_modes = {}) { + std::set swing_modes = {}, std::set presets = {}) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; @@ -30,6 +30,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: this->supports_fan_only_ = supports_fan_only; this->fan_modes_ = std::move(fan_modes); this->swing_modes_ = std::move(swing_modes); + this->presets_ = std::move(presets); } void setup() override; @@ -61,6 +62,7 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: bool supports_fan_only_{false}; std::set fan_modes_ = {}; std::set swing_modes_ = {}; + std::set presets_ = {}; remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index c9145e4ecf..76ec1627c2 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -1,4 +1,5 @@ #include "coolix.h" +#include "esphome/components/remote_base/coolix_protocol.h" #include "esphome/core/log.h" namespace esphome { @@ -6,29 +7,29 @@ namespace coolix { static const char *const 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; +static const uint32_t COOLIX_OFF = 0xB27BE0; +static const uint32_t COOLIX_SWING = 0xB26BE0; +static const uint32_t COOLIX_LED = 0xB5F5A5; +static const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; // On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. -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_MODE_AUTO_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; +static const uint8_t COOLIX_COOL = 0b0000; +static const uint8_t COOLIX_DRY_FAN = 0b0100; +static const uint8_t COOLIX_AUTO = 0b1000; +static const uint8_t COOLIX_HEAT = 0b1100; +static const uint32_t COOLIX_MODE_MASK = 0b1100; +static const uint32_t COOLIX_FAN_MASK = 0xF000; +static const uint32_t COOLIX_FAN_MODE_AUTO_DRY = 0x1000; +static const uint32_t COOLIX_FAN_AUTO = 0xB000; +static const uint32_t COOLIX_FAN_MIN = 0x9000; +static const uint32_t COOLIX_FAN_MED = 0x5000; +static const uint32_t COOLIX_FAN_MAX = 0x3000; // Temperature -const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; -const uint8_t COOLIX_FAN_TEMP_CODE = 0b11100000; // Part of Fan Mode. -const uint32_t COOLIX_TEMP_MASK = 0b11110000; -const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { +static const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; +static const uint8_t COOLIX_FAN_TEMP_CODE = 0b11100000; // Part of Fan Mode. +static const uint32_t COOLIX_TEMP_MASK = 0b11110000; +static const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { 0b00000000, // 17C 0b00010000, // 18c 0b00110000, // 19C @@ -45,17 +46,6 @@ const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { 0b10110000 // 30C }; -// Constants -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; - void CoolixClimate::transmit_state() { uint32_t remote_state = 0xB20F00; @@ -111,119 +101,60 @@ void CoolixClimate::transmit_state() { } } } - ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state); + ESP_LOGV(TAG, "Sending coolix code: 0x%06X", remote_state); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); - - data->set_carrier_frequency(38000); - uint16_t repeat = 1; - for (uint16_t r = 0; r <= repeat; r++) { - // Header - data->mark(HEADER_MARK_US); - data->space(HEADER_SPACE_US); - // Data - // 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 byte = (remote_state >> (COOLIX_BITS - i)) & 0xFF; - // Normal - for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - 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(BIT_MARK_US); - data->space(!(byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); - } - } - // Footer - data->mark(BIT_MARK_US); - data->space(FOOTER_SPACE_US); // Pause before repeating - } - + remote_base::CoolixProtocol().encode(data, remote_state); transmit.perform(); } -bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { +bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data) { + auto decoded = remote_base::CoolixProtocol().decode(data); + if (!decoded.has_value()) + return false; // 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) + uint32_t remote_state = *decoded; + ESP_LOGV(TAG, "Decoded 0x%06X", remote_state); + if ((remote_state & 0xFF0000) != 0xB20000) return false; if (remote_state == COOLIX_OFF) { - this->mode = climate::CLIMATE_MODE_OFF; + parent->mode = climate::CLIMATE_MODE_OFF; } else if (remote_state == COOLIX_SWING) { - this->swing_mode = - this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; + parent->swing_mode = + parent->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; } else { if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) - this->mode = climate::CLIMATE_MODE_HEAT; + parent->mode = climate::CLIMATE_MODE_HEAT; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) - this->mode = climate::CLIMATE_MODE_HEAT_COOL; + parent->mode = climate::CLIMATE_MODE_HEAT_COOL; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) { if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_MODE_AUTO_DRY) - this->mode = climate::CLIMATE_MODE_DRY; + parent->mode = climate::CLIMATE_MODE_DRY; else - this->mode = climate::CLIMATE_MODE_FAN_ONLY; + parent->mode = climate::CLIMATE_MODE_FAN_ONLY; } else - this->mode = climate::CLIMATE_MODE_COOL; + parent->mode = climate::CLIMATE_MODE_COOL; // Fan Speed - if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_HEAT_COOL || - this->mode == climate::CLIMATE_MODE_DRY) - this->fan_mode = climate::CLIMATE_FAN_AUTO; + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL || + parent->mode == climate::CLIMATE_MODE_DRY) + parent->fan_mode = climate::CLIMATE_FAN_AUTO; else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) - this->fan_mode = climate::CLIMATE_FAN_LOW; + parent->fan_mode = climate::CLIMATE_FAN_LOW; else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED) - this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + parent->fan_mode = climate::CLIMATE_FAN_MEDIUM; else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX) - this->fan_mode = climate::CLIMATE_FAN_HIGH; + parent->fan_mode = climate::CLIMATE_FAN_HIGH; // Temperature uint8_t temperature_code = remote_state & COOLIX_TEMP_MASK; for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++) if (COOLIX_TEMP_MAP[i] == temperature_code) - this->target_temperature = i + COOLIX_TEMP_MIN; + parent->target_temperature = i + COOLIX_TEMP_MIN; } - this->publish_state(); + parent->publish_state(); return true; } diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index caf93f7621..3419795875 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -26,11 +26,15 @@ class CoolixClimate : public climate_ir::ClimateIR { climate_ir::ClimateIR::control(call); } + /// This static method can be used in other climate components that accept the Coolix protocol. See midea_ir for + /// example. + static bool on_coolix(climate::Climate *parent, remote_base::RemoteReceiveData data); + 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; + bool on_receive(remote_base::RemoteReceiveData data) override { return CoolixClimate::on_coolix(this, data); } bool send_swing_cmd_{false}; }; diff --git a/esphome/components/midea/ir_transmitter.h b/esphome/components/midea/ir_transmitter.h index 34a9f8498e..a8b89f9b7b 100644 --- a/esphome/components/midea/ir_transmitter.h +++ b/esphome/components/midea/ir_transmitter.h @@ -23,12 +23,12 @@ class IrFollowMeData : public IrData { } /* TEMPERATURE */ - uint8_t temp() const { return this->data_[4] - 1; } - void set_temp(uint8_t val) { this->data_[4] = std::min(MAX_TEMP, val) + 1; } + uint8_t temp() const { return this->get_value_(4) - 1; } + void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); } /* BEEPER */ - bool beeper() const { return this->data_[3] & 128; } - void set_beeper(bool val) { this->set_value_(3, 1, 7, val); } + bool beeper() const { return this->get_value_(3, 128); } + void set_beeper(bool val) { this->set_mask_(3, val, 128); } protected: static const uint8_t MAX_TEMP = 37; diff --git a/esphome/components/midea_ir/__init__.py b/esphome/components/midea_ir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py new file mode 100644 index 0000000000..140e4ee4e0 --- /dev/null +++ b/esphome/components/midea_ir/climate.py @@ -0,0 +1,25 @@ +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", "coolix"] +CODEOWNERS = ["@dudanov"] + +midea_ir_ns = cg.esphome_ns.namespace("midea_ir") +MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR) + +CONF_USE_FAHRENHEIT = "use_fahrenheit" + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(MideaIR), + 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/midea_ir/midea_data.h b/esphome/components/midea_ir/midea_data.h new file mode 100644 index 0000000000..0f7e24907d --- /dev/null +++ b/esphome/components/midea_ir/midea_data.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/components/remote_base/midea_protocol.h" +#include "esphome/components/climate/climate_mode.h" + +namespace esphome { +namespace midea_ir { + +using climate::ClimateMode; +using climate::ClimateFanMode; +using remote_base::MideaData; + +class ControlData : public MideaData { + public: + // Default constructor (power: ON, mode: AUTO, fan: AUTO, temp: 25C) + ControlData() : MideaData({MIDEA_TYPE_CONTROL, 0x82, 0x48, 0xFF, 0xFF}) {} + // Copy from Base + ControlData(const MideaData &data) : MideaData(data) {} + + void set_temp(float temp); + float get_temp() const; + + void set_mode(ClimateMode mode); + ClimateMode get_mode() const; + + void set_fan_mode(ClimateFanMode mode); + ClimateFanMode get_fan_mode() const; + + void set_sleep_preset(bool value) { this->set_mask_(1, value, 64); } + bool get_sleep_preset() const { return this->get_value_(1, 64); } + + void set_fahrenheit(bool value) { this->set_mask_(2, value, 32); } + bool get_fahrenheit() const { return this->get_value_(2, 32); } + + void fix(); + + protected: + enum Mode : uint8_t { + MODE_COOL, + MODE_DRY, + MODE_AUTO, + MODE_HEAT, + MODE_FAN_ONLY, + }; + enum FanMode : uint8_t { + FAN_AUTO, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + }; + void set_fan_mode_(FanMode mode) { this->set_value_(1, mode, 3, 3); } + FanMode get_fan_mode_() const { return static_cast(this->get_value_(1, 3, 3)); } + void set_mode_(Mode mode) { this->set_value_(1, mode, 7); } + Mode get_mode_() const { return static_cast(this->get_value_(1, 7)); } + void set_power_(bool value) { this->set_mask_(1, value, 128); } + bool get_power_() const { return this->get_value_(1, 128); } +}; + +class FollowMeData : public MideaData { + public: + // Default constructor (temp: 30C, beeper: off) + FollowMeData() : MideaData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} + // Copy from Base + FollowMeData(const MideaData &data) : MideaData(data) {} + // Direct from temperature and beeper values + FollowMeData(uint8_t temp, bool beeper = false) : FollowMeData() { + this->set_temp(temp); + this->set_beeper(beeper); + } + + /* TEMPERATURE */ + uint8_t temp() const { return this->get_value_(4) - 1; } + void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); } + + /* BEEPER */ + bool beeper() const { return this->get_value_(3, 128); } + void set_beeper(bool value) { this->set_mask_(3, value, 128); } + + protected: + static const uint8_t MAX_TEMP = 37; +}; + +class SpecialData : public MideaData { + public: + SpecialData(uint8_t code) : MideaData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {} + static const uint8_t VSWING_STEP = 1; + static const uint8_t VSWING_TOGGLE = 2; + static const uint8_t TURBO_TOGGLE = 9; +}; + +} // namespace midea_ir +} // namespace esphome diff --git a/esphome/components/midea_ir/midea_ir.cpp b/esphome/components/midea_ir/midea_ir.cpp new file mode 100644 index 0000000000..5e507cbbb0 --- /dev/null +++ b/esphome/components/midea_ir/midea_ir.cpp @@ -0,0 +1,201 @@ +#include "midea_ir.h" +#include "midea_data.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/components/coolix/coolix.h" + +namespace esphome { +namespace midea_ir { + +static const char *const TAG = "midea_ir.climate"; + +void ControlData::set_temp(float temp) { + uint8_t min; + if (this->get_fahrenheit()) { + min = MIDEA_TEMPF_MIN; + temp = esphome::clamp(celsius_to_fahrenheit(temp), MIDEA_TEMPF_MIN, MIDEA_TEMPF_MAX); + } else { + min = MIDEA_TEMPC_MIN; + temp = esphome::clamp(temp, MIDEA_TEMPC_MIN, MIDEA_TEMPC_MAX); + } + this->set_value_(2, lroundf(temp) - min, 31); +} + +float ControlData::get_temp() const { + const uint8_t temp = this->get_value_(2, 31); + if (this->get_fahrenheit()) + return fahrenheit_to_celsius(static_cast(temp + MIDEA_TEMPF_MIN)); + return static_cast(temp + MIDEA_TEMPC_MIN); +} + +void ControlData::fix() { + // In FAN_AUTO, modes COOL, HEAT and FAN_ONLY bit #5 in byte #1 must be set + const uint8_t value = this->get_value_(1, 31); + if (value == 0 || value == 3 || value == 4) + this->set_mask_(1, true, 32); + // In FAN_ONLY mode we need to set all temperature bits + if (this->get_mode_() == MODE_FAN_ONLY) + this->set_mask_(2, true, 31); +} + +void ControlData::set_mode(ClimateMode mode) { + switch (mode) { + case ClimateMode::CLIMATE_MODE_OFF: + this->set_power_(false); + return; + case ClimateMode::CLIMATE_MODE_COOL: + this->set_mode_(MODE_COOL); + break; + case ClimateMode::CLIMATE_MODE_DRY: + this->set_mode_(MODE_DRY); + break; + case ClimateMode::CLIMATE_MODE_FAN_ONLY: + this->set_mode_(MODE_FAN_ONLY); + break; + case ClimateMode::CLIMATE_MODE_HEAT: + this->set_mode_(MODE_HEAT); + break; + default: + this->set_mode_(MODE_AUTO); + break; + } + this->set_power_(true); +} + +ClimateMode ControlData::get_mode() const { + if (!this->get_power_()) + return ClimateMode::CLIMATE_MODE_OFF; + switch (this->get_mode_()) { + case MODE_COOL: + return ClimateMode::CLIMATE_MODE_COOL; + case MODE_DRY: + return ClimateMode::CLIMATE_MODE_DRY; + case MODE_FAN_ONLY: + return ClimateMode::CLIMATE_MODE_FAN_ONLY; + case MODE_HEAT: + return ClimateMode::CLIMATE_MODE_HEAT; + default: + return ClimateMode::CLIMATE_MODE_HEAT_COOL; + } +} + +void ControlData::set_fan_mode(ClimateFanMode mode) { + switch (mode) { + case ClimateFanMode::CLIMATE_FAN_LOW: + this->set_fan_mode_(FAN_LOW); + break; + case ClimateFanMode::CLIMATE_FAN_MEDIUM: + this->set_fan_mode_(FAN_MEDIUM); + break; + case ClimateFanMode::CLIMATE_FAN_HIGH: + this->set_fan_mode_(FAN_HIGH); + break; + default: + this->set_fan_mode_(FAN_AUTO); + break; + } +} + +ClimateFanMode ControlData::get_fan_mode() const { + switch (this->get_fan_mode_()) { + case FAN_LOW: + return ClimateFanMode::CLIMATE_FAN_LOW; + case FAN_MEDIUM: + return ClimateFanMode::CLIMATE_FAN_MEDIUM; + case FAN_HIGH: + return ClimateFanMode::CLIMATE_FAN_HIGH; + default: + return ClimateFanMode::CLIMATE_FAN_AUTO; + } +} + +void MideaIR::control(const climate::ClimateCall &call) { + // swing and preset resets after unit powered off + if (call.get_mode() == climate::CLIMATE_MODE_OFF) { + this->swing_mode = climate::CLIMATE_SWING_OFF; + this->preset = climate::CLIMATE_PRESET_NONE; + } else if (call.get_swing_mode().has_value() && ((*call.get_swing_mode() == climate::CLIMATE_SWING_OFF && + this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || + (*call.get_swing_mode() == climate::CLIMATE_SWING_VERTICAL && + this->swing_mode == climate::CLIMATE_SWING_OFF))) { + this->swing_ = true; + } else if (call.get_preset().has_value() && + ((*call.get_preset() == climate::CLIMATE_PRESET_NONE && this->preset == climate::CLIMATE_PRESET_BOOST) || + (*call.get_preset() == climate::CLIMATE_PRESET_BOOST && this->preset == climate::CLIMATE_PRESET_NONE))) { + this->boost_ = true; + } + climate_ir::ClimateIR::control(call); +} + +void MideaIR::transmit_(MideaData &data) { + data.finalize(); + auto transmit = this->transmitter_->transmit(); + remote_base::MideaProtocol().encode(transmit.get_data(), data); + transmit.perform(); +} + +void MideaIR::transmit_state() { + if (this->swing_) { + SpecialData data(SpecialData::VSWING_TOGGLE); + this->transmit_(data); + this->swing_ = false; + return; + } + if (this->boost_) { + SpecialData data(SpecialData::TURBO_TOGGLE); + this->transmit_(data); + this->boost_ = false; + return; + } + ControlData data; + data.set_fahrenheit(this->fahrenheit_); + data.set_temp(this->target_temperature); + data.set_mode(this->mode); + data.set_fan_mode(this->fan_mode.value_or(ClimateFanMode::CLIMATE_FAN_AUTO)); + data.set_sleep_preset(this->preset == climate::CLIMATE_PRESET_SLEEP); + data.fix(); + this->transmit_(data); +} + +bool MideaIR::on_receive(remote_base::RemoteReceiveData data) { + auto midea = remote_base::MideaProtocol().decode(data); + if (midea.has_value()) + return this->on_midea_(*midea); + return coolix::CoolixClimate::on_coolix(this, data); +} + +bool MideaIR::on_midea_(const MideaData &data) { + ESP_LOGV(TAG, "Decoded Midea IR data: %s", data.to_string().c_str()); + if (data.type() == MideaData::MIDEA_TYPE_CONTROL) { + const ControlData status = data; + if (status.get_mode() != climate::CLIMATE_MODE_FAN_ONLY) + this->target_temperature = status.get_temp(); + this->mode = status.get_mode(); + this->fan_mode = status.get_fan_mode(); + if (status.get_sleep_preset()) + this->preset = climate::CLIMATE_PRESET_SLEEP; + else if (this->preset == climate::CLIMATE_PRESET_SLEEP) + this->preset = climate::CLIMATE_PRESET_NONE; + this->publish_state(); + return true; + } + if (data.type() == MideaData::MIDEA_TYPE_SPECIAL) { + switch (data[1]) { + case SpecialData::VSWING_TOGGLE: + this->swing_mode = this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? climate::CLIMATE_SWING_OFF + : climate::CLIMATE_SWING_VERTICAL; + break; + case SpecialData::TURBO_TOGGLE: + this->preset = this->preset == climate::CLIMATE_PRESET_BOOST ? climate::CLIMATE_PRESET_NONE + : climate::CLIMATE_PRESET_BOOST; + break; + } + this->publish_state(); + return true; + } + + return false; +} + +} // namespace midea_ir +} // namespace esphome diff --git a/esphome/components/midea_ir/midea_ir.h b/esphome/components/midea_ir/midea_ir.h new file mode 100644 index 0000000000..b89b2a7efc --- /dev/null +++ b/esphome/components/midea_ir/midea_ir.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" +#include "midea_data.h" + +namespace esphome { +namespace midea_ir { + +// Temperature +const uint8_t MIDEA_TEMPC_MIN = 17; // Celsius +const uint8_t MIDEA_TEMPC_MAX = 30; // Celsius +const uint8_t MIDEA_TEMPF_MIN = 62; // Fahrenheit +const uint8_t MIDEA_TEMPF_MAX = 86; // Fahrenheit + +class MideaIR : public climate_ir::ClimateIR { + public: + MideaIR() + : climate_ir::ClimateIR( + MIDEA_TEMPC_MIN, MIDEA_TEMPC_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_PRESET_NONE, climate::CLIMATE_PRESET_SLEEP, climate::CLIMATE_PRESET_BOOST}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + + /// Set use of Fahrenheit units + void set_fahrenheit(bool value) { + this->fahrenheit_ = value; + this->temperature_step_ = value ? 0.5f : 1.0f; + } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + void transmit_(MideaData &data); + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool on_midea_(const MideaData &data); + bool fahrenheit_{false}; + bool swing_{false}; + bool boost_{false}; +}; + +} // namespace midea_ir +} // namespace esphome diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 914ce42efe..9982447988 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -234,6 +234,45 @@ async def build_dumpers(config): return dumpers +# Coolix +( + CoolixData, + CoolixBinarySensor, + CoolixTrigger, + CoolixAction, + CoolixDumper, +) = declare_protocol("Coolix") +COOLIX_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t}) + + +@register_binary_sensor("coolix", CoolixBinarySensor, COOLIX_SCHEMA) +def coolix_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + CoolixData, + ("data", config[CONF_DATA]), + ) + ) + ) + + +@register_trigger("coolix", CoolixTrigger, CoolixData) +def coolix_trigger(var, config): + pass + + +@register_dumper("coolix", CoolixDumper) +def coolix_dumper(var, config): + pass + + +@register_action("coolix", CoolixAction, COOLIX_SCHEMA) +async def coolix_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) + cg.add(var.set_data(template_)) + + # Dish DishData, DishBinarySensor, DishTrigger, DishAction, DishDumper = declare_protocol( "Dish" diff --git a/esphome/components/remote_base/coolix_protocol.cpp b/esphome/components/remote_base/coolix_protocol.cpp new file mode 100644 index 0000000000..3e6e7e185a --- /dev/null +++ b/esphome/components/remote_base/coolix_protocol.cpp @@ -0,0 +1,84 @@ +#include "coolix_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.coolix"; + +static const int32_t TICK_US = 560; +static const int32_t HEADER_MARK_US = 8 * TICK_US; +static const int32_t HEADER_SPACE_US = 8 * TICK_US; +static const int32_t BIT_MARK_US = 1 * TICK_US; +static const int32_t BIT_ONE_SPACE_US = 3 * TICK_US; +static const int32_t BIT_ZERO_SPACE_US = 1 * TICK_US; +static const int32_t FOOTER_MARK_US = 1 * TICK_US; +static const int32_t FOOTER_SPACE_US = 10 * TICK_US; + +static void encode_data(RemoteTransmitData *dst, const CoolixData &src) { + // Break data into bytes, starting at the Most Significant + // Byte. Each byte then being sent normal, then followed inverted. + for (unsigned shift = 16;; shift -= 8) { + // Grab a bytes worth of data. + const uint8_t byte = src >> shift; + // Normal + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); + // Inverted + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (byte & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US); + // Data end + if (shift == 0) + break; + } +} + +void CoolixProtocol::encode(RemoteTransmitData *dst, const CoolixData &data) { + dst->set_carrier_frequency(38000); + dst->reserve(2 + 2 * 48 + 2 + 2 + 2 * 48 + 1); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + encode_data(dst, data); + dst->item(FOOTER_MARK_US, FOOTER_SPACE_US); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + encode_data(dst, data); + dst->mark(FOOTER_MARK_US); +} + +static bool decode_data(RemoteReceiveData &src, CoolixData &dst) { + uint32_t data = 0; + for (unsigned n = 3;; data <<= 8) { + // Read byte + for (uint32_t mask = 1 << 7; mask; mask >>= 1) { + if (!src.expect_mark(BIT_MARK_US)) + return false; + if (src.expect_space(BIT_ONE_SPACE_US)) + data |= mask; + else if (!src.expect_space(BIT_ZERO_SPACE_US)) + return false; + } + // Check for inverse byte + for (uint32_t mask = 1 << 7; mask; mask >>= 1) { + if (!src.expect_item(BIT_MARK_US, (data & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US)) + return false; + } + // Checking the end of reading + if (--n == 0) { + dst = data; + return true; + } + } +} + +optional CoolixProtocol::decode(RemoteReceiveData data) { + CoolixData first, second; + if (data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && decode_data(data, first) && + data.expect_item(FOOTER_MARK_US, FOOTER_SPACE_US) && data.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && + decode_data(data, second) && data.expect_mark(FOOTER_MARK_US) && first == second) + return first; + return {}; +} + +void CoolixProtocol::dump(const CoolixData &data) { ESP_LOGD(TAG, "Received Coolix: 0x%06X", data); } + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/coolix_protocol.h b/esphome/components/remote_base/coolix_protocol.h new file mode 100644 index 0000000000..9ce3eabb0e --- /dev/null +++ b/esphome/components/remote_base/coolix_protocol.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +using CoolixData = uint32_t; + +class CoolixProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const CoolixData &data) override; + optional decode(RemoteReceiveData data) override; + void dump(const CoolixData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Coolix) + +template class CoolixAction : public RemoteTransmitterActionBase { + TEMPLATABLE_VALUE(CoolixData, data) + void encode(RemoteTransmitData *dst, Ts... x) override { + CoolixData data = this->data_.value(x...); + CoolixProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.cpp b/esphome/components/remote_base/midea_protocol.cpp index a19a5b50c1..bf67429001 100644 --- a/esphome/components/remote_base/midea_protocol.cpp +++ b/esphome/components/remote_base/midea_protocol.cpp @@ -6,89 +6,63 @@ namespace remote_base { static const char *const TAG = "remote.midea"; +static const int32_t TICK_US = 560; +static const int32_t HEADER_MARK_US = 8 * TICK_US; +static const int32_t HEADER_SPACE_US = 8 * TICK_US; +static const int32_t BIT_MARK_US = 1 * TICK_US; +static const int32_t BIT_ONE_SPACE_US = 3 * TICK_US; +static const int32_t BIT_ZERO_SPACE_US = 1 * TICK_US; +static const int32_t FOOTER_MARK_US = 1 * TICK_US; +static const int32_t FOOTER_SPACE_US = 10 * TICK_US; + uint8_t MideaData::calc_cs_() const { uint8_t cs = 0; - for (const uint8_t *it = this->data(); it != this->data() + OFFSET_CS; ++it) - cs -= reverse_bits(*it); + for (uint8_t idx = 0; idx < OFFSET_CS; idx++) + cs -= reverse_bits(this->data_[idx]); return reverse_bits(cs); } -bool MideaData::check_compliment(const MideaData &rhs) const { - const uint8_t *it0 = rhs.data(); - for (const uint8_t *it1 = this->data(); it1 != this->data() + this->size(); ++it0, ++it1) { - if (*it0 != ~(*it1)) - return false; - } - return true; +bool MideaData::is_compliment(const MideaData &rhs) const { + return std::equal(this->data_.begin(), this->data_.end(), rhs.data_.begin(), + [](const uint8_t &a, const uint8_t &b) { return a + b == 255; }); } -void MideaProtocol::data(RemoteTransmitData *dst, const MideaData &src, bool compliment) { - for (const uint8_t *it = src.data(); it != src.data() + src.size(); ++it) { - const uint8_t data = compliment ? ~(*it) : *it; - for (uint8_t mask = 128; mask; mask >>= 1) { - if (data & mask) - one(dst); - else - zero(dst); - } - } -} - -void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &data) { +void MideaProtocol::encode(RemoteTransmitData *dst, const MideaData &src) { dst->set_carrier_frequency(38000); - dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 2); - MideaProtocol::header(dst); - MideaProtocol::data(dst, data); - MideaProtocol::footer(dst); - MideaProtocol::header(dst); - MideaProtocol::data(dst, data, true); - MideaProtocol::footer(dst); + dst->reserve(2 + 48 * 2 + 2 + 2 + 48 * 2 + 1); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + for (unsigned idx = 0; idx < 6; idx++) + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (src[idx] & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); + dst->item(FOOTER_MARK_US, FOOTER_SPACE_US); + dst->item(HEADER_MARK_US, HEADER_SPACE_US); + for (unsigned idx = 0; idx < 6; idx++) + for (uint8_t mask = 1 << 7; mask; mask >>= 1) + dst->item(BIT_MARK_US, (src[idx] & mask) ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US); + dst->mark(FOOTER_MARK_US); } -bool MideaProtocol::expect_one(RemoteReceiveData &src) { - if (!src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US)) - return false; - src.advance(2); - return true; -} - -bool MideaProtocol::expect_zero(RemoteReceiveData &src) { - if (!src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) - return false; - src.advance(2); - return true; -} - -bool MideaProtocol::expect_header(RemoteReceiveData &src) { - if (!src.peek_item(HEADER_HIGH_US, HEADER_LOW_US)) - return false; - src.advance(2); - return true; -} - -bool MideaProtocol::expect_footer(RemoteReceiveData &src) { - if (!src.peek_item(BIT_HIGH_US, MIN_GAP_US)) - return false; - src.advance(2); - return true; -} - -bool MideaProtocol::expect_data(RemoteReceiveData &src, MideaData &out) { - for (uint8_t *dst = out.data(); dst != out.data() + out.size(); ++dst) { - for (uint8_t mask = 128; mask; mask >>= 1) { - if (MideaProtocol::expect_one(src)) - *dst |= mask; - else if (!MideaProtocol::expect_zero(src)) +static bool decode_data(RemoteReceiveData &src, MideaData &dst) { + for (unsigned idx = 0; idx < 6; idx++) { + uint8_t data = 0; + for (uint8_t mask = 1 << 7; mask; mask >>= 1) { + if (!src.expect_mark(BIT_MARK_US)) + return false; + if (src.expect_space(BIT_ONE_SPACE_US)) + data |= mask; + else if (!src.expect_space(BIT_ZERO_SPACE_US)) return false; } + dst[idx] = data; } return true; } optional MideaProtocol::decode(RemoteReceiveData src) { MideaData out, inv; - if (MideaProtocol::expect_header(src) && MideaProtocol::expect_data(src, out) && MideaProtocol::expect_footer(src) && - out.is_valid() && MideaProtocol::expect_data(src, inv) && out.check_compliment(inv)) + if (src.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && decode_data(src, out) && out.is_valid() && + src.expect_item(FOOTER_MARK_US, FOOTER_SPACE_US) && src.expect_item(HEADER_MARK_US, HEADER_SPACE_US) && + decode_data(src, inv) && src.expect_mark(FOOTER_MARK_US) && out.is_compliment(inv)) return out; return {}; } diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 61e511601b..135a93b36d 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -1,5 +1,6 @@ #pragma once +#include #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "remote_base.h" @@ -9,70 +10,61 @@ namespace remote_base { class MideaData { public: - // Make zero-filled - MideaData() { memset(this->data_, 0, sizeof(this->data_)); } + // Make default + MideaData() {} // Make from initializer_list - MideaData(std::initializer_list data) { std::copy(data.begin(), data.end(), this->data()); } + MideaData(std::initializer_list data) { + std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); + } // Make from vector MideaData(const std::vector &data) { - memcpy(this->data_, data.data(), std::min(data.size(), sizeof(this->data_))); + std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); } // Default copy constructor MideaData(const MideaData &) = default; - uint8_t *data() { return this->data_; } - const uint8_t *data() const { return this->data_; } - uint8_t size() const { return sizeof(this->data_); } + uint8_t *data() { return this->data_.data(); } + const uint8_t *data() const { return this->data_.data(); } + uint8_t size() const { return this->data_.size(); } bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); } void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); } - bool check_compliment(const MideaData &rhs) const; - std::string to_string() const { return format_hex_pretty(this->data_, sizeof(this->data_)); } + bool is_compliment(const MideaData &rhs) const; + std::string to_string() const { return format_hex_pretty(this->data_.data(), this->data_.size()); } // compare only 40-bits - bool operator==(const MideaData &rhs) const { return !memcmp(this->data_, rhs.data_, OFFSET_CS); } + bool operator==(const MideaData &rhs) const { + return std::equal(this->data_.begin(), this->data_.begin() + OFFSET_CS, rhs.data_.begin()); + } enum MideaDataType : uint8_t { - MIDEA_TYPE_COMMAND = 0xA1, + MIDEA_TYPE_CONTROL = 0xA1, MIDEA_TYPE_SPECIAL = 0xA2, MIDEA_TYPE_FOLLOW_ME = 0xA4, }; MideaDataType type() const { return static_cast(this->data_[0]); } template T to() const { return T(*this); } + uint8_t &operator[](size_t idx) { return this->data_[idx]; } + const uint8_t &operator[](size_t idx) const { return this->data_[idx]; } protected: - void set_value_(uint8_t offset, uint8_t val_mask, uint8_t shift, uint8_t val) { - data_[offset] &= ~(val_mask << shift); - data_[offset] |= (val << shift); + uint8_t get_value_(uint8_t idx, uint8_t mask = 255, uint8_t shift = 0) const { + return (this->data_[idx] >> shift) & mask; } + void set_value_(uint8_t idx, uint8_t value, uint8_t mask = 255, uint8_t shift = 0) { + this->data_[idx] &= ~(mask << shift); + this->data_[idx] |= (value << shift); + } + void set_mask_(uint8_t idx, bool state, uint8_t mask = 255) { this->set_value_(idx, state ? mask : 0, mask); } static const uint8_t OFFSET_CS = 5; // 48-bits data - uint8_t data_[6]; + std::array data_; // Calculate checksum uint8_t calc_cs_() const; }; class MideaProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const MideaData &data) override; + void encode(RemoteTransmitData *dst, const MideaData &src) override; optional decode(RemoteReceiveData src) override; void dump(const MideaData &data) override; - - protected: - static const int32_t TICK_US = 560; - static const int32_t HEADER_HIGH_US = 8 * TICK_US; - static const int32_t HEADER_LOW_US = 8 * TICK_US; - static const int32_t BIT_HIGH_US = 1 * TICK_US; - static const int32_t BIT_ONE_LOW_US = 3 * TICK_US; - static const int32_t BIT_ZERO_LOW_US = 1 * TICK_US; - static const int32_t MIN_GAP_US = 10 * TICK_US; - static void one(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); } - static void zero(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); } - static void header(RemoteTransmitData *dst) { dst->item(HEADER_HIGH_US, HEADER_LOW_US); } - static void footer(RemoteTransmitData *dst) { dst->item(BIT_HIGH_US, MIN_GAP_US); } - static void data(RemoteTransmitData *dst, const MideaData &src, bool compliment = false); - static bool expect_one(RemoteReceiveData &src); - static bool expect_zero(RemoteReceiveData &src); - static bool expect_header(RemoteReceiveData &src); - static bool expect_footer(RemoteReceiveData &src); - static bool expect_data(RemoteReceiveData &src, MideaData &out); }; class MideaBinarySensor : public RemoteReceiverBinarySensorBase { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index db6bfeb79b..c039447fef 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -378,7 +378,7 @@ std::string format_hex(const uint8_t *data, size_t length) { } return ret; } -std::string format_hex(std::vector data) { return format_hex(data.data(), data.size()); } +std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } std::string format_hex_pretty(const uint8_t *data, size_t length) { @@ -396,6 +396,6 @@ std::string format_hex_pretty(const uint8_t *data, size_t length) { return ret + " (" + to_string(length) + ")"; return ret; } -std::string format_hex_pretty(std::vector data) { return format_hex_pretty(data.data(), data.size()); } +std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); } } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index d677e34649..5f793df1ea 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -110,6 +110,11 @@ void rgb_to_hsv(float red, float green, float blue, int &hue, float &saturation, /// Convert hue (0-360) & saturation/value percentage (0-1) to RGB floats (0-1) void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green, float &blue); +/// Convert degrees Celsius to degrees Fahrenheit. +static inline float celsius_to_fahrenheit(float value) { return value * 1.8f + 32.0f; } +/// Convert degrees Fahrenheit to degrees Celsius. +static inline float fahrenheit_to_celsius(float value) { return (value - 32.0f) / 1.8f; } + /*** * An interrupt helper class. * @@ -491,7 +496,7 @@ template::value, int> = 0> optional< /// Format the byte array \p data of length \p len in lowercased hex. std::string format_hex(const uint8_t *data, size_t length); /// Format the vector \p data in lowercased hex. -std::string format_hex(std::vector data); +std::string format_hex(const std::vector &data); /// Format an unsigned integer in lowercased hex, starting with the most significant byte. template::value, int> = 0> std::string format_hex(T val) { val = convert_big_endian(val); @@ -501,7 +506,7 @@ template::value, int> = 0> std::stri /// Format the byte array \p data of length \p len in pretty-printed, human-readable hex. std::string format_hex_pretty(const uint8_t *data, size_t length); /// Format the vector \p data in pretty-printed, human-readable hex. -std::string format_hex_pretty(std::vector data); +std::string format_hex_pretty(const std::vector &data); /// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte. template::value, int> = 0> std::string format_hex_pretty(T val) { val = convert_big_endian(val); diff --git a/tests/test1.yaml b/tests/test1.yaml index 2836a97e4f..959ffb0d2d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1718,6 +1718,9 @@ climate: name: HeatpumpIR Climate min_temperature: 18 max_temperature: 30 + - platform: midea_ir + name: Midea IR + use_fahrenheit: true - platform: midea on_state: logger.log: "State changed!"