diff --git a/CODEOWNERS b/CODEOWNERS index 770c1fa7de..0a9be8364d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -238,6 +238,7 @@ esphome/components/version/* @esphome/core esphome/components/wake_on_lan/* @willwill2will54 esphome/components/web_server_base/* @OttoWinter esphome/components/whirlpool/* @glmnet +esphome/components/whynter/* @aeonsablaze esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs diff --git a/esphome/components/whynter/__init__.py b/esphome/components/whynter/__init__.py new file mode 100644 index 0000000000..7316776d78 --- /dev/null +++ b/esphome/components/whynter/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@aeonsablaze"] diff --git a/esphome/components/whynter/climate.py b/esphome/components/whynter/climate.py new file mode 100644 index 0000000000..b9dc5868bc --- /dev/null +++ b/esphome/components/whynter/climate.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] + +whynter_ns = cg.esphome_ns.namespace("whynter") +Whynter = whynter_ns.class_("Whynter", climate_ir.ClimateIR) + +CONF_USE_FAHRENHEIT = "use_fahrenheit" + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Whynter), + 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/whynter/whynter.cpp b/esphome/components/whynter/whynter.cpp new file mode 100644 index 0000000000..190bf70acc --- /dev/null +++ b/esphome/components/whynter/whynter.cpp @@ -0,0 +1,181 @@ +#include "whynter.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace whynter { + +static const char *const TAG = "climate.whynter"; + +const uint16_t BITS = 32; + +// Static First Byte +const uint32_t COMMAND_MASK = 0xFF << 24; +const uint32_t COMMAND_CODE = 0x12 << 24; + +// Power +const uint32_t POWER_SHIFT = 8; +const uint32_t POWER_MASK = 1 << POWER_SHIFT; +const uint32_t POWER_OFF = 0 << POWER_SHIFT; + +// Mode +const uint32_t MODE_SHIFT = 16; +const uint32_t MODE_MASK = 0b1111 << MODE_SHIFT; +const uint32_t MODE_FAN = 0b0001 << MODE_SHIFT; +const uint32_t MODE_DRY = 0b0010 << MODE_SHIFT; +const uint32_t MODE_HEAT = 0b0100 << MODE_SHIFT; +const uint32_t MODE_COOL = 0b1000 << MODE_SHIFT; + +// Fan Speed +const uint32_t FAN_SHIFT = 20; +const uint32_t FAN_MASK = 0b111 << FAN_SHIFT; +const uint32_t FAN_HIGH = 0b001 << FAN_SHIFT; +const uint32_t FAN_MED = 0b010 << FAN_SHIFT; +const uint32_t FAN_LOW = 0b100 << FAN_SHIFT; + +// Temperature Unit +const uint32_t UNIT_SHIFT = 10; +const uint32_t UNIT_MASK = 1 << UNIT_SHIFT; + +// Temperature Value +const uint32_t TEMP_MASK = 0xFF; +const uint32_t TEMP_OFFSET_C = 16; + +void Whynter::transmit_state() { + uint32_t remote_state = COMMAND_CODE; + if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) + this->mode = climate::CLIMATE_MODE_COOL; + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state |= POWER_MASK; + remote_state |= MODE_FAN; + break; + case climate::CLIMATE_MODE_DRY: + remote_state |= POWER_MASK; + remote_state |= MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state |= POWER_MASK; + remote_state |= MODE_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + remote_state |= POWER_MASK; + remote_state |= MODE_COOL; + break; + case climate::CLIMATE_MODE_OFF: + remote_state |= POWER_OFF; + break; + default: + remote_state |= POWER_OFF; + } + mode_before_ = this->mode; + + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + remote_state |= FAN_LOW; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= FAN_MED; + break; + case climate::CLIMATE_FAN_HIGH: + remote_state |= FAN_HIGH; + break; + default: + remote_state |= FAN_HIGH; + } + + if (fahrenheit_) { + remote_state |= UNIT_MASK; + uint8_t temp = + (uint8_t) clamp(esphome::celsius_to_fahrenheit(this->target_temperature), TEMP_MIN_F, TEMP_MAX_F); + temp = esphome::reverse_bits(temp); + remote_state |= temp; + } else { + uint8_t temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN_C, TEMP_MAX_C) - TEMP_OFFSET_C); + temp = esphome::reverse_bits(temp); + remote_state |= temp; + } + + transmit_(remote_state); + this->publish_state(); +} + +bool Whynter::on_receive(remote_base::RemoteReceiveData data) { + uint8_t nbits = 0; + uint32_t remote_state = 0; + + if (!data.expect_item(this->header_high_, this->header_low_)) + return false; + + for (nbits = 0; nbits < 32; nbits++) { + if (data.expect_item(this->bit_high_, this->bit_one_low_)) { + remote_state = (remote_state << 1) | 1; + } else if (data.expect_item(this->bit_high_, this->bit_zero_low_)) { + remote_state = (remote_state << 1) | 0; + } else if (nbits == BITS) { + break; + } else { + return false; + } + } + + ESP_LOGD(TAG, "Decoded 0x%02X", remote_state); + if ((remote_state & COMMAND_MASK) != COMMAND_CODE) + return false; + if ((remote_state & POWER_MASK) != POWER_MASK) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + if ((remote_state & MODE_MASK) == MODE_FAN) { + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + } else if ((remote_state & MODE_MASK) == MODE_DRY) { + this->mode = climate::CLIMATE_MODE_DRY; + } else if ((remote_state & MODE_MASK) == MODE_HEAT) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else if ((remote_state & MODE_MASK) == MODE_COOL) { + this->mode = climate::CLIMATE_MODE_COOL; + } + + // Temperature + if ((remote_state & UNIT_MASK) == UNIT_MASK) { // Fahrenheit + this->target_temperature = esphome::fahrenheit_to_celsius(esphome::reverse_bits(remote_state & TEMP_MASK) >> 24); + } else { // Celsius + this->target_temperature = (esphome::reverse_bits(remote_state & TEMP_MASK) >> 24) + TEMP_OFFSET_C; + } + + // Fan Speed + if ((remote_state & FAN_MASK) == FAN_LOW) { + this->fan_mode = climate::CLIMATE_FAN_LOW; + } else if ((remote_state & FAN_MASK) == FAN_MED) { + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + } else if ((remote_state & FAN_MASK) == FAN_HIGH) { + this->fan_mode = climate::CLIMATE_FAN_HIGH; + } + } + this->publish_state(); + + return true; +} + +void Whynter::transmit_(uint32_t value) { + ESP_LOGD(TAG, "Sending whynter code: 0x%02X", value); + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + + data->set_carrier_frequency(38000); + data->reserve(2 + BITS * 2u); + + data->item(this->header_high_, this->header_low_); + + for (uint32_t mask = 1UL << (BITS - 1); mask != 0; mask >>= 1) { + if (value & mask) { + data->item(this->bit_high_, this->bit_one_low_); + } else { + data->item(this->bit_high_, this->bit_zero_low_); + } + } + data->mark(this->bit_high_); + transmit.perform(); +} + +} // namespace whynter +} // namespace esphome diff --git a/esphome/components/whynter/whynter.h b/esphome/components/whynter/whynter.h new file mode 100644 index 0000000000..939583ebfb --- /dev/null +++ b/esphome/components/whynter/whynter.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace whynter { + +// Temperature +const uint8_t TEMP_MIN_C = 16; // Celsius +const uint8_t TEMP_MAX_C = 32; // Celsius +const uint8_t TEMP_MIN_F = 61; // Fahrenheit +const uint8_t TEMP_MAX_F = 89; // Fahrenheit + +class Whynter : public climate_ir::ClimateIR { + public: + Whynter() + : climate_ir::ClimateIR(TEMP_MIN_C, TEMP_MAX_C, 1.0, true, true, + {climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, {}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { climate_ir::ClimateIR::control(call); } + + // Set use of Fahrenheit units + void set_fahrenheit(bool value) { + this->fahrenheit_ = value; + this->temperature_step_ = 1.0f; + this->minimum_temperature_ = esphome::fahrenheit_to_celsius(TEMP_MIN_F); + this->maximum_temperature_ = esphome::fahrenheit_to_celsius(TEMP_MAX_F); + } + + 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; + + void transmit_(uint32_t value); + + uint32_t header_high_ = 8000; + uint32_t header_low_ = 4000; + uint32_t bit_high_ = 600; + uint32_t bit_one_low_ = 1600; + uint32_t bit_zero_low_ = 550; + + bool fahrenheit_{false}; + + climate::ClimateMode mode_before_{climate::CLIMATE_MODE_OFF}; +}; + +} // namespace whynter +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index b5a7b13722..40648f7a8d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1897,6 +1897,8 @@ climate: name: My Bedjet bedjet_id: my_bedjet_client heat_mode: extended + - platform: whynter + name: Whynter script: - id: climate_custom