From 6d77950e6b438528391df791d52945447e658b98 Mon Sep 17 00:00:00 2001 From: Florian Simmer Date: Tue, 1 Jul 2025 20:22:06 +0200 Subject: [PATCH] first try --- components/TCL-AC/__init__.py | 0 components/TCL-AC/climate.py | 293 +++++++++++++++ components/TCL-AC/tcl_climate.h | 640 ++++++++++++++++++++++++++++++++ 3 files changed, 933 insertions(+) create mode 100644 components/TCL-AC/__init__.py create mode 100644 components/TCL-AC/climate.py create mode 100644 components/TCL-AC/tcl_climate.h diff --git a/components/TCL-AC/__init__.py b/components/TCL-AC/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/TCL-AC/climate.py b/components/TCL-AC/climate.py new file mode 100644 index 0000000..b8bb44a --- /dev/null +++ b/components/TCL-AC/climate.py @@ -0,0 +1,293 @@ + +from esphome import automation +import esphome.codegen as cg +from esphome.components import climate, remote_transmitter, sensor, uart +from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode +from esphome.components.remote_base import CONF_TRANSMITTER_ID +import esphome.config_validation as cv +from esphome.const import ( + CONF_AUTOCONF, + CONF_BEEPER, + CONF_CUSTOM_FAN_MODES, + CONF_CUSTOM_PRESETS, + CONF_ID, + CONF_NUM_ATTEMPTS, + CONF_OUTDOOR_TEMPERATURE, + CONF_PERIOD, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, + CONF_SUPPORTED_SWING_MODES, + CONF_TEMPERATURE, + CONF_TIMEOUT, + CONF_USE_FAHRENHEIT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ICON_POWER, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_WATT, +) +from esphome.core import coroutine + +CODEOWNERS = ["@floriansimmer"] +DEPENDENCIES = ["climate", "uart"] +AUTO_LOAD = ["sensor"] +CONF_POWER_USAGE = "power_usage" +CONF_HUMIDITY_SETPOINT = "humidity_setpoint" +tcl_ac_ns = cg.esphome_ns.namespace("tcl").namespace("ac") +AirConditioner = tcl_ac_ns.class_("AirConditioner", climate.Climate, cg.Component) +Capabilities = tcl_ac_ns.namespace("Constants") + + +def templatize(value): + if isinstance(value, cv.Schema): + value = value.schema + ret = {} + for key, val in value.items(): + ret[key] = cv.templatable(val) + return cv.Schema(ret) + + +def register_action(name, type_, schema): + validator = templatize(schema).extend(tcl_ACTION_BASE_SCHEMA) + registerer = automation.register_action(f"tcl_ac.{name}", type_, validator) + + def decorator(func): + async def new_func(config, action_id, template_arg, args): + ac_ = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg) + cg.add(var.set_parent(ac_)) + await coroutine(func)(var, config, args) + return var + + return registerer(new_func) + + return decorator + + +ALLOWED_CLIMATE_MODES = { + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + +ALLOWED_CLIMATE_PRESETS = { + "ECO": ClimatePreset.CLIMATE_PRESET_ECO, + "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, +} + +ALLOWED_CLIMATE_SWING_MODES = { + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, +} + +CUSTOM_FAN_MODES = { + "SILENT": Capabilities.SILENT, + "TURBO": Capabilities.TURBO, +} + +CUSTOM_PRESETS = { + "FREEZE_PROTECTION": Capabilities.FREEZE_PROTECTION, +} + +validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True) +validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True) +validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) +validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) +validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True) + +CONFIG_SCHEMA = cv.All( + climate.climate_schema(AirConditioner) + .extend( + { + cv.Optional(CONF_PERIOD, default="1s"): cv.time_period, + cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period, + cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5), + cv.OnlyWith(CONF_TRANSMITTER_ID, "remote_transmitter"): cv.use_id( + remote_transmitter.RemoteTransmitterComponent + ), + cv.Optional(CONF_BEEPER, default=False): cv.boolean, + cv.Optional(CONF_AUTOCONF, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes), + cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list( + validate_swing_modes + ), + cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets), + cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets), + cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list( + validate_custom_fan_modes + ), + cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + icon=ICON_POWER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.only_with_arduino, +) + +# Actions +FollowMeAction = tcl_ac_ns.class_("FollowMeAction", automation.Action) +DisplayToggleAction = tcl_ac_ns.class_("DisplayToggleAction", automation.Action) +SwingStepAction = tcl_ac_ns.class_("SwingStepAction", automation.Action) +BeeperOnAction = tcl_ac_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = tcl_ac_ns.class_("BeeperOffAction", automation.Action) +PowerOnAction = tcl_ac_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = tcl_ac_ns.class_("PowerOffAction", automation.Action) +PowerToggleAction = tcl_ac_ns.class_("PowerToggleAction", automation.Action) + +tcl_ACTION_BASE_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(AirConditioner), + } +) + +# FollowMe action +tcl_FOLLOW_ME_SCHEMA = cv.Schema( + { + cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.templatable(cv.boolean), + cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean), + } +) + + +@register_action("follow_me", FollowMeAction, tcl_FOLLOW_ME_SCHEMA) +async def follow_me_to_code(var, config, args): + template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_) + cg.add(var.set_beeper(template_)) + template_ = await cg.templatable(config[CONF_USE_FAHRENHEIT], args, cg.bool_) + cg.add(var.set_use_fahrenheit(template_)) + template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_) + cg.add(var.set_temperature(template_)) + + +# Toggle Display action +@register_action( + "display_toggle", + DisplayToggleAction, + cv.Schema({}), +) +async def display_toggle_to_code(var, config, args): + pass + + +# Swing Step action +@register_action( + "swing_step", + SwingStepAction, + cv.Schema({}), +) +async def swing_step_to_code(var, config, args): + pass + + +# Beeper On action +@register_action( + "beeper_on", + BeeperOnAction, + cv.Schema({}), +) +async def beeper_on_to_code(var, config, args): + pass + + +# Beeper Off action +@register_action( + "beeper_off", + BeeperOffAction, + cv.Schema({}), +) +async def beeper_off_to_code(var, config, args): + pass + + +# Power On action +@register_action( + "power_on", + PowerOnAction, + cv.Schema({}), +) +async def power_on_to_code(var, config, args): + pass + + +# Power Off action +@register_action( + "power_off", + PowerOffAction, + cv.Schema({}), +) +async def power_off_to_code(var, config, args): + pass + + +# Power Toggle action +@register_action( + "power_toggle", + PowerToggleAction, + cv.Schema({}), +) +async def power_inv_to_code(var, config, args): + pass + + +async def to_code(config): + var = await climate.new_climate(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds)) + cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds)) + cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS])) + if CONF_TRANSMITTER_ID in config: + cg.add_define("USE_REMOTE_TRANSMITTER") + transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) + cg.add(var.set_transmitter(transmitter_)) + cg.add(var.set_beeper_feedback(config[CONF_BEEPER])) + cg.add(var.set_autoconf(config[CONF_AUTOCONF])) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + if CONF_SUPPORTED_SWING_MODES in config: + cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + if CONF_SUPPORTED_PRESETS in config: + cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) + if CONF_CUSTOM_PRESETS in config: + cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) + if CONF_CUSTOM_FAN_MODES in config: + cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) + if CONF_OUTDOOR_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE]) + cg.add(var.set_outdoor_temperature_sensor(sens)) + if CONF_POWER_USAGE in config: + sens = await sensor.new_sensor(config[CONF_POWER_USAGE]) + cg.add(var.set_power_sensor(sens)) + if CONF_HUMIDITY_SETPOINT in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) + cg.add(var.set_humidity_setpoint_sensor(sens)) diff --git a/components/TCL-AC/tcl_climate.h b/components/TCL-AC/tcl_climate.h new file mode 100644 index 0000000..e8dadf8 --- /dev/null +++ b/components/TCL-AC/tcl_climate.h @@ -0,0 +1,640 @@ +#include "esphome.h" + +class TclClimate : public PollingComponent, public Climate, public UARTDevice { +public: + + TclClimate(UARTComponent *parent) : UARTDevice(parent) {} + TclClimate() : PollingComponent() {} + bool is_changed : 1; + + union get_cmd_resp_t{ + struct { + uint8_t header; + uint8_t byte_1; + uint8_t byte_2; + uint8_t type; + uint8_t len; + uint8_t byte_5; + uint8_t byte_6; + + uint8_t mode : 4; + uint8_t power : 1; + uint8_t disp : 1; + uint8_t eco : 1; + uint8_t turbo : 1; + + uint8_t temp : 4; + uint8_t fan : 3; + uint8_t byte_8_bit_7 : 1; + + uint8_t byte_9; + + uint8_t byte_10_bit_0_4 : 5; + uint8_t hswing : 1; + uint8_t vswing : 1; + uint8_t byte_10_bit_7 : 1; + + uint8_t byte_11; + uint8_t byte_12; + uint8_t byte_13; + uint8_t byte_14; + uint8_t byte_15; + uint8_t byte_16; + uint8_t byte_17; + uint8_t byte_18; + uint8_t byte_19; + + uint8_t byte_20; + uint8_t byte_21; + uint8_t byte_22; + uint8_t byte_23; + uint8_t byte_24; + uint8_t byte_25; + uint8_t byte_26; + uint8_t byte_27; + uint8_t byte_28; + uint8_t byte_29; + + uint8_t byte_30; + uint8_t byte_31; + uint8_t byte_32; + + uint8_t byte_33_bit_0_6 : 7; + uint8_t mute : 1; + + uint8_t byte_34; + uint8_t byte_35; + uint8_t byte_36; + uint8_t byte_37; + uint8_t byte_38; + uint8_t byte_39; + + uint8_t byte_40; + uint8_t byte_41; + uint8_t byte_42; + uint8_t byte_43; + uint8_t byte_44; + uint8_t byte_45; + uint8_t byte_46; + uint8_t byte_47; + uint8_t byte_48; + uint8_t byte_49; + + uint8_t byte_50; + + uint8_t vswing_fix : 3; + uint8_t vswing_mv : 2; + uint8_t byte_51_bit_5_7 : 3; + + uint8_t hswing_fix : 3; + uint8_t hswing_mv : 3; + uint8_t byte_52_bit_6_7 : 2; + + uint8_t byte_53; + uint8_t byte_54; + uint8_t byte_55; + uint8_t byte_56; + uint8_t byte_57; + uint8_t byte_58; + uint8_t byte_59; + + uint8_t xor_sum; + } data; + uint8_t raw[61]; + }; + + union set_cmd_t { + struct { + uint8_t header; + uint8_t byte_1; + uint8_t byte_2; + uint8_t type; + uint8_t len; + uint8_t byte_5; + uint8_t byte_6; + + uint8_t byte_7_bit_0_1 : 2; + uint8_t power : 1; + uint8_t off_timer_en : 1; + uint8_t on_timer_en : 1; + uint8_t beep : 1; + uint8_t disp : 1; + uint8_t eco : 1; + + uint8_t mode : 4; + uint8_t byte_8_bit_4_5 : 2; + uint8_t turbo : 1; + uint8_t mute : 1; + + uint8_t temp : 4; + uint8_t byte_9_bit_4_7 : 4; + + uint8_t fan : 3; + uint8_t vswing : 3; + uint8_t byte_10_bit_6 : 1; + uint8_t byte_10_bit_7 : 1; + + uint8_t byte_11_bit_0_2 : 3; + uint8_t hswing : 1; + uint8_t byte_11_bit_4_7 : 4; + + uint8_t byte_12; + uint8_t byte_13; + + uint8_t byte_14_bit_0_2 : 3; + uint8_t byte_14_bit_3 : 1; + uint8_t byte_14_bit_4 : 1;//uint8_t hswing : 1; + uint8_t half_degree : 1; + uint8_t byte_14_bit_6_7 : 2; + + uint8_t byte_15; + uint8_t byte_16; + uint8_t byte_17; + uint8_t byte_18; + uint8_t byte_19; + + uint8_t byte_20; + uint8_t byte_21; + uint8_t byte_22; + uint8_t byte_23; + uint8_t byte_24; + uint8_t byte_25; + uint8_t byte_26; + uint8_t byte_27; + uint8_t byte_28; + uint8_t byte_29; + + uint8_t byte_30; + uint8_t byte_31; + uint8_t vswing_fix : 3; + uint8_t vswing_mv : 2; + uint8_t byte_32_bit_5_7 : 3; + + uint8_t hswing_fix : 3; + uint8_t hswing_mv : 3; + uint8_t byte_33_bit_6_7 : 2; + + uint8_t xor_sum; + } data; + uint8_t raw[35]; + }; + + bool ready_to_send_set_cmd_flag = false; + + uint8_t set_cmd_base[35] = {0xBB, 0x00, 0x01, 0x03, 0x1D, 0x00, 0x00, 0x64, 0x03, 0xF3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + get_cmd_resp_t m_get_cmd_resp = {0}; + set_cmd_t m_set_cmd = {0}; + + void set_current_temperature (float current_temperature) { + if (this->current_temperature == current_temperature) return; + this->is_changed = true; + this->current_temperature = current_temperature; + } + + void set_custom_fan_mode (const std::string &fan_mode) { + if (this->custom_fan_mode == fan_mode) return; + this->is_changed = true; + this->custom_fan_mode = fan_mode; + } + + void set_mode (esphome::climate::ClimateMode mode) { + if (this->mode == mode) return; + this->is_changed = true; + this->mode = mode; + } + + void set_swing_mode (esphome::climate::ClimateSwingMode swing_mode) { + if (this->swing_mode == swing_mode) return; + this->is_changed = true; + this->swing_mode = swing_mode; + } + + std::string hswing_pos = ""; + std::string vswing_pos = ""; + + void set_hswing_pos (const std::string &hswing_pos) { + if (this->hswing_pos == hswing_pos) return; + id(hswing).publish_state(hswing_pos); + this->hswing_pos = hswing_pos; + } + + void set_vswing_pos (const std::string &vswing_pos) { + if (this->vswing_pos == vswing_pos) return; + id(vswing).publish_state(vswing_pos); + this->vswing_pos = vswing_pos; + } + + void set_target_temperature (float target_temperature) { + if (this->target_temperature == target_temperature) return; + this->is_changed = true; + this->target_temperature = target_temperature; + } + + void build_set_cmd(get_cmd_resp_t * get_cmd_resp) { + memcpy(m_set_cmd.raw, set_cmd_base, sizeof(m_set_cmd.raw)); + + m_set_cmd.data.power = get_cmd_resp->data.power; + m_set_cmd.data.off_timer_en = 0; + m_set_cmd.data.on_timer_en = 0; + m_set_cmd.data.beep = 1; + m_set_cmd.data.disp = 1; + m_set_cmd.data.eco = 0; + + switch (get_cmd_resp->data.mode) { + case 0x01: + m_set_cmd.data.mode = 0x03; + break; + case 0x03: + m_set_cmd.data.mode = 0x02; + break; + case 0x02: + m_set_cmd.data.mode = 0x07; + break; + case 0x04: + m_set_cmd.data.mode = 0x01; + break; + case 0x05: + m_set_cmd.data.mode = 0x08; + break; + } + + m_set_cmd.data.turbo = get_cmd_resp->data.turbo; + m_set_cmd.data.mute = get_cmd_resp->data.mute; + m_set_cmd.data.temp = 15 - get_cmd_resp->data.temp; + + switch (get_cmd_resp->data.fan) { + case 0x00: + m_set_cmd.data.fan = 0x00; + break; + case 0x01: + m_set_cmd.data.fan = 0x02; + break; + case 0x04: + m_set_cmd.data.fan = 0x06; + break; + case 0x02: + m_set_cmd.data.fan = 0x03; + break; + case 0x05: + m_set_cmd.data.fan = 0x07; + break; + case 0x03: + m_set_cmd.data.fan = 0x05; + break; + } + + //m_set_cmd.data.vswing = get_cmd_resp->data.vswing ? 0x07 : 0x00; + //m_set_cmd.data.hswing = get_cmd_resp->data.hswing; + + if (get_cmd_resp->data.vswing_mv) { + m_set_cmd.data.vswing = 0x07; + m_set_cmd.data.vswing_fix = 0; + m_set_cmd.data.vswing_mv = get_cmd_resp->data.vswing_mv; + } else if (get_cmd_resp->data.vswing_fix) { + m_set_cmd.data.vswing = 0; + m_set_cmd.data.vswing_fix = get_cmd_resp->data.vswing_fix; + m_set_cmd.data.vswing_mv = 0; + } + + if (get_cmd_resp->data.hswing_mv) { + m_set_cmd.data.hswing = 0x01; + m_set_cmd.data.hswing_fix = 0; + m_set_cmd.data.hswing_mv = get_cmd_resp->data.hswing_mv; + } else if (get_cmd_resp->data.hswing_fix) { + m_set_cmd.data.hswing = 0; + m_set_cmd.data.hswing_fix = get_cmd_resp->data.hswing_fix; + m_set_cmd.data.hswing_mv = 0; + } + + m_set_cmd.data.half_degree = 0; + + for (int i = 0; i < sizeof(m_set_cmd.raw) - 1; i++) m_set_cmd.raw[sizeof(m_set_cmd.raw) - 1] ^= m_set_cmd.raw[i]; + } + + void setup() override { + // This will be called by App.setup() + set_update_interval(500); + } + + + void control_vertical_swing(const std::string &swing_mode) { + // Implement the vertical swing control logic + ESP_LOGD("TclClimate", "Vertical swing set to: %s", swing_mode.c_str()); + + get_cmd_resp_t get_cmd_resp = {0}; + memcpy(get_cmd_resp.raw, m_get_cmd_resp.raw, sizeof(get_cmd_resp.raw)); + + get_cmd_resp.data.vswing_mv = 0; + get_cmd_resp.data.vswing_fix = 0; + + if (swing_mode == "Move full") get_cmd_resp.data.vswing_mv = 0x01; + else if (swing_mode == "Move upper") get_cmd_resp.data.vswing_mv = 0x02; + else if (swing_mode == "Move lower") get_cmd_resp.data.vswing_mv = 0x03; + else if (swing_mode == "Fix top") get_cmd_resp.data.vswing_fix = 0x01; + else if (swing_mode == "Fix upper") get_cmd_resp.data.vswing_fix = 0x02; + else if (swing_mode == "Fix mid") get_cmd_resp.data.vswing_fix = 0x03; + else if (swing_mode == "Fix lower") get_cmd_resp.data.vswing_fix = 0x04; + else if (swing_mode == "Fix bottom") get_cmd_resp.data.vswing_fix = 0x05; + + if (get_cmd_resp.data.vswing_mv) get_cmd_resp.data.vswing = 0x01; + else get_cmd_resp.data.vswing = 0; + + build_set_cmd(&get_cmd_resp); + ready_to_send_set_cmd_flag = true; +} + + void control_horizontal_swing(const std::string &swing_mode) { + // Implement the vertical swing control logic + ESP_LOGD("TclClimate", "Horizontal swing set to: %s", swing_mode.c_str()); + + get_cmd_resp_t get_cmd_resp = {0}; + memcpy(get_cmd_resp.raw, m_get_cmd_resp.raw, sizeof(get_cmd_resp.raw)); + + get_cmd_resp.data.hswing_mv = 0; + get_cmd_resp.data.hswing_fix = 0; + + if (swing_mode == "Move full") get_cmd_resp.data.hswing_mv = 0x01; + else if (swing_mode == "Move left") get_cmd_resp.data.hswing_mv = 0x02; + else if (swing_mode == "Move mid") get_cmd_resp.data.hswing_mv = 0x03; + else if (swing_mode == "Move right") get_cmd_resp.data.hswing_mv = 0x04; + else if (swing_mode == "Fix left") get_cmd_resp.data.hswing_fix = 0x01; + else if (swing_mode == "Fix mid left") get_cmd_resp.data.hswing_fix = 0x02; + else if (swing_mode == "Fix mid") get_cmd_resp.data.hswing_fix = 0x03; + else if (swing_mode == "Fix mid right") get_cmd_resp.data.hswing_fix = 0x04; + else if (swing_mode == "Fix right") get_cmd_resp.data.hswing_fix = 0x05; + + if (get_cmd_resp.data.vswing_mv) get_cmd_resp.data.hswing = 0x01; + else get_cmd_resp.data.hswing = 0; + + build_set_cmd(&get_cmd_resp); + ready_to_send_set_cmd_flag = true; + } + + void control(const ClimateCall &call) override { + if (call.get_mode().has_value()) { + // User requested mode change + ClimateMode climate_mode = *call.get_mode(); + // Send mode to hardware + + get_cmd_resp_t get_cmd_resp = {0}; + memcpy(get_cmd_resp.raw, m_get_cmd_resp.raw, sizeof(get_cmd_resp.raw)); + + if (climate_mode == climate::CLIMATE_MODE_OFF) { + get_cmd_resp.data.power = 0x00; + } else { + get_cmd_resp.data.power = 0x01; + switch (climate_mode) { + case climate::CLIMATE_MODE_COOL: + get_cmd_resp.data.mode = 0x01; + break; + case climate::CLIMATE_MODE_DRY: + get_cmd_resp.data.mode = 0x03; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + get_cmd_resp.data.mode = 0x02; + break; + case climate::CLIMATE_MODE_HEAT: + case climate::CLIMATE_MODE_HEAT_COOL: + get_cmd_resp.data.mode = 0x04; + break; + case climate::CLIMATE_MODE_AUTO: + get_cmd_resp.data.mode = 0x05; + break; + case CLIMATE_MODE_OFF: + get_cmd_resp.data.power = 0x00; + break; + } + } + + build_set_cmd(&get_cmd_resp); + ready_to_send_set_cmd_flag = true; + + // Publish updated state + // this->mode = mode; + // this->publish_state(); + } + if (call.get_target_temperature().has_value()) { + // User requested target temperature change + float temp = *call.get_target_temperature(); + + get_cmd_resp_t get_cmd_resp = {0}; + memcpy(get_cmd_resp.raw, m_get_cmd_resp.raw, sizeof(get_cmd_resp.raw)); + + get_cmd_resp.data.temp = uint8_t(temp) - 16; + + build_set_cmd(&get_cmd_resp); + ready_to_send_set_cmd_flag = true; + } + if (false) {//call.get_swing_mode().has_value()) { + // User requested target temperature change + ClimateSwingMode swing_mode = *call.get_swing_mode(); + + get_cmd_resp_t get_cmd_resp = {0}; + memcpy(get_cmd_resp.raw, m_get_cmd_resp.raw, sizeof(get_cmd_resp.raw)); + + switch(swing_mode) { + case climate::CLIMATE_SWING_OFF: + get_cmd_resp.data.hswing = 0; + get_cmd_resp.data.vswing = 0; + break; + case climate::CLIMATE_SWING_BOTH: + get_cmd_resp.data.hswing = 1; + get_cmd_resp.data.vswing = 1; + break; + case climate::CLIMATE_SWING_VERTICAL: + get_cmd_resp.data.hswing = 0; + get_cmd_resp.data.vswing = 1; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + get_cmd_resp.data.hswing = 1; + get_cmd_resp.data.vswing = 0; + break; + } + + build_set_cmd(&get_cmd_resp); + ready_to_send_set_cmd_flag = true; + + } + if (call.get_custom_fan_mode().has_value()) { + ESP_LOGI("ads", "ajskndjanjanwnjwa"); + // User requested target temperature change + std::string fan_mode = *call.get_custom_fan_mode(); + + get_cmd_resp_t get_cmd_resp = {0}; + memcpy(get_cmd_resp.raw, m_get_cmd_resp.raw, sizeof(get_cmd_resp.raw)); + + get_cmd_resp.data.turbo = 0x00; + get_cmd_resp.data.mute = 0x00; + if (fan_mode == esphome::to_string("Turbo")) { + get_cmd_resp.data.fan = 0x03; + get_cmd_resp.data.turbo = 0x01; + } else if (fan_mode == esphome::to_string("Mute")) { + get_cmd_resp.data.fan = 0x01; + get_cmd_resp.data.mute = 0x01; + } else if (fan_mode == esphome::to_string("Automatic")) get_cmd_resp.data.fan = 0x00; + else if (fan_mode == esphome::to_string("1")) get_cmd_resp.data.fan = 0x01; + else if (fan_mode == esphome::to_string("2")) get_cmd_resp.data.fan = 0x04; + else if (fan_mode == esphome::to_string("3")) get_cmd_resp.data.fan = 0x02; + else if (fan_mode == esphome::to_string("4")) get_cmd_resp.data.fan = 0x05; + else if (fan_mode == esphome::to_string("5")) get_cmd_resp.data.fan = 0x03; + + build_set_cmd(&get_cmd_resp); + ready_to_send_set_cmd_flag = true; + + } + } + + ClimateTraits traits() override { + // The capabilities of the climate device + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_AUTO}); + traits.set_supported_custom_fan_modes({"Turbo", "Mute", "Automatic", "1", "2", "3", "4", "5"}); + traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL}); + traits.set_visual_min_temperature(16.0); + traits.set_visual_max_temperature(31.0); + traits.set_visual_target_temperature_step(1.0); + return traits; + } + + void update() override { + // This will be called every "update_interval" milliseconds. + uint8_t req_cmd[] = {0xBB, 0x00, 0x01, 0x04, 0x02, 0x01, 0x00, 0xBD}; + + if (ready_to_send_set_cmd_flag) { + ESP_LOGW("TCL", "Sending data"); + ready_to_send_set_cmd_flag = false; + write_array(m_set_cmd.raw, sizeof(m_set_cmd.raw)); + } + else write_array(req_cmd, sizeof(req_cmd)); + } + + int read_data_line(int readch, uint8_t *buffer, int len) + { + static int pos = 0; + static bool wait_len = false; + static int skipch = 0; + + //ESP_LOGI("TclClimate", "%02X", readch); + + if (readch >= 0) { + if (readch == 0xBB && skipch == 0 && !wait_len) { + pos = 0; + skipch = 3; // wait char with len + wait_len = true; + if (pos < len-1) buffer[pos++] = readch; + } else if (skipch == 0 && wait_len) { + if (pos < len-1) buffer[pos++] = readch; + skipch = readch + 1; // +1 control sum + ESP_LOGI("TCL", "len: %d", readch); + wait_len = false; + } else if (skipch > 0) { + if (pos < len-1) buffer[pos++] = readch; + if (--skipch == 0 && !wait_len) return pos; + } + } + // No end of line has been found, so return -1. + return -1; + } + + bool is_valid_xor(uint8_t *buffer, int len) + { + uint8_t xor_byte = 0; + for (int i = 0; i < len - 1; i++) xor_byte ^= buffer[i]; + if (xor_byte == buffer[len - 1]) return true; + else { + ESP_LOGW("TCL", "No valid xor crc %02X (calculated %02X)", buffer[len], xor_byte); + return false; + } + + } + + void print_hex_str(uint8_t *buffer, int len) + { + char str[250] = {0}; + char *pstr = str; + if (len * 2 > sizeof(str)) ESP_LOGE("TCL", "too long byte data"); + + for (int i = 0; i < len; i++) { + pstr += sprintf(pstr, "%02X ", buffer[i]); + } + + ESP_LOGI("TCL", "%s", str); + } + + void loop() override { + const int max_line_length = 100; + static uint8_t buffer[max_line_length]; + + while (available()) { + int len = read_data_line(read(), buffer, max_line_length); + if(len == sizeof(m_get_cmd_resp) && buffer[3] == 0x04) { + memcpy(m_get_cmd_resp.raw, buffer, len); + print_hex_str(buffer, len); + if (is_valid_xor(buffer, len)) { + float curr_temp = (((buffer[17] << 8) | buffer[18]) / 374 - 32) / 1.8; + this->is_changed = false; + + if (m_get_cmd_resp.data.power == 0x00) this->set_mode(climate::CLIMATE_MODE_OFF); + else if (m_get_cmd_resp.data.mode == 0x01) this->set_mode(climate::CLIMATE_MODE_COOL); + else if (m_get_cmd_resp.data.mode == 0x03) this->set_mode(climate::CLIMATE_MODE_DRY); + else if (m_get_cmd_resp.data.mode == 0x02) this->set_mode(climate::CLIMATE_MODE_FAN_ONLY); + else if (m_get_cmd_resp.data.mode == 0x04) this->set_mode(climate::CLIMATE_MODE_HEAT); + else if (m_get_cmd_resp.data.mode == 0x05) this->set_mode(climate::CLIMATE_MODE_AUTO); + + + if (m_get_cmd_resp.data.turbo) this->set_custom_fan_mode(esphome::to_string("Turbo")); + else if (m_get_cmd_resp.data.mute) this->set_custom_fan_mode(esphome::to_string("Mute")); + else if (m_get_cmd_resp.data.fan == 0x00) this->set_custom_fan_mode(esphome::to_string("Automatic")); + else if (m_get_cmd_resp.data.fan == 0x01) this->set_custom_fan_mode(esphome::to_string("1")); + else if (m_get_cmd_resp.data.fan == 0x04) this->set_custom_fan_mode(esphome::to_string("2")); + else if (m_get_cmd_resp.data.fan == 0x02) this->set_custom_fan_mode(esphome::to_string("3")); + else if (m_get_cmd_resp.data.fan == 0x05) this->set_custom_fan_mode(esphome::to_string("4")); + else if (m_get_cmd_resp.data.fan == 0x03) this->set_custom_fan_mode(esphome::to_string("5")); + + + if (m_get_cmd_resp.data.hswing && m_get_cmd_resp.data.vswing) this->set_swing_mode(climate::CLIMATE_SWING_BOTH); + else if (!m_get_cmd_resp.data.hswing && !m_get_cmd_resp.data.vswing) this->set_swing_mode(climate::CLIMATE_SWING_OFF); + else if (m_get_cmd_resp.data.vswing) this->set_swing_mode(climate::CLIMATE_SWING_VERTICAL); + else if (m_get_cmd_resp.data.hswing) this->set_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); + + if (m_get_cmd_resp.data.vswing_mv == 0x01) set_vswing_pos("Move full"); + else if (m_get_cmd_resp.data.vswing_mv == 0x02) set_vswing_pos("Move upper"); + else if (m_get_cmd_resp.data.vswing_mv == 0x03) set_vswing_pos("Move lower"); + else if (m_get_cmd_resp.data.vswing_fix == 0x01) set_vswing_pos("Fix top"); + else if (m_get_cmd_resp.data.vswing_fix == 0x02) set_vswing_pos("Fix upper"); + else if (m_get_cmd_resp.data.vswing_fix == 0x03) set_vswing_pos("Fix mid"); + else if (m_get_cmd_resp.data.vswing_fix == 0x04) set_vswing_pos("Fix lower"); + else if (m_get_cmd_resp.data.vswing_fix == 0x05) set_vswing_pos("Fix bottom"); + else set_vswing_pos("Last position"); + + if (m_get_cmd_resp.data.hswing_mv == 0x01) set_hswing_pos("Move full"); + else if (m_get_cmd_resp.data.hswing_mv == 0x02) set_hswing_pos("Move left"); + else if (m_get_cmd_resp.data.hswing_mv == 0x03) set_hswing_pos("Move mid"); + else if (m_get_cmd_resp.data.hswing_mv == 0x04) set_hswing_pos("Move right"); + else if (m_get_cmd_resp.data.hswing_fix == 0x01) set_hswing_pos("Fix left"); + else if (m_get_cmd_resp.data.hswing_fix == 0x02) set_hswing_pos("Fix mid left"); + else if (m_get_cmd_resp.data.hswing_fix == 0x03) set_hswing_pos("Fix mid"); + else if (m_get_cmd_resp.data.hswing_fix == 0x04) set_hswing_pos("Fix mid right"); + else if (m_get_cmd_resp.data.hswing_fix == 0x05) set_hswing_pos("Fix right"); + else set_hswing_pos("Last position"); + + ESP_LOGI("TCL", "fan %02X", m_get_cmd_resp.data.fan); + ESP_LOGI("TCL", "mode %02X", m_get_cmd_resp.data.mode); + this->set_target_temperature(float(m_get_cmd_resp.data.temp + 16)); + this->set_current_temperature(curr_temp); + if (this->is_changed) + this->publish_state(); + } + //publish_state(buffer); + } + } + + //this->target_temperature = 20.0; + //this->publish_state(); + // if(readline(read(), buffer, max_line_length) > 0) { + // publish_state(buffer); + // } + // } + } +};