diff --git a/esphome/components/climate_ir_samsung/__init__.py b/esphome/components/climate_ir_samsung/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/climate_ir_samsung/climate.py b/esphome/components/climate_ir_samsung/climate.py new file mode 100644 index 0000000000..fd1b7cc19a --- /dev/null +++ b/esphome/components/climate_ir_samsung/climate.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +from esphome.components import climate_ir +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@jorofi"] + +AUTO_LOAD = ["climate_ir"] + +samsung_ns = cg.esphome_ns.namespace("samsung") +SamsungClimate = samsung_ns.class_("SamsungClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SamsungClimate) + } +) + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/climate_ir_samsung/climate_ir_samsung.cpp b/esphome/components/climate_ir_samsung/climate_ir_samsung.cpp new file mode 100644 index 0000000000..41b7fe7892 --- /dev/null +++ b/esphome/components/climate_ir_samsung/climate_ir_samsung.cpp @@ -0,0 +1,240 @@ +#include "climate_ir_samsung.h" +#include +#include + +/// Count the number of bits of a certain type in an array. +/// @param[in] start A ptr to the start of the byte array to calculate over. +/// @param[in] length How many bytes to use in the calculation. +/// @param[in] ones Count the binary nr of `1` bits. False is count the `0`s. +/// @param[in] init Starting value of the calculation to use. (Default is 0) +/// @return The nr. of bits found of the given type found in the array. +uint16_t countBits(const uint8_t * const start, const uint16_t length, const bool ones, const uint16_t init) { + uint16_t count = init; + + for (uint16_t offset = 0; offset < length; offset++) + for (uint8_t currentbyte = *(start + offset); currentbyte; currentbyte >>= 1) + if (currentbyte & 1) count++; + + if (ones || length == 0) + return count; + else + return (length * 8) - count; +} + +/// Count the number of bits of a certain type in an Integer. +/// @param[in] data The value you want bits counted for. Starting from the LSB. +/// @param[in] length How many bits to use in the calculation? Starts at the LSB +/// @param[in] ones Count the binary nr of `1` bits. False is count the `0`s. +/// @param[in] init Starting value of the calculation to use. (Default is 0) +/// @return The nr. of bits found of the given type found in the Integer. +uint16_t countBits(const uint64_t data, const uint8_t length, const bool ones, const uint16_t init) { + uint16_t count = init; + uint8_t bitsSoFar = length; + + for (uint64_t remainder = data; remainder && bitsSoFar; remainder >>= 1, bitsSoFar--) + if (remainder & 1) count++; + + if (ones || length == 0) + return count; + else + return length - count; +} + +namespace esphome { +namespace climate_ir_samsung { + + void SamsungClimateIR::transmit_state() { + if(current_climate_mode != climate::ClimateMode::CLIMATE_MODE_OFF && this->mode == climate::ClimateMode::CLIMATE_MODE_OFF) { + setAndSendPowerState(false); + return; + } + + if(current_climate_mode == climate::ClimateMode::CLIMATE_MODE_OFF && this->mode != climate::ClimateMode::CLIMATE_MODE_OFF) { + setAndSendPowerState(true); + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + + current_climate_mode = this->mode; + + setMode(this->mode); + setTemp(this->target_temperature); + setSwing(this->swing_mode); + setFan(this->fan_mode.has_value() ? this->fan_mode.value() : climate::CLIMATE_FAN_AUTO); + + send(); + } + + /// Send the current state of the climate object. + void SamsungClimateIR::send() { + + checksum(); + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(SAMSUNG_IR_FREQUENCY); + + // Header + data->mark(SAMSUNG_AIRCON1_HDR_MARK); + data->space(SAMSUNG_AIRCON1_HDR_SPACE); + + for (int i = 0; i < 21; i++) + { + + if (i == 7 || i == 14) + { + data->mark(SAMSUNG_AIRCON1_BIT_MARK); + data->space(SAMSUNG_AIRCON1_MSG_SPACE); + + data->mark(SAMSUNG_AIRCON1_HDR_MARK); + data->space(SAMSUNG_AIRCON1_HDR_SPACE); + } + + uint8_t sendByte = protocol.raw[i]; + + for (int y = 0; y < 8; y++) + { + if (sendByte & 0x01) + { + // ESP_LOGI(TAG, "For Y %d 1", y); + data->mark(SAMSUNG_AIRCON1_BIT_MARK); + data->space(SAMSUNG_AIRCON1_ONE_SPACE); + } + else + { + // ESP_LOGI(TAG, "For Y %d 0", y); + data->mark(SAMSUNG_AIRCON1_BIT_MARK); + data->space(SAMSUNG_AIRCON1_ZERO_SPACE); + } + + sendByte >>= 1; + } + } + + data->mark(SAMSUNG_AIRCON1_BIT_MARK); + data->space(0); + + transmit.perform(); + } + + /// Set the vertical swing setting of the A/C. + void SamsungClimateIR::setSwing(const climate::ClimateSwingMode swingMode) { + switch (swingMode) { + case climate::ClimateSwingMode::CLIMATE_SWING_BOTH: + protocol.Swing = kSamsungAcSwingBoth; + break; + case climate::ClimateSwingMode::CLIMATE_SWING_HORIZONTAL: + protocol.Swing = kSamsungAcSwingH;; + break; + case climate::ClimateSwingMode::CLIMATE_SWING_VERTICAL: + protocol.Swing = kSamsungAcSwingV;; + break; + case climate::ClimateSwingMode::CLIMATE_SWING_OFF: + default: + protocol.Swing = kSamsungAcSwingOff;; + break; + } + } + + /// Set the operating mode of the A/C. + /// @param[in] climateMode The desired operating mode. + void SamsungClimateIR::setMode(const climate::ClimateMode climateMode) { + switch (climateMode) { + case climate::ClimateMode::CLIMATE_MODE_HEAT: + protocol.Mode = kSamsungAcHeat; + break; + case climate::ClimateMode::CLIMATE_MODE_DRY: + protocol.Mode = kSamsungAcDry; + break; + case climate::ClimateMode::CLIMATE_MODE_COOL: + protocol.Mode = kSamsungAcCool; + break; + case climate::ClimateMode::CLIMATE_MODE_FAN_ONLY: + protocol.Mode = kSamsungAcFan; + break; + case climate::ClimateMode::CLIMATE_MODE_HEAT_COOL: + case climate::ClimateMode::CLIMATE_MODE_AUTO: + default: + protocol.Mode = kSamsungAcAuto; + break; + } + } + + /// Set the temperature. + /// @param[in] temp The temperature in degrees celsius. + void SamsungClimateIR::setTemp(const uint8_t temp) { + uint8_t newtemp = std::max(kSamsungAcMinTemp, temp); + newtemp = std::min(kSamsungAcMaxTemp, newtemp); + protocol.Temp = newtemp - kSamsungAcMinTemp; + } + + /// Change the AC power state. + /// @param[in] on true, the AC is on. false, the AC is off. + void SamsungClimateIR::setAndSendPowerState(const bool on) { + + static const uint8_t kOn[kSamsungAcExtendedStateLength] = { + 0x02, 0x92, 0x0F, 0x00, 0x00, 0x00, 0xF0, + 0x01, 0xD2, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xE2, 0xFE, 0x71, 0x80, 0x11, 0xF0}; + + static const uint8_t kOff[kSamsungAcExtendedStateLength] = { + 0x02, 0xB2, 0x0F, 0x00, 0x00, 0x00, 0xC0, + 0x01, 0xD2, 0x0F, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x02, 0xFF, 0x71, 0x80, 0x11, 0xC0}; + + std::memcpy(protocol.raw, on ? kOn : kOff, kSamsungAcExtendedStateLength); + + send(); + + std::memcpy(protocol.raw, kReset, kSamsungAcExtendedStateLength); + } + + /// Set the fan speed. + void SamsungClimateIR::setFan(const climate::ClimateFanMode fanMode) { + switch (fanMode) { + case climate::ClimateFanMode::CLIMATE_FAN_LOW: + protocol.Fan = kSamsungAcFanAuto; + break; + case climate::ClimateFanMode::CLIMATE_FAN_MEDIUM: + protocol.Fan = kSamsungAcFanMed; + break; + case climate::ClimateFanMode::CLIMATE_FAN_HIGH: + protocol.Fan = kSamsungAcFanHigh; + break; + case climate::ClimateFanMode::CLIMATE_FAN_AUTO: + default: + protocol.Fan = kSamsungAcFanAuto; + break; + } + } + + /// Calculate the checksum for a given state section. + /// @param[in] section The array to calc the checksum of. + /// @return The calculated checksum value. + /// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1538#issuecomment-894645947 + uint8_t SamsungClimateIR::calcSectionChecksum(const uint8_t *section) { + uint8_t sum = 0; + + sum += countBits(*section, 8); // Include the entire first byte + // The lower half of the second byte. + sum += countBits(GETBITS8(*(section + 1), kLowNibble, kNibbleSize), 8); + // The upper half of the third byte. + sum += countBits(GETBITS8(*(section + 2), kHighNibble, kNibbleSize), 8); + // The next 4 bytes. + sum += countBits(section + 3, 4); + // Bitwise invert the result. + return sum ^ UINT8_MAX; + } + + /// Update the checksum for the internal state. + void SamsungClimateIR::checksum(void) { + uint8_t sectionsum = calcSectionChecksum(protocol.raw); + protocol.Sum1Upper = GETBITS8(sectionsum, kHighNibble, kNibbleSize); + protocol.Sum1Lower = GETBITS8(sectionsum, kLowNibble, kNibbleSize); + sectionsum = calcSectionChecksum(protocol.raw + kSamsungAcSectionLength); + protocol.Sum2Upper = GETBITS8(sectionsum, kHighNibble, kNibbleSize); + protocol.Sum2Lower = GETBITS8(sectionsum, kLowNibble, kNibbleSize); + sectionsum = calcSectionChecksum(protocol.raw + kSamsungAcSectionLength * 2); + protocol.Sum3Upper = GETBITS8(sectionsum, kHighNibble, kNibbleSize); + protocol.Sum3Lower = GETBITS8(sectionsum, kLowNibble, kNibbleSize); + } +}} diff --git a/esphome/components/climate_ir_samsung/climate_ir_samsung.h b/esphome/components/climate_ir_samsung/climate_ir_samsung.h new file mode 100644 index 0000000000..5c7be0240d --- /dev/null +++ b/esphome/components/climate_ir_samsung/climate_ir_samsung.h @@ -0,0 +1,208 @@ +#pragma once + +#include "esphome/components/climate/climate_mode.h" +#include "esphome/components/climate_ir/climate_ir.h" + +#define SAMSUNG_AIRCON1_HDR_MARK 3000 +#define SAMSUNG_AIRCON1_HDR_SPACE 9000 +#define SAMSUNG_AIRCON1_BIT_MARK 500 +#define SAMSUNG_AIRCON1_ONE_SPACE 1500 +#define SAMSUNG_AIRCON1_ZERO_SPACE 500 +#define SAMSUNG_AIRCON1_MSG_SPACE 2000 +#define GETBITS8(data, offset, size) \ + (((data) & (((uint8_t)UINT8_MAX >> (8 - (size))) << (offset))) >> (offset)) + +uint16_t countBits(const uint8_t * const start, const uint16_t length, const bool ones = true, const uint16_t init = 0); +uint16_t countBits(const uint64_t data, const uint8_t length, const bool ones = true, const uint16_t init = 0); + +namespace esphome { +namespace climate_ir_samsung { + + static const char *const TAG = "samsung.climate"; + static const uint32_t SAMSUNG_IR_FREQUENCY = 38000; + const uint16_t kSamsungAcExtendedStateLength = 21; + const uint16_t kSamsungAcSectionLength = 7; + + // Temperature + const uint8_t kSamsungAcMinTemp = 16; // C Mask 0b11110000 + const uint8_t kSamsungAcMaxTemp = 30; // C Mask 0b11110000 + const uint8_t kSamsungAcAutoTemp = 25; // C Mask 0b11110000 + + // Mode + const uint8_t kSamsungAcAuto = 0; + const uint8_t kSamsungAcCool = 1; + const uint8_t kSamsungAcDry = 2; + const uint8_t kSamsungAcFan = 3; + const uint8_t kSamsungAcHeat = 4; + + // Fan + const uint8_t kSamsungAcFanAuto = 0; + const uint8_t kSamsungAcFanLow = 2; + const uint8_t kSamsungAcFanMed = 4; + const uint8_t kSamsungAcFanHigh = 5; + const uint8_t kSamsungAcFanAuto2 = 6; + const uint8_t kSamsungAcFanTurbo = 7; + + // Swing + const uint8_t kSamsungAcSwingV = 0b010; + const uint8_t kSamsungAcSwingH = 0b011; + const uint8_t kSamsungAcSwingBoth = 0b100; + const uint8_t kSamsungAcSwingOff = 0b111; + + // Power + const uint8_t kNibbleSize = 4; + const uint8_t kLowNibble = 0; + const uint8_t kHighNibble = 4; + const uint8_t kModeBitsSize = 3; + + // static const uint8_t kReset[kSamsungAcExtendedStateLength] = {0x02, 0x92, 0x0F, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x02, 0xAE, 0x71, 0x00, 0x15, 0xF0}; + static const uint8_t kReset[kSamsungAcExtendedStateLength] = { + 0x02, 0x92, 0x0F, 0x00, 0x00, 0x00, 0xF0, + 0x01, 0x02, 0xAE, 0x71, 0x00, 0x15, 0xF0}; + + /// Native representation of a Samsung A/C message. + union SamsungProtocol { + uint8_t raw[kSamsungAcExtendedStateLength]; ///< State in code form. + struct { // Standard message map + // Byte 0 + uint8_t :8; + // Byte 1 + uint8_t :4; + uint8_t :4; // Sum1Lower + // Byte 2 + uint8_t :4; // Sum1Upper + uint8_t :4; + // Byte 3 + uint8_t :8; + // Byte 4 + uint8_t :8; + // Byte 5 + uint8_t :4; + uint8_t Sleep5 :1; + uint8_t Quiet :1; + uint8_t :2; + // Byte 6 + uint8_t :4; + uint8_t Power1 :2; + uint8_t :2; + // Byte 7 + uint8_t :8; + // Byte 8 + uint8_t :4; + uint8_t :4; // Sum2Lower + // Byte 9 + uint8_t :4; // Sum1Upper + uint8_t Swing :3; + uint8_t :1; + // Byte 10 + uint8_t :1; + uint8_t FanSpecial :3; // Powerful, Breeze/WindFree, Econo + uint8_t Display :1; + uint8_t :2; + uint8_t CleanToggle10 :1; + // Byte 11 + uint8_t Ion :1; + uint8_t CleanToggle11 :1; + uint8_t :2; + uint8_t Temp :4; + // Byte 12 + uint8_t :1; + uint8_t Fan :3; + uint8_t Mode :3; + uint8_t :1; + // Byte 13 + uint8_t :2; + uint8_t BeepToggle :1; + uint8_t :1; + uint8_t Power2 :2; + uint8_t :2; + }; + struct { // Extended message map + // 1st Section + // Byte 0 + uint8_t :8; + // Byte 1 + uint8_t :4; + uint8_t Sum1Lower :4; + // Byte 2 + uint8_t Sum1Upper :4; + uint8_t :4; + // Byte 3 + uint8_t :8; + // Byte 4 + uint8_t :8; + // Byte 5 + uint8_t :8; + // Byte 6 + uint8_t :8; + // 2nd Section + // Byte 7 + uint8_t :8; + // Byte 8 + uint8_t :4; + uint8_t Sum2Lower :4; + // Byte 9 + uint8_t Sum2Upper :4; + uint8_t OffTimeMins :3; // In units of 10's of mins + uint8_t OffTimeHrs1 :1; // LSB of the number of hours. + // Byte 10 + uint8_t OffTimeHrs2 :4; // MSBs of the number of hours. + uint8_t OnTimeMins :3; // In units of 10's of mins + uint8_t OnTimeHrs1 :1; // LSB of the number of hours. + // Byte 11 + uint8_t OnTimeHrs2 :4; // MSBs of the number of hours. + uint8_t :4; + // Byte 12 + uint8_t OffTimeDay :1; + uint8_t OnTimerEnable :1; + uint8_t OffTimerEnable :1; + uint8_t Sleep12 :1; + uint8_t OnTimeDay :1; + uint8_t :3; + // Byte 13 + uint8_t :8; + // 3rd Section + // Byte 14 + uint8_t :8; + // Byte 15 + uint8_t :4; + uint8_t Sum3Lower :4; + // Byte 16 + uint8_t Sum3Upper :4; + uint8_t :4; + // Byte 17 + uint8_t :8; + // Byte 18 + uint8_t :8; + // Byte 19 + uint8_t :8; + // Byte 20 + uint8_t :8; + }; + }; + + class SamsungClimate : public climate_ir::ClimateIR { + + SamsungProtocol protocol; + climate::ClimateMode current_climate_mode; + + public: SamsungClimate() : + climate_ir::ClimateIR( + kSamsungAcMinTemp, kSamsungAcMaxTemp, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) { } + + protected: + void transmit_state() override; + + void send(); + void setSwing(const climate::ClimateSwingMode swingMode); + void setMode(const climate::ClimateMode mode); + void setTemp(const uint8_t temp); + void setAndSendPowerState(const bool on); + void setFan(const climate::ClimateFanMode fanMode); + + void checksum(void); + static uint8_t calcSectionChecksum(const uint8_t *section); + }; +}} \ No newline at end of file