mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	New Midea IR component, improvements and fixes (#2847)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -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(); | ||||
| } | ||||
|   | ||||
| @@ -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<climate::ClimateFanMode> fan_modes = {}, | ||||
|             std::set<climate::ClimateSwingMode> swing_modes = {}) { | ||||
|             std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> 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<climate::ClimateFanMode> fan_modes_ = {}; | ||||
|   std::set<climate::ClimateSwingMode> swing_modes_ = {}; | ||||
|   std::set<climate::ClimatePreset> presets_ = {}; | ||||
|  | ||||
|   remote_transmitter::RemoteTransmitterComponent *transmitter_; | ||||
|   sensor::Sensor *sensor_{nullptr}; | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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}; | ||||
| }; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/midea_ir/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/midea_ir/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										25
									
								
								esphome/components/midea_ir/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								esphome/components/midea_ir/climate.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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])) | ||||
							
								
								
									
										92
									
								
								esphome/components/midea_ir/midea_data.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								esphome/components/midea_ir/midea_data.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<FanMode>(this->get_value_(1, 3, 3)); } | ||||
|   void set_mode_(Mode mode) { this->set_value_(1, mode, 7); } | ||||
|   Mode get_mode_() const { return static_cast<Mode>(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 | ||||
							
								
								
									
										201
									
								
								esphome/components/midea_ir/midea_ir.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								esphome/components/midea_ir/midea_ir.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<float>(celsius_to_fahrenheit(temp), MIDEA_TEMPF_MIN, MIDEA_TEMPF_MAX); | ||||
|   } else { | ||||
|     min = MIDEA_TEMPC_MIN; | ||||
|     temp = esphome::clamp<float>(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<float>(temp + MIDEA_TEMPF_MIN)); | ||||
|   return static_cast<float>(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 | ||||
							
								
								
									
										47
									
								
								esphome/components/midea_ir/midea_ir.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/midea_ir/midea_ir.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -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" | ||||
|   | ||||
							
								
								
									
										84
									
								
								esphome/components/remote_base/coolix_protocol.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								esphome/components/remote_base/coolix_protocol.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -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<CoolixData> 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 | ||||
							
								
								
									
										30
									
								
								esphome/components/remote_base/coolix_protocol.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/remote_base/coolix_protocol.h
									
									
									
									
									
										Normal file
									
								
							| @@ -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<CoolixData> { | ||||
|  public: | ||||
|   void encode(RemoteTransmitData *dst, const CoolixData &data) override; | ||||
|   optional<CoolixData> decode(RemoteReceiveData data) override; | ||||
|   void dump(const CoolixData &data) override; | ||||
| }; | ||||
|  | ||||
| DECLARE_REMOTE_PROTOCOL(Coolix) | ||||
|  | ||||
| template<typename... Ts> class CoolixAction : public RemoteTransmitterActionBase<Ts...> { | ||||
|   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 | ||||
| @@ -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<MideaData> 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 {}; | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <array> | ||||
| #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<uint8_t> data) { std::copy(data.begin(), data.end(), this->data()); } | ||||
|   MideaData(std::initializer_list<uint8_t> data) { | ||||
|     std::copy_n(data.begin(), std::min(data.size(), this->data_.size()), this->data_.begin()); | ||||
|   } | ||||
|   // Make from vector | ||||
|   MideaData(const std::vector<uint8_t> &data) { | ||||
|     memcpy(this->data_, data.data(), std::min<size_t>(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<MideaDataType>(this->data_[0]); } | ||||
|   template<typename T> 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<uint8_t, 6> data_; | ||||
|   // Calculate checksum | ||||
|   uint8_t calc_cs_() const; | ||||
| }; | ||||
|  | ||||
| class MideaProtocol : public RemoteProtocol<MideaData> { | ||||
|  public: | ||||
|   void encode(RemoteTransmitData *dst, const MideaData &data) override; | ||||
|   void encode(RemoteTransmitData *dst, const MideaData &src) override; | ||||
|   optional<MideaData> 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 { | ||||
|   | ||||
| @@ -378,7 +378,7 @@ std::string format_hex(const uint8_t *data, size_t length) { | ||||
|   } | ||||
|   return ret; | ||||
| } | ||||
| std::string format_hex(std::vector<uint8_t> data) { return format_hex(data.data(), data.size()); } | ||||
| std::string format_hex(const std::vector<uint8_t> &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<uint8_t> data) { return format_hex_pretty(data.data(), data.size()); } | ||||
| std::string format_hex_pretty(const std::vector<uint8_t> &data) { return format_hex_pretty(data.data(), data.size()); } | ||||
|  | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -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<typename T, enable_if_t<std::is_unsigned<T>::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<uint8_t> data); | ||||
| std::string format_hex(const std::vector<uint8_t> &data); | ||||
| /// Format an unsigned integer in lowercased hex, starting with the most significant byte. | ||||
| template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex(T val) { | ||||
|   val = convert_big_endian(val); | ||||
| @@ -501,7 +506,7 @@ template<typename T, enable_if_t<std::is_unsigned<T>::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<uint8_t> data); | ||||
| std::string format_hex_pretty(const std::vector<uint8_t> &data); | ||||
| /// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte. | ||||
| template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex_pretty(T val) { | ||||
|   val = convert_big_endian(val); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user