From 8292024306431f3d7c8d5d4b22e2015d523b4885 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Fri, 18 Oct 2019 04:22:55 -0300 Subject: [PATCH] add tcl112 receiver (#762) --- esphome/components/tcl112/climate.py | 9 +- esphome/components/tcl112/tcl112.cpp | 131 ++++++++++++++++----------- esphome/components/tcl112/tcl112.h | 36 ++------ 3 files changed, 96 insertions(+), 80 deletions(-) diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 50fef7b125..d21300d946 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -1,20 +1,22 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, sensor +from esphome.components import climate, remote_transmitter, remote_receiver, sensor from esphome.const import CONF_ID, CONF_SENSOR -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ['sensor', 'climate_ir'] tcl112_ns = cg.esphome_ns.namespace('tcl112') Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate.Climate, cg.Component) CONF_TRANSMITTER_ID = 'transmitter_id' +CONF_RECEIVER_ID = 'receiver_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_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), 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), @@ -31,6 +33,9 @@ def to_code(config): if CONF_SENSOR in config: sens = yield cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_RECEIVER_ID in config: + receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) + cg.add(receiver.register_listener(var)) 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 index cbe2f53402..2907ae1743 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -18,62 +18,15 @@ 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_MARK = 3100; 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 uint16_t TCL112_ONE_SPACE = 1100; +const uint16_t TCL112_ZERO_SPACE = 350; 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_() { +void Tcl112Climate::transmit_state() { uint8_t remote_state[TCL112_STATE_LENGTH] = {0}; // A known good state. (On, Cool, 24C) @@ -124,7 +77,10 @@ void Tcl112Climate::transmit_state_() { 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]); + ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0], + remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], + remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12], + remote_state[13]); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); @@ -148,5 +104,76 @@ void Tcl112Climate::transmit_state_() { transmit.perform(); } +bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(TCL112_HEADER_MARK, TCL112_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t remote_state[TCL112_STATE_LENGTH] = {0}; + // Read all bytes. + for (int i = 0; i < TCL112_STATE_LENGTH; i++) { + // Read bit + for (int j = 0; j < 8; j++) { + if (data.expect_item(TCL112_BIT_MARK, TCL112_ONE_SPACE)) + remote_state[i] |= 1 << j; + else if (!data.expect_item(TCL112_BIT_MARK, TCL112_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + } + // Validate footer + if (!data.expect_mark(TCL112_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + uint8_t checksum = 0; + // 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++) + checksum += remote_state[checksum_byte]; + if (checksum != remote_state[TCL112_STATE_LENGTH - 1]) { + ESP_LOGV(TAG, "Checksum fail"); + return false; + } + + ESP_LOGV(TAG, "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", + remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], + remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], + remote_state[12], remote_state[13]); + + // two first bytes are constant + if (remote_state[0] != 0x23 || remote_state[1] != 0xCB) + return false; + + if ((remote_state[5] & TCL112_POWER_MASK) == 0) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + auto mode = remote_state[6] & 0x0F; + switch (mode) { + case TCL112_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case TCL112_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case TCL112_DRY: + case TCL112_FAN: + case TCL112_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + } + } + auto temp = TCL112_TEMP_MAX - remote_state[7]; + if (remote_state[12] & TCL112_HALF_DEGREE) + temp += .5f; + this->target_temperature = temp; + this->publish_state(); + return true; +} + } // namespace tcl112 } // namespace esphome diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h index 0b80dedbef..7c5257a0f3 100644 --- a/esphome/components/tcl112/tcl112.h +++ b/esphome/components/tcl112/tcl112.h @@ -1,39 +1,23 @@ #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" +#include "esphome/components/climate_ir/climate_ir.h" namespace esphome { namespace tcl112 { -class Tcl112Climate : public climate::Climate, public Component { +// Temperature +const float TCL112_TEMP_MAX = 31.0; +const float TCL112_TEMP_MIN = 16.0; + +class Tcl112Climate : public climate::ClimateIR { 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; } + Tcl112Climate() : climate::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f) {} 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}; + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; }; } // namespace tcl112