From e62443933c92f3e34f9fe6d3284e7fa7047f1b7d Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 27 May 2019 15:17:12 -0300 Subject: [PATCH] Add TCL112 climate (#523) * Add TCL112 climate * fix default mode * Updates * Update * Update climate.py My mistake :( Co-authored-by: Otto Winter --- esphome/components/tcl112/__init__.py | 0 esphome/components/tcl112/climate.py | 36 ++++++ esphome/components/tcl112/tcl112.cpp | 152 ++++++++++++++++++++++++++ esphome/components/tcl112/tcl112.h | 40 +++++++ tests/test1.yaml | 9 ++ 5 files changed, 237 insertions(+) create mode 100644 esphome/components/tcl112/__init__.py create mode 100644 esphome/components/tcl112/climate.py create mode 100644 esphome/components/tcl112/tcl112.cpp create mode 100644 esphome/components/tcl112/tcl112.h diff --git a/esphome/components/tcl112/__init__.py b/esphome/components/tcl112/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py new file mode 100644 index 0000000000..50fef7b125 --- /dev/null +++ b/esphome/components/tcl112/climate.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, remote_transmitter, sensor +from esphome.const import CONF_ID, CONF_SENSOR + +AUTO_LOAD = ['sensor'] + +tcl112_ns = cg.esphome_ns.namespace('tcl112') +Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate.Climate, cg.Component) + +CONF_TRANSMITTER_ID = 'transmitter_id' +CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SUPPORTS_COOL = 'supports_cool' + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(Tcl112Climate), + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), +}).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) + cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) + if CONF_SENSOR in config: + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + + transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp new file mode 100644 index 0000000000..cbe2f53402 --- /dev/null +++ b/esphome/components/tcl112/tcl112.cpp @@ -0,0 +1,152 @@ +#include "tcl112.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tcl112 { + +static const char *TAG = "tcl112.climate"; + +const uint16_t TCL112_STATE_LENGTH = 14; +const uint16_t TCL112_BITS = TCL112_STATE_LENGTH * 8; + +const uint8_t TCL112_HEAT = 1; +const uint8_t TCL112_DRY = 2; +const uint8_t TCL112_COOL = 3; +const uint8_t TCL112_FAN = 7; +const uint8_t TCL112_AUTO = 8; + +const uint8_t TCL112_POWER_MASK = 0x04; + +const uint8_t TCL112_HALF_DEGREE = 0b00100000; +const float TCL112_TEMP_MAX = 31.0; +const float TCL112_TEMP_MIN = 16.0; + +const uint16_t TCL112_HEADER_MARK = 3000; +const uint16_t TCL112_HEADER_SPACE = 1650; +const uint16_t TCL112_BIT_MARK = 500; +const uint16_t TCL112_ONE_SPACE = 1050; +const uint16_t TCL112_ZERO_SPACE = 325; +const uint32_t TCL112_GAP = TCL112_HEADER_SPACE; + +climate::ClimateTraits Tcl112Climate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->sensor_ != nullptr); + traits.set_supports_auto_mode(true); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_two_point_target_temperature(false); + traits.set_supports_away(false); + traits.set_visual_min_temperature(TCL112_TEMP_MIN); + traits.set_visual_max_temperature(TCL112_TEMP_MAX); + traits.set_visual_temperature_step(.5f); + return traits; +} + +void Tcl112Climate::setup() { + if (this->sensor_) { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + // current temperature changed, publish state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + } else + this->current_temperature = NAN; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } else { + // restore from defaults + this->mode = climate::CLIMATE_MODE_OFF; + this->target_temperature = 24; + } +} + +void Tcl112Climate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + + this->transmit_state_(); + this->publish_state(); +} + +void Tcl112Climate::transmit_state_() { + uint8_t remote_state[TCL112_STATE_LENGTH] = {0}; + + // A known good state. (On, Cool, 24C) + remote_state[0] = 0x23; + remote_state[1] = 0xCB; + remote_state[2] = 0x26; + remote_state[3] = 0x01; + remote_state[5] = 0x24; + remote_state[6] = 0x03; + remote_state[7] = 0x07; + remote_state[8] = 0x40; + + // Set mode + switch (this->mode) { + case climate::CLIMATE_MODE_AUTO: + remote_state[6] &= 0xF0; + remote_state[6] |= TCL112_AUTO; + break; + case climate::CLIMATE_MODE_COOL: + remote_state[6] &= 0xF0; + remote_state[6] |= TCL112_COOL; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[6] &= 0xF0; + remote_state[6] |= TCL112_HEAT; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state[5] &= ~TCL112_POWER_MASK; + break; + } + + // Set temperature + // Make sure we have desired temp in the correct range. + float safecelsius = std::max(this->target_temperature, TCL112_TEMP_MIN); + safecelsius = std::min(safecelsius, TCL112_TEMP_MAX); + // Convert to integer nr. of half degrees. + auto half_degrees = static_cast(safecelsius * 2); + if (half_degrees & 1) // Do we have a half degree celsius? + remote_state[12] |= TCL112_HALF_DEGREE; // Add 0.5 degrees + else + remote_state[12] &= ~TCL112_HALF_DEGREE; // Clear the half degree. + remote_state[7] &= 0xF0; // Clear temp bits. + remote_state[7] |= ((uint8_t) TCL112_TEMP_MAX - half_degrees / 2); + + // Calculate & set the checksum for the current internal state of the remote. + // Stored the checksum value in the last byte. + for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) + remote_state[TCL112_STATE_LENGTH - 1] += remote_state[checksum_byte]; + + ESP_LOGV(TAG, "Sending tcl code: %u", remote_state[7]); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + + // Header + data->mark(TCL112_HEADER_MARK); + data->space(TCL112_HEADER_SPACE); + // Data + for (uint8_t i : remote_state) + for (uint8_t j = 0; j < 8; j++) { + data->mark(TCL112_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? TCL112_ONE_SPACE : TCL112_ZERO_SPACE); + } + // Footer + data->mark(TCL112_BIT_MARK); + data->space(TCL112_GAP); + + transmit.perform(); +} + +} // namespace tcl112 +} // namespace esphome diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h new file mode 100644 index 0000000000..0b80dedbef --- /dev/null +++ b/esphome/components/tcl112/tcl112.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/remote_base/remote_base.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace tcl112 { + +class Tcl112Climate : public climate::Climate, public Component { + public: + void setup() override; + void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { + this->transmitter_ = transmitter; + } + void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } + void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Transmit via IR the state of this climate controller. + void transmit_state_(); + + bool supports_cool_{true}; + bool supports_heat_{true}; + + remote_transmitter::RemoteTransmitterComponent *transmitter_; + sensor::Sensor *sensor_{nullptr}; +}; + +} // namespace tcl112 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index bb692d431e..ba8abc3ead 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -895,6 +895,15 @@ remote_transmitter: - pin: 32 carrier_duty_percent: 100% +climate: + - platform: tcl112 + name: TCL112 Climate With Sensor + supports_heat: True + supports_cool: True + sensor: my_sensor + - platform: tcl112 + name: TCL112 Climate + switch: - platform: gpio pin: GPIO25