commit fe3f68e4b7ecbd0a3e2d29804c9b9cc1370f3342 Author: Михаил Соловьев Date: Sat Mar 30 15:30:55 2024 +0300 Initial commit diff --git a/components/tclac/__init__.py b/components/tclac/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/tclac/automation.h b/components/tclac/automation.h new file mode 100644 index 0000000..d55547c --- /dev/null +++ b/components/tclac/automation.h @@ -0,0 +1,114 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "tclac.h" + +namespace esphome { +namespace tclac { + +// Шаблон действия: изменение вертикальной фиксации заслонки +template class VerticalAirflowAction : public Action { + public: + VerticalAirflowAction(tclacClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(AirflowVerticalDirection, direction) + void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: изменение горизонтальной фиксации заслонок +template class HorizontalAirflowAction : public Action { + public: + HorizontalAirflowAction(tclacClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction) + void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: изменение режима вертикального качания заслонки +template class VerticalSwingDirectionAction : public Action { + public: + VerticalSwingDirectionAction(tclacClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(VerticalSwingDirection, direction) + void play(Ts... x) { this->parent_->set_vertical_swing_direction(this->direction_.value(x...)); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: изменение режима горизонтального качания заслонок +template class HorizontalSwingDirectionAction : public Action { + public: + HorizontalSwingDirectionAction(tclacClimate *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(HorizontalSwingDirection, direction) + void play(Ts... x) { this->parent_->set_horizontal_swing_direction(this->direction_.value(x...)); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: включение дисплея +template class DisplayOnAction : public Action { + public: + DisplayOnAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(true); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: выключение дисплея +template class DisplayOffAction : public Action { + public: + DisplayOffAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_display_state(false); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: включение пищалки +template class BeeperOnAction : public Action { + public: + BeeperOnAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(true); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: выклюение пищалки +template class BeeperOffAction : public Action { + public: + BeeperOffAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_beeper_state(false); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: включение индикатора модуля +template class ModuleDisplayOnAction : public Action { + public: + ModuleDisplayOnAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_module_display_state(true); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: выключение индикатора модуля +template class ModuleDisplayOffAction : public Action { + public: + ModuleDisplayOffAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_module_display_state(false); } + + protected: + tclacClimate *parent_; +}; + +} // namespace tclac +} // namespace esphome diff --git a/components/tclac/climate.py b/components/tclac/climate.py new file mode 100644 index 0000000..75f7bde --- /dev/null +++ b/components/tclac/climate.py @@ -0,0 +1,265 @@ +from esphome import automation, pins +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, uart +from esphome.const import ( + CONF_ID, + CONF_LEVEL, + CONF_BEEPER, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, + CONF_SUPPORTED_SWING_MODES, +) + +from esphome.components.climate import ( + ClimateMode, + ClimatePreset, + ClimateSwingMode, + CONF_CURRENT_TEMPERATURE, +) + +AUTO_LOAD = ["climate"] +CODEOWNERS = ["@I-am-nightingale"] +DEPENDENCIES = ["climate", "uart"] +CONF_RX_LED = "rx_led" +CONF_TX_LED = "tx_led" +CONF_DISPLAY = "show_display" +CONF_USE_OLED = "use_oled_display" +CONF_MODULE_DISPLAY = "show_module_display" +CONF_VERTICAL_AIRFLOW = "vertical_airflow" +CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" +CONF_VERTICAL_SWING_MODE = "vertical_swing_mode" +CONF_HORIZONTAL_SWING_MODE = "horizontal_swing_mode" + +tclac_ns = cg.esphome_ns.namespace("tclac") +tclacClimate = tclac_ns.class_("tclacClimate", uart.UARTDevice, climate.Climate, cg.PollingComponent) + +AirflowVerticalDirection = tclac_ns.enum("AirflowVerticalDirection", True) +AIRFLOW_VERTICAL_DIRECTION_OPTIONS = { + "LAST": AirflowVerticalDirection.LAST, + "MAX_UP": AirflowVerticalDirection.MAX_UP, + "UP": AirflowVerticalDirection.UP, + "CENTER": AirflowVerticalDirection.CENTER, + "DOWN": AirflowVerticalDirection.DOWN, + "MAX_DOWN": AirflowVerticalDirection.MAX_DOWN, +} + +AirflowHorizontalDirection = tclac_ns.enum("AirflowHorizontalDirection", True) +AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = { + "LAST": AirflowHorizontalDirection.LAST, + "MAX_LEFT": AirflowHorizontalDirection.MAX_LEFT, + "LEFT": AirflowHorizontalDirection.LEFT, + "CENTER": AirflowHorizontalDirection.CENTER, + "RIGHT": AirflowHorizontalDirection.RIGHT, + "MAX_RIGHT": AirflowHorizontalDirection.MAX_RIGHT, +} + +VerticalSwingDirection = tclac_ns.enum("VerticalSwingDirection", True) +VERTICAL_SWING_DIRECTION_OPTIONS = { + "UP_DOWN": VerticalSwingDirection.UPDOWN, + "UPSIDE": VerticalSwingDirection.UPSIDE, + "DOWNSIDE": VerticalSwingDirection.DOWNSIDE, +} + +HorizontalSwingDirection = tclac_ns.enum("HorizontalSwingDirection", True) +HORIZONTAL_SWING_DIRECTION_OPTIONS = { + "LEFT_RIGHT": HorizontalSwingDirection.LEFT_RIGHT, + "LEFTSIDE": HorizontalSwingDirection.LEFTSIDE, + "CENTER": HorizontalSwingDirection.CENTER, + "RIGHTSIDE": HorizontalSwingDirection.RIGHTSIDE, +} + +SUPPORTED_SWING_MODES_OPTIONS = { + "OFF": ClimateSwingMode.CLIMATE_SWING_OFF, + "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, + "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, + "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH, +} + +SUPPORTED_CLIMATE_MODES_OPTIONS = { + "OFF": ClimateMode.CLIMATE_MODE_OFF, # always available + "AUTO": ClimateMode.CLIMATE_MODE_AUTO, # always available + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + +# Упрощаем себе житзнь и даем спискам названия покороче +validate_presets = cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True) + +CONFIG_SCHEMA = cv.All( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(tclacClimate), + cv.Optional(CONF_BEEPER, default=True): cv.boolean, + cv.Optional(CONF_DISPLAY, default=True): cv.boolean, + cv.Optional(CONF_RX_LED): pins.gpio_output_pin_schema, + cv.Optional(CONF_TX_LED): pins.gpio_output_pin_schema, + cv.Optional(CONF_MODULE_DISPLAY, default=True): cv.boolean, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)), + cv.Optional(CONF_SUPPORTED_SWING_MODES,default=["OFF","VERTICAL","HORIZONTAL","BOTH",],): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +BeeperOnAction = tclac_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = tclac_ns.class_("BeeperOffAction", automation.Action) +DisplayOnAction = tclac_ns.class_("DisplayOnAction", automation.Action) +DisplayOffAction = tclac_ns.class_("DisplayOffAction", automation.Action) +ModuleDisplayOnAction = tclac_ns.class_("ModuleDisplayOnAction", automation.Action) +ModuleDisplayOffAction = tclac_ns.class_("ModuleDisplayOffAction", automation.Action) +VerticalAirflowAction = tclac_ns.class_("VerticalAirflowAction", automation.Action) +HorizontalAirflowAction = tclac_ns.class_("HorizontalAirflowAction", automation.Action) +VerticalSwingDirectionAction = tclac_ns.class_("VerticalSwingDirectionAction", automation.Action) +HorizontalSwingDirectionAction = tclac_ns.class_("HorizontalSwingDirectionAction", automation.Action) + +# TCLAC_ACTION_BASE_SCHEMA = cv.Schema( +TCLAC_ACTION_BASE_SCHEMA = automation.maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(tclacClimate),}) + +@automation.register_action( + "climate.tclac.display_on", DisplayOnAction, cv.Schema +) +@automation.register_action( + "climate.tclac.display_off", DisplayOffAction, cv.Schema +) +async def display_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.tclac.beeper_on", BeeperOnAction, cv.Schema +) +@automation.register_action( + "climate.tclac.beeper_off", BeeperOffAction, cv.Schema +) +async def beeper_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +@automation.register_action( + "climate.tclac.module_display_on", ModuleDisplayOnAction, cv.Schema +) +@automation.register_action( + "climate.tclac.module_display_off", ModuleDisplayOffAction, cv.Schema +) +async def module_display_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + return var + + +# Set vertical airflow direction action +@automation.register_action( + "climate.tclac.set_vertical_airflow", + VerticalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(tclacClimate), + cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable(cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True)), + } + ), +) +async def tclac_set_vertical_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable( + config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection + ) + cg.add(var.set_direction(template_)) + return var + + +# Set horizontal airflow direction action +@automation.register_action( + "climate.tclac.set_horizontal_airflow", + HorizontalAirflowAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(tclacClimate), + cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable(cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True)), + } + ), +) +async def tclac_set_horizontal_airflow_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection) + cg.add(var.set_direction(template_)) + return var + + +# Регистрация события установки вертикального качания шторки +@automation.register_action( + "climate.tclac.set_vertical_swing_direction", + VerticalSwingDirectionAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(tclacClimate), + cv.Required(CONF_VERTICAL_SWING_MODE): cv.templatable(cv.enum(VERTICAL_SWING_DIRECTION_OPTIONS, upper=True)), + } + ), +) +async def tclac_set_vertical_swing_direction_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_VERTICAL_SWING_MODE], args, VerticalSwingDirection) + cg.add(var.set_swing_direction(template_)) + return var + + +# Регистрация события установки горизонтального качания шторок +@automation.register_action( + "climate.tclac.set_horizontal_swing_direction", + HorizontalSwingDirectionAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(tclacClimate), + cv.Required(CONF_HORIZONTAL_SWING_MODE): cv.templatable(cv.enum(HORIZONTAL_SWING_DIRECTION_OPTIONS, upper=True)), + } + ), +) +async def tclac_set_horizontal_swing_direction_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_HORIZONTAL_SWING_MODE], args, HorizontalSwingDirection) + cg.add(var.set_swing_direction(template_)) + return var + + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + yield climate.register_climate(var, config) + + 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_BEEPER in config: + cg.add(var.set_beeper_state(config[CONF_BEEPER])) + if CONF_DISPLAY in config: + cg.add(var.set_display_state(config[CONF_DISPLAY])) + if CONF_MODULE_DISPLAY in config: + cg.add(var.set_module_display_state(config[CONF_MODULE_DISPLAY])) + if CONF_USE_OLED in config: + cg.add_define("USE_OLED_DISPLAY") + if CONF_RX_LED in config: + cg.add_define("CONF_RX_LED") + rx_led_pin = yield cg.gpio_pin_expression(config[CONF_RX_LED]) + cg.add(var.set_rx_led_pin(rx_led_pin)) + if CONF_TX_LED in config: + cg.add_define("CONF_TX_LED") + tx_led_pin = yield cg.gpio_pin_expression(config[CONF_TX_LED]) + cg.add(var.set_tx_led_pin(tx_led_pin)) \ No newline at end of file diff --git a/components/tclac/tclac.cpp b/components/tclac/tclac.cpp new file mode 100644 index 0000000..2573b3a --- /dev/null +++ b/components/tclac/tclac.cpp @@ -0,0 +1,660 @@ +#include "esphome.h" +#include "esphome/core/defines.h" +#include "tclac.h" + +namespace esphome{ +namespace tclac{ + + +ClimateTraits tclacClimate::traits() { + auto traits = climate::ClimateTraits(); + + traits.set_supports_action(false); + + traits.set_supported_modes( + { + climate::CLIMATE_MODE_AUTO, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_FAN_ONLY, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_OFF + }); + + traits.set_supported_fan_modes( + { + climate::CLIMATE_FAN_AUTO, // auto + climate::CLIMATE_FAN_QUIET, // silent + climate::CLIMATE_FAN_LOW, // | + climate::CLIMATE_FAN_MIDDLE, // || + climate::CLIMATE_FAN_MEDIUM, // ||| + climate::CLIMATE_FAN_HIGH, // |||| + climate::CLIMATE_FAN_FOCUS, // ||||| + climate::CLIMATE_FAN_DIFFUSE // POWER [7] + }); + + //traits.set_supported_swing_modes({climate::CLIMATE_SWING_OFF,climate::CLIMATE_SWING_BOTH,climate::CLIMATE_SWING_VERTICAL,climate::CLIMATE_SWING_HORIZONTAL}); + traits.set_supported_swing_modes(this->supported_swing_modes_); + + if (!traits.get_supported_swing_modes().empty()) + traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF); + + traits.set_visual_min_temperature(MIN_SET_TEMPERATURE); + traits.set_visual_max_temperature(MAX_SET_TEMPERATURE); + traits.set_visual_temperature_step(STEP_TEMPERATURE); + traits.set_supports_current_temperature(true); + traits.set_supports_two_point_target_temperature(false); + + return traits; +} + + +void tclacClimate::setup() { + +// Serial.begin(9600); +// ESP_LOGD("TCL" , "Started Serial"); +#ifdef CONF_RX_LED + this->rx_led_pin_->setup(); + this->rx_led_pin_->digital_write(false); +#endif +#ifdef CONF_TX_LED + this->tx_led_pin_->setup(); + this->tx_led_pin_->digital_write(false); +#endif +} + +void tclacClimate::loop() { + // Если в буфере UART что-то есть, то читаем это что-то + if (esphome::uart::UARTDevice::available() > 0) { + dataShow(0, true); + dataRX[0] = esphome::uart::UARTDevice::read(); + // Если принятый байт- не заголовок (0xBB), то просто покидаем цикл + if (dataRX[0] != 0xBB) { + ESP_LOGD("TCL", "Wrong byte"); + dataShow(0,0); + return; + } + // А вот если совпал заголовок (0xBB), то начинаем чтение по цепочке еще 4 байт + delay(5); + dataRX[1] = esphome::uart::UARTDevice::read(); + delay(5); + dataRX[2] = esphome::uart::UARTDevice::read(); + delay(5); + dataRX[3] = esphome::uart::UARTDevice::read(); + delay(5); + dataRX[4] = esphome::uart::UARTDevice::read(); + + auto raw = getHex(dataRX, 5); + ESP_LOGD("TCL", "first 5 byte : %s ", raw.c_str()); + + //Из первых 5 байт нам нужен пятый- он содержит длину сообщения + esphome::uart::UARTDevice::read_array(dataRX+5, dataRX[4]+1); + + byte check = getChecksum(dataRX, sizeof(dataRX)); + + raw = getHex(dataRX, sizeof(dataRX)); + ESP_LOGD("TCL", "RX full : %s ", raw.c_str()); + // Проверяем контрольную сумму + if (check != dataRX[60]) { + ESP_LOGD("TCL", "Invalid checksum %x", check); + tclacClimate::dataShow(0,0); + return; + } else { + ESP_LOGD("TCL", "checksum OK %x", check); + } + tclacClimate::dataShow(0,0); + // Прочитав все из буфера приступаем к разбору данных + tclacClimate::readData(); + } +} + +void tclacClimate::update() { + + tclacClimate::dataShow(1,1); + //Serial.write(poll, sizeof(poll)); + this->esphome::uart::UARTDevice::write_array(poll, sizeof(poll)); + auto raw = tclacClimate::getHex(poll, sizeof(poll)); + ESP_LOGD("TCL", "chek status sended"); + tclacClimate::dataShow(1,0); +} + +void tclacClimate::readData() { + + current_temperature = float((( (dataRX[17] << 8) | dataRX[18] ) / 374 - 32)/1.8); + target_temperature = (dataRX[FAN_SPEED_POS] & SET_TEMP_MASK) + 16; + + ESP_LOGD("TCL", "TEMP: %f ", current_temperature); + + if (dataRX[MODE_POS] & ( 1 << 4)) { + //Если кондиционер включен, то разбираем данные для отображения + uint8_t modeswitch = MODE_MASK & dataRX[MODE_POS]; + uint8_t fanspeedswitch = FAN_SPEED_MASK & dataRX[FAN_SPEED_POS]; + uint8_t swingmodeswitch = SWING_MODE_MASK & dataRX[SWING_POS]; + + switch (modeswitch) { + case MODE_AUTO: + mode = climate::CLIMATE_MODE_AUTO; + break; + case MODE_COOL: + mode = climate::CLIMATE_MODE_COOL; + break; + case MODE_DRY: + mode = climate::CLIMATE_MODE_DRY; + break; + case MODE_FAN_ONLY: + mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case MODE_HEAT: + mode = climate::CLIMATE_MODE_HEAT; + break; + default: + mode = climate::CLIMATE_MODE_AUTO; + } + + if ( dataRX[FAN_QUIET_POS] & FAN_QUIET) { + fan_mode = climate::CLIMATE_FAN_QUIET; + } else if (dataRX[MODE_POS] & FAN_DIFFUSE){ + fan_mode = climate::CLIMATE_FAN_DIFFUSE; + } else { + switch (fanspeedswitch) { + case FAN_AUTO: + fan_mode = climate::CLIMATE_FAN_AUTO; + break; + case FAN_LOW: + fan_mode = climate::CLIMATE_FAN_LOW; + break; + case FAN_MIDDLE: + fan_mode = climate::CLIMATE_FAN_MIDDLE; + break; + case FAN_MEDIUM: + fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case FAN_HIGH: + fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case FAN_FOCUS: + fan_mode = climate::CLIMATE_FAN_FOCUS; + break; + default: + fan_mode = climate::CLIMATE_FAN_AUTO; + } + } + + switch (swingmodeswitch) { + case SWING_OFF: + swing_mode = climate::CLIMATE_SWING_OFF; + break; + case SWING_HORIZONTAL: + swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + break; + case SWING_VERTICAL: + swing_mode = climate::CLIMATE_SWING_VERTICAL; + break; + case SWING_BOTH: + swing_mode = climate::CLIMATE_SWING_BOTH; + break; + } + } else { + // Если кондиционер выключен, то все режимы показываются, как выключенные + mode = climate::CLIMATE_MODE_OFF; + fan_mode = climate::CLIMATE_FAN_OFF; + swing_mode = climate::CLIMATE_SWING_OFF; + } + // Публикуем данные + this->publish_state(); + } + +// Climate control +void tclacClimate::control(const ClimateCall &call) { + + uint8_t switchvar = 0; + + dataTX[7] = 0b00000000;//eco,display,beep,ontimerenable, offtimerenable,power,0,0 + dataTX[8] = 0b00000000;//mute,0,turbo,health,mode(4) 0=cool 1=fan 2=dry 3=heat 4=auto + dataTX[9] = 0b00000000;//[9] = 0,0,0,0,temp(4) 31 - value + dataTX[10] = 0b00000000;//[10] = 0,timerindicator,swingv(3),fan(3) 0=auto 1=low 2=med 3=high + // {0,2,3,5,0}; + dataTX[11] = 0b00000000; + dataTX[32] = 0b00000000; + dataTX[33] = 0b00000000; + + if (call.get_mode().has_value()){ + switchvar = call.get_mode().value(); + } else { + switchvar = mode; + } + + // Включаем или отключаем пищалку в зависимости от переключателя в настройках + if (beeper_status_){ + ESP_LOGD("TCL", "Beep mode ON"); + dataTX[7] += 0b00100000; + } else { + ESP_LOGD("TCL", "Beep mode OFF"); + dataTX[7] += 0b00000000; + } + + // Включаем или отключаем дисплей на кондиционере в зависимости от переключателя в настройках + // Включаем дисплей только если кондиционер в одном из рабочих режимов + + // ВНИМАНИЕ! При выключении дисплея кондиционер сам принудительно переходит в автоматический режим! + + if ((display_status_) && (switchvar != climate::CLIMATE_MODE_OFF)){ + ESP_LOGD("TCL", "Dispaly turn ON"); + dataTX[7] += 0b01000000; + } else { + ESP_LOGD("TCL", "Dispaly turn OFF"); + dataTX[7] += 0b00000000; + } + + // Настраиваем режим работы кондиционера + switch (switchvar) { + case climate::CLIMATE_MODE_OFF: + dataTX[7] += 0b00000000; + dataTX[8] += 0b00000000; + break; + case climate::CLIMATE_MODE_AUTO: + dataTX[7] += 0b00000100; + dataTX[8] += 0b00001000; + break; + case climate::CLIMATE_MODE_COOL: + dataTX[7] += 0b00000100; + dataTX[8] += 0b00000011; + break; + case climate::CLIMATE_MODE_DRY: + dataTX[7] += 0b00000100; + dataTX[8] += 0b00000010; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + dataTX[7] += 0b00000100; + dataTX[8] += 0b00000111; + break; + case climate::CLIMATE_MODE_HEAT: + dataTX[7] += 0b00000100; + dataTX[8] += 0b00000001; + break; + } + + // Настраиваем режим вентилятора + if (call.get_fan_mode().has_value()){ + switchvar = call.get_fan_mode().value(); + switch(switchvar) { + case climate::CLIMATE_FAN_AUTO: + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000000; + break; + case climate::CLIMATE_FAN_QUIET: + dataTX[8] += 0b10000000; + dataTX[10] += 0b00000000; + break; + case climate::CLIMATE_FAN_LOW: + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000001; + break; + case climate::CLIMATE_FAN_MIDDLE: + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000110; + break; + case climate::CLIMATE_FAN_MEDIUM: + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000011; + break; + case climate::CLIMATE_FAN_HIGH: + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000111; + break; + case climate::CLIMATE_FAN_FOCUS: + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000101; + break; + case climate::CLIMATE_FAN_DIFFUSE: + dataTX[8] += 0b01000000; + dataTX[10] += 0b00000000; + break; + } + } else { + if(fan_mode == climate::CLIMATE_FAN_AUTO){ + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000000; + } else if(fan_mode == climate::CLIMATE_FAN_QUIET){ + dataTX[8] += 0b10000000; + dataTX[10] += 0b00000000; + } else if(fan_mode == climate::CLIMATE_FAN_LOW){ + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000001; + } else if(fan_mode == climate::CLIMATE_FAN_MIDDLE){ + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000110; + } else if(fan_mode == climate::CLIMATE_FAN_MEDIUM){ + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000011; + } else if(fan_mode == climate::CLIMATE_FAN_HIGH){ + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000111; + } else if(fan_mode == climate::CLIMATE_FAN_FOCUS){ + dataTX[8] += 0b00000000; + dataTX[10] += 0b00000101; + } else if(fan_mode == climate::CLIMATE_FAN_DIFFUSE){ + dataTX[8] += 0b01000000; + dataTX[10] += 0b00000000; + } + } + + //Режим заслонок + // Вертикальная заслонка + // Качание вертикальной заслонки [10 байт, маска 00111000]: + // 000 - Качание отключено, заслонка в последней позиции или в фиксации + // 111 - Качание включено в выбранном режиме + // Режим качания вертикальной заслонки (режим фиксации заслонки роли не играет, если качание включено) [32 байт, маска 00011000]: + // 01 - качание сверху вниз, ПО УМОЛЧАНИЮ + // 10 - качание в верхней половине + // 11 - качание в нижней половине + // Режим фиксации заслонки (режим качания заслонки роли не играет, если качание выключено) [32 байт, маска 00000111]: + // 000 - нет фиксации, ПО УМОЛЧАНИЮ + // 001 - фиксация вверху + // 010 - фиксация между верхом и серединой + // 011 - фиксация в середине + // 100 - фиксация между серединой и низом + // 101 - фиксация внизу + // Горизонтальные заслонки + // Качание горизонтальных заслонок [11 байт, маска 00001000]: + // 0 - Качание отключено, заслонки в последней позиции или в фиксации + // 1 - Качание включено в выбранном режиме + // Режим качания горизонтальных заслонок (режим фиксации заслонок роли не играет, если качание включено) [33 байт, маска 00111000]: + // 001 - качание слева направо, ПО УМОЛЧАНИЮ + // 010 - качание слева + // 011 - качание по середине + // 100 - качание справа + // Режим фиксации горизонтальных заслонок (режим качания заслонок роли не играет, если качание выключено) [33 байт, маска 00000111]: + // 000 - нет фиксации, ПО УМОЛЧАНИЮ + // 001 - фиксация слева + // 010 - фиксация между левой стороной и серединой + // 011 - фиксация в середине + // 100 - фиксация между серединой и правой стороной + // 101 - фиксация справа + + + //Запрашиваем данные из переключателя режимов качания заслонок + if (call.get_swing_mode().has_value()){ + switchvar = call.get_swing_mode().value(); + } else { + // А если в переключателе пусто- заполняем значением из последнего опроса состояния. Типа, ничего не поменялось. + switchvar = swing_mode; + } + + switch(switchvar) { + case climate::CLIMATE_SWING_OFF: + dataTX[10] += 0b00000000; + dataTX[11] += 0b00000000; + break; + case climate::CLIMATE_SWING_VERTICAL: + dataTX[10] += 0b00111000; + dataTX[11] += 0b00000000; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + dataTX[10] += 0b00000000; + dataTX[11] += 0b00001000; + break; + case climate::CLIMATE_SWING_BOTH: + dataTX[10] += 0b00111000; + dataTX[11] += 0b00001000; + break; + } + //Выбираем режим для качания вертикальной заслонки + switch(vertical_swing_direction_) { + case VerticalSwingDirection::UP_DOWN: + dataTX[32] += 0b00001000; + ESP_LOGD("TCL", "Vertical swing: up-down"); + break; + case VerticalSwingDirection::UPSIDE: + dataTX[32] += 0b00010000; + ESP_LOGD("TCL", "Vertical swing: upper"); + break; + case VerticalSwingDirection::DOWNSIDE: + dataTX[32] += 0b00011000; + ESP_LOGD("TCL", "Vertical swing: downer"); + break; + } + //Выбираем режим для качания горизонтальных заслонок + switch(horizontal_swing_direction_) { + case HorizontalSwingDirection::LEFT_RIGHT: + dataTX[33] += 0b00001000; + ESP_LOGD("TCL", "Horizontal swing: left-right"); + break; + case HorizontalSwingDirection::LEFTSIDE: + dataTX[33] += 0b00010000; + ESP_LOGD("TCL", "Horizontal swing: lefter"); + break; + case HorizontalSwingDirection::CENTER: + dataTX[33] += 0b00011000; + ESP_LOGD("TCL", "Horizontal swing: center"); + break; + case HorizontalSwingDirection::RIGHTSIDE: + dataTX[33] += 0b00100000; + ESP_LOGD("TCL", "Horizontal swing: righter"); + break; + } + //Выбираем положение фиксации вертикальной заслонки + switch(this->vertical_direction_) { + case AirflowVerticalDirection::LAST: + dataTX[32] += 0b00000000; + ESP_LOGD("TCL", "Vertical fix: last position"); + break; + case AirflowVerticalDirection::MAX_UP: + dataTX[32] += 0b00000001; + ESP_LOGD("TCL", "Vertical fix: up"); + break; + case AirflowVerticalDirection::UP: + dataTX[32] += 0b00000010; + ESP_LOGD("TCL", "Vertical fix: upper"); + break; + case AirflowVerticalDirection::CENTER: + dataTX[32] += 0b00000011; + ESP_LOGD("TCL", "Vertical fix: center"); + break; + case AirflowVerticalDirection::DOWN: + dataTX[32] += 0b00000100; + ESP_LOGD("TCL", "Vertical fix: downer"); + break; + case AirflowVerticalDirection::MAX_DOWN: + dataTX[32] += 0b00000101; + ESP_LOGD("TCL", "Vertical fix: down"); + break; + } + //Выбираем положение фиксации горизонтальных заслонок + switch(this->horizontal_direction_) { + case AirflowHorizontalDirection::LAST: + dataTX[33] += 0b00000000; + ESP_LOGD("TCL", "Horizontal fix: last position"); + break; + case AirflowHorizontalDirection::MAX_LEFT: + dataTX[33] += 0b00000001; + ESP_LOGD("TCL", "Horizontal fix: left"); + break; + case AirflowHorizontalDirection::LEFT: + dataTX[33] += 0b00000010; + ESP_LOGD("TCL", "Horizontal fix: lefter"); + break; + case AirflowHorizontalDirection::CENTER: + dataTX[33] += 0b00000011; + ESP_LOGD("TCL", "Horizontal fix: center"); + break; + case AirflowHorizontalDirection::RIGHT: + dataTX[33] += 0b00000100; + ESP_LOGD("TCL", "Horizontal fix: righter"); + break; + case AirflowHorizontalDirection::MAX_RIGHT: + dataTX[33] += 0b00000101; + ESP_LOGD("TCL", "Horizontal fix: right"); + break; + } + + // Расчет и установка температуры + if (call.get_target_temperature().has_value()) { + dataTX[9] = 31-(int)call.get_target_temperature().value(); //0,0,0,0, temp(4) + } else { + dataTX[9] = 31-(int)target_temperature; + } + + //Собираем массив байт для отправки в кондиционер + dataTX[0] = 0xBB; //стартовый байт заголовка + dataTX[1] = 0x00; //стартовый байт заголовка + dataTX[2] = 0x01; //стартовый байт заголовка + dataTX[3] = 0x03; //0x03 - управление, 0x04 - опрос + dataTX[4] = 0x20; //0x20 - управление, 0x19 - опрос + dataTX[5] = 0x03; //?? + dataTX[6] = 0x01; //?? + //dataTX[7] = 0x64; //eco,display,beep,ontimerenable, offtimerenable,power,0,0 + //dataTX[8] = 0x08; //mute,0,turbo,health, mode(4) mode 01 heat, 02 dry, 03 cool, 07 fan, 08 auto, health(+16), 41=turbo-heat 43=turbo-cool (turbo = 0x40+ 0x01..0x08) + //dataTX[9] = 0x0f; //0 -31 ; 15 - 16 0,0,0,0, temp(4) settemp 31 - x + //dataTX[10] = 0x00; //0,timerindicator,swingv(3),fan(3) fan+swing modes //0=auto 1=low 2=med 3=high + //dataTX[11] = 0x00; //0,offtimer(6),0 + dataTX[12] = 0x00; //fahrenheit,ontimer(6),0 cf 80=f 0=c + dataTX[13] = 0x01; //?? + dataTX[14] = 0x00; //0,0,halfdegree,0,0,0,0,0 + dataTX[15] = 0x00; //?? + dataTX[16] = 0x00; //?? + dataTX[17] = 0x00; //?? + dataTX[18] = 0x00; //?? + dataTX[19] = 0x00; //sleep on = 1 off=0 + dataTX[20] = 0x00; //?? + dataTX[21] = 0x00; //?? + dataTX[22] = 0x00; //?? + dataTX[23] = 0x00; //?? + dataTX[24] = 0x00; //?? + dataTX[25] = 0x00; //?? + dataTX[26] = 0x00; //?? + dataTX[27] = 0x00; //?? + dataTX[28] = 0x00; //?? + dataTX[30] = 0x00; //?? + dataTX[31] = 0x00; //?? + //dataTX[32] = 0x00; //0,0,0,режим вертикального качания(2),режим вертикальной фиксации(3) + //dataTX[33] = 0x00; //0,0,режим горизонтального качания(3),режим горизонтальной фиксации(3) + dataTX[34] = 0x00; //?? + dataTX[35] = 0x00; //?? + dataTX[36] = 0x00; //?? + dataTX[37] = 0xFF; //Контрольная сумма + dataTX[37] = tclacClimate::getChecksum(dataTX, sizeof(dataTX)); + + tclacClimate::sendData(dataTX, sizeof(dataTX)); +} + +void tclacClimate::sendData(byte * message, byte size) { + tclacClimate::dataShow(1,1); + //Serial.write(message, size); + this->esphome::uart::UARTDevice::write_array(message, size); + auto raw = getHex(message, size); + ESP_LOGD("TCL", "Message to TCL sended..."); + tclacClimate::dataShow(1,0); +} + +String tclacClimate::getHex(byte *message, byte size) { + String raw; + for (int i = 0; i < size; i++) { + raw += "\n" + String(message[i]); + } + raw.toUpperCase(); + return raw; +} + +byte tclacClimate::getChecksum(const byte * message, size_t size) { + byte position = size - 1; + byte crc = 0; + for (int i = 0; i < position; i++) + crc ^= message[i]; + return crc; +} + +void tclacClimate::dataShow(bool flow, bool shine) { + if (module_display_status_){ + if (flow == 0){ + if (shine == 1){ +#ifdef CONF_RX_LED + this->rx_led_pin_->digital_write(true); +#endif + } else { +#ifdef CONF_RX_LED + this->rx_led_pin_->digital_write(false); +#endif + } + } + if (flow == 1) { + if (shine == 1){ +#ifdef CONF_TX_LED + this->tx_led_pin_->digital_write(true); +#endif + } else { +#ifdef CONF_TX_LED + this->tx_led_pin_->digital_write(false); +#endif + } + } + } +} + +// Действия с данными из конфига + +//void tclacClimate::set_supported_modes(const std::set &modes) { +// this->traits.set_supported_modes(modes); +// this->traits.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available +// this->traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available +//} + +void tclacClimate::set_beeper_state(bool state) { + this->beeper_status_ = state; +} + +void tclacClimate::set_display_state(bool state) { + this->display_status_ = state; +} + +#ifdef CONF_RX_LED +void tclacClimate::set_rx_led_pin(GPIOPin *rx_led_pin) { + this->rx_led_pin_ = rx_led_pin; +} +#endif + +#ifdef CONF_TX_LED +void tclacClimate::set_tx_led_pin(GPIOPin *tx_led_pin) { + this->tx_led_pin_ = tx_led_pin; +} +#endif + +void tclacClimate::set_module_display_state(bool state) { + this->module_display_status_ = state; +} + +void tclacClimate::set_vertical_airflow(AirflowVerticalDirection direction) { + this->vertical_direction_ = direction; +} + +void tclacClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { + this->horizontal_direction_ = direction; +} + +void tclacClimate::set_vertical_swing_direction(VerticalSwingDirection direction) { + this->vertical_swing_direction_ = direction; +} + +void tclacClimate::set_horizontal_swing_direction(HorizontalSwingDirection direction) { + this->horizontal_swing_direction_ = direction; +} + +void tclacClimate::set_supported_swing_modes(const std::set &modes) { + this->supported_swing_modes_ = modes; +} + + +// Заготовки функций запроса состояния, может пригодиться в будущем, если делать обратную связь. Очень не хочется, будет очень костыльно. + +//bool tclacClimate::get_beeper_state() const { return this->beeper_status_; } +//bool tclacClimate::get_display_state() const { return this->display_status_; } +//bool tclacClimate::get_module_display_state() const { return this->module_display_status_; } +//AirflowVerticalDirection tclacClimate::get_vertical_airflow() const { return this->vertical_direction_; }; +//AirflowHorizontalDirection tclacClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } +//VerticalSwingDirection tclacClimate::get_vertical_swing_direction() const { return this->vertical_swing_direction_; } +//HorizontalSwingDirection tclacClimate::get_horizontal_swing_direction() const { return this->horizontal_swing_direction_; } + + + +} +} \ No newline at end of file diff --git a/components/tclac/tclac.h b/components/tclac/tclac.h new file mode 100644 index 0000000..921545e --- /dev/null +++ b/components/tclac/tclac.h @@ -0,0 +1,160 @@ +/** +* Create by Miguel Ángel López on 20/07/19 +* and modify by xaxexa +* and finally made it by Solovey 13.08.2023 +**/ + +#ifndef TCL_ESP_TCL_H +#define TCL_ESP_TCL_H + +#include "esphome.h" +#include "esphome/core/defines.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/display/display.h" + +namespace esphome { +namespace tclac { + +#define SET_TEMP_MASK 0b00001111 + +#define MODE_POS 7 +#define MODE_MASK 0b00111111 + +#define MODE_AUTO 0b00110101 +#define MODE_COOL 0b00110001 +#define MODE_DRY 0b00110011 +#define MODE_FAN_ONLY 0b00110010 +#define MODE_HEAT 0b00110100 + +#define FAN_SPEED_POS 8 +#define FAN_QUIET_POS 33 + + +#define FAN_AUTO 0b10000000 //auto +#define FAN_QUIET 0x80 //silent +#define FAN_LOW 0b10010000 // | +#define FAN_MIDDLE 0b11000000 // || +#define FAN_MEDIUM 0b10100000 // ||| +#define FAN_HIGH 0b11010000 // |||| +#define FAN_FOCUS 0b10110000 // ||||| +#define FAN_DIFFUSE 0b10000000 // POWER [7] +#define FAN_SPEED_MASK 0b11110000 //FAN SPEED MASK + + +#define SWING_POS 10 +#define SWING_OFF 0b00000000 +#define SWING_HORIZONTAL 0b00100000 +#define SWING_VERTICAL 0b01000000 +#define SWING_BOTH 0b01100000 +#define SWING_MODE_MASK 0b01100000 + + +#define MIN_SET_TEMPERATURE 16 +#define MAX_SET_TEMPERATURE 31 +#define STEP_TEMPERATURE 1 + +using climate::ClimateCall; +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateMode; +using climate::ClimateSwingMode; +using climate::ClimateFanMode; + +enum class AirflowVerticalDirection : uint8_t { + LAST = 0, + MAX_UP = 1, + UP = 2, + CENTER = 3, + DOWN = 4, + MAX_DOWN = 5, +}; +enum class AirflowHorizontalDirection : uint8_t { + LAST = 0, + MAX_LEFT = 1, + LEFT = 2, + CENTER = 3, + RIGHT = 4, + MAX_RIGHT = 5, +}; +enum class VerticalSwingDirection : uint8_t { + UP_DOWN = 0, + UPSIDE = 1, + DOWNSIDE = 2, +}; +enum class HorizontalSwingDirection : uint8_t { + LEFT_RIGHT = 0, + LEFTSIDE = 1, + CENTER = 2, + RIGHTSIDE = 3, +}; + +class tclacClimate : public climate::Climate, public esphome::uart::UARTDevice, public PollingComponent { + + private: + byte checksum; + // dataTX с управлением состоит из 38 байт + byte dataTX[38]; + // А dataRX по прежнему из 61 байта + byte dataRX[61]; + // Команда запроса состояния + byte poll[8] = {0xBB,0x00,0x01,0x04,0x02,0x01,0x00,0xBD}; + // Инициализация и начальное наполнение переменных состоянй переключателей + bool beeper_status_; + bool display_status_; + bool module_display_status_; + + public: + + tclacClimate() : PollingComponent(5 * 1000) { + checksum = 0; + } + + void setup() override; + void loop() override; + void update() override; + void readData(); + void control(const ClimateCall &call) override; // Climate control + void sendData(byte * message, byte size); + static String getHex(byte *message, byte size); + static byte getChecksum(const byte * message, size_t size); + void dataShow(bool flow, bool shine); + + // Заготовки функций запроса состояния, может пригодиться в будущем, если делать обратную связь. Очень не хочется, будет очень костыльно. + + //bool get_beeper_state() const; + //bool get_display_state() const; + //bool tclacClimate::get_module_display_state() const; + //AirflowVerticalDirection get_vertical_airflow() const; + //AirflowHorizontalDirection get_horizontal_airflow() const; + //VerticalSwingDirection tclacClimate::get_vertical_swing_direction() const; + //HorizontalSwingDirection tclacClimate::get_horizontal_swing_direction() const; + + void set_beeper_state(bool state); + void set_display_state(bool state); + void set_rx_led_pin(GPIOPin *rx_led_pin); + void set_tx_led_pin(GPIOPin *tx_led_pin); + void set_module_display_state(bool state); + void set_vertical_airflow(AirflowVerticalDirection direction); + void set_horizontal_airflow(AirflowHorizontalDirection direction); + void set_vertical_swing_direction(VerticalSwingDirection direction); + void set_horizontal_swing_direction(HorizontalSwingDirection direction); + void set_supported_modes(const std::set &modes); + void set_supported_swing_modes(const std::set &modes); + + + protected: + GPIOPin *rx_led_pin_; + GPIOPin *tx_led_pin_; + ClimateTraits traits() override; + AirflowVerticalDirection vertical_direction_; + AirflowHorizontalDirection horizontal_direction_; + VerticalSwingDirection vertical_swing_direction_; + std::set supported_swing_modes_{}; + HorizontalSwingDirection horizontal_swing_direction_; +}; + +} +} + +#endif //TCL_ESP_TCL_H diff --git a/packages/core.yaml b/packages/core.yaml new file mode 100644 index 0000000..fef2be2 --- /dev/null +++ b/packages/core.yaml @@ -0,0 +1,286 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # НЕ РЕДАКТИРОВАТЬ!! # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# Компонент климата +external_components: + - source: https://github.com/I-am-nightingale/tclac.git + type: git + ref: main + components: [ tclac ] + +# Конфигурация ESPHome +esphome: + name: ${device_name} + friendly_name: ${humanly_name} + on_boot: + priority: -100 + then: + lambda: !lambda |- + if (id(beep_mode).state){ + id(${device_name}climate).set_beeper_state(true); + } else { + id(${device_name}climate).set_beeper_state(false); + } + if (id(display_mode).state){ + id(${device_name}climate).set_display_state(true); + } else { + id(${device_name}climate).set_display_state(false); + } + if (id(ledflash_mode).state){ + id(${device_name}climate).set_module_display_state(true); + } else { + id(${device_name}climate).set_module_display_state(false); + } + + if (id(vswing).active_index() == 1){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::UP_DOWN); + } else if (id(vswing).active_index() == 2){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::UPSIDE); + } else if (id(vswing).active_index() == 3){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::DOWNSIDE); + } + + if (id(hswing).active_index() == 1){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::LEFT_RIGHT); + } else if (id(hswing).active_index() == 2){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::LEFTSIDE); + } else if (id(hswing).active_index() == 3){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::CENTER); + } else if (id(hswing).active_index() == 4){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::RIGHTSIDE); + } + + if (id(vfixing).active_index() == 1){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::LAST); + } else if (id(vfixing).active_index() == 2){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::MAX_UP); + } else if (id(vfixing).active_index() == 3){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::UP); + } else if (id(vfixing).active_index() == 4){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::CENTER); + } else if (id(vfixing).active_index() == 5){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::DOWN); + } else if (id(vfixing).active_index() == 6){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::MAX_DOWN); + } + + if (id(vswing).active_index() == 1){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::LAST); + } else if (id(vswing).active_index() == 2){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::MAX_LEFT); + } else if (id(vswing).active_index() == 3){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::LEFT); + } else if (id(vswing).active_index() == 4){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::CENTER); + } else if (id(vswing).active_index() == 5){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::RIGHT); + } else if (id(vswing).active_index() == 6){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::MAX_RIGHT); + } + +uart: + baud_rate: 9600 + rx_pin: ${uart_rx} + tx_pin: ${uart_tx} + +api: + encryption: + key: "${api_key}" + +ota: + password: "${ota_pass}" + +wifi: + id: "${device_name}_wifi" + ssid: ${wifi_ssid} + password: ${wifi_password} + ap: + ssid: "${device_name} Fallback Hotspot" + password: "${recovery_pass}" +# Если требуется ручное указание настроек сети- это здесь: + manual_ip: + static_ip: 192.168.1.204 + gateway: 192.168.1.1 + subnet: 255.255.255.0 + +captive_portal: + + +# # # # # # # # # # # # # # # # # # # # # # # +# # # # # НЕ РЕДАКТИРОВАТЬ! # # # # # +# # # # # # # # # # # # # # # # # # # # # # # + +# ОБЯЗАТЕЛЬНО отключаем логгирование через UART +logger: + baud_rate: 0 + +# Раздел настроек устройства: +# - Переключатели +switch: + # Пищалка для подтверждения команд, по умолчанию выключена + - name: Beeper + platform: template + device_class: switch + id: beep_mode + entity_category: config + restore_mode: RESTORE_DEFAULT_ON + optimistic: true + on_turn_on: + then: + lambda: !lambda |- + id(${device_name}climate).set_beeper_state(true); + on_turn_off: + then: + lambda: !lambda |- + id(${device_name}climate).set_beeper_state(false); + + # Индикатор уставки температуры на корпусе внутреннего блока, по умолчанию включен + - name: Display + platform: template + device_class: switch + id: display_mode + entity_category: config + restore_mode: RESTORE_DEFAULT_ON + optimistic: true + on_turn_on: + then: + lambda: !lambda |- + id(${device_name}climate).set_display_state(true); + on_turn_off: + then: + lambda: !lambda |- + id(${device_name}climate).set_display_state(false); + + # Светодиодная индикация обмена данными с кондиционером, по умолчанию выключена + - name: Module display + platform: template + device_class: switch + id: ledflash_mode + entity_category: config + restore_mode: RESTORE_DEFAULT_ON + optimistic: true + on_turn_on: + then: + lambda: !lambda |- + id(${device_name}climate).set_module_display_state(true); + on_turn_off: + then: + lambda: !lambda |- + id(${device_name}climate).set_module_display_state(false); + +# - Выпадающие списки +select: + # Настройка вертикального качания + - platform: template + name: Vertical swing + id: vswing + entity_category: config + options: + - "Сверху вниз" + - "В верхней половине" + - "В нижней половине" + optimistic: true + restore_value: true + set_action: + then: + lambda: !lambda |- + if (id(vswing).active_index() == 1){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::UP_DOWN); + } else if (id(vswing).active_index() == 2){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::UPSIDE); + } else if (id(vswing).active_index() == 3){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::DOWNSIDE); + } + + # Настройка горизонтального качания + - platform: template + name: Horizontal swing + id: hswing + entity_category: config + options: + - "Слева направо" + - "В левой части" + - "В центре" + - "В правой части" + optimistic: true + restore_value: true + set_action: + then: + lambda: !lambda |- + if (x == "Слева направо"){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::LEFT_RIGHT); + } else if (x == "В левой части"){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::LEFTSIDE); + } else if (x == "В центре"){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::CENTER); + } else if (x == "В правой части"){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::RIGHTSIDE); + } + + # Настройка фиксации вертикальной заслонки + - platform: template + name: Vertical fixing + id: vfixing + entity_category: config + options: + - Last + - Max_Up + - Up + - Center + - Down + - Max_Down + optimistic: true + restore_value: true + set_action: + then: + lambda: !lambda |- + if (x == "Last"){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::LAST); + } else if (x == "Max_Up"){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::MAX_UP); + } else if (x == "Up"){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::UP); + } else if (x == "Center"){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::CENTER); + } else if (x == "Down"){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::DOWN); + } else if (x == "Max_Down"){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::MAX_DOWN); + } + + # Настройка фиксации горизонатальных заслонок + - platform: template + name: Horizontal fixing + id: hfixing + entity_category: config + options: + - "Последнее положение" + - "До упора влево" + - "В левой половине" + - "По середине" + - "В правой половине" + - "До упора вправо" + optimistic: true + restore_value: true + set_action: + then: + lambda: !lambda |- + if (x == "Последнее положение"){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::LAST); + } else if (x == "До упора влево"){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::MAX_LEFT); + } else if (x == "В левой половине"){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::LEFT); + } else if (x == "По середине"){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::CENTER); + } else if (x == "В правой половине"){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::RIGHT); + } else if (x == "До упора вправо"){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::MAX_RIGHT); + } + +climate: + - platform: tclac + name: "${device_name} Climate" + id: ${device_name}climate \ No newline at end of file diff --git a/packages/leds.yaml b/packages/leds.yaml new file mode 100644 index 0000000..3ec0e77 --- /dev/null +++ b/packages/leds.yaml @@ -0,0 +1,5 @@ +climate: + - platform: tclac + id: !extend ${device_name}climate + rx_led: ${receive_led} + tx_led: ${transmit_led} \ No newline at end of file diff --git a/packages/screen.yaml b/packages/screen.yaml new file mode 100644 index 0000000..9ae1a94 --- /dev/null +++ b/packages/screen.yaml @@ -0,0 +1,78 @@ +time: + - platform: homeassistant + id: my_time + +sensor: + - platform: wifi_signal + id: wifi_strenght + internal: true + update_interval: 1s + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + +font: + - file: "gfonts://Press Start 2P" + id: font1 + size: 16 + bpp: 1 + glyphs: '!"%()+=,-_.:°/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzАБВГДЕЖЗИКЛМНОПРСТУФХЦЧШЩЪЬЭЮЯЫабвгдежзиклмнопрстуфхцчшщъыьэюя' + + - file: "gfonts://Press Start 2P" + id: font2 + size: 8 + bpp: 1 + glyphs: '!"%()+=,-_.:°/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzАБВГДЕЖЗИКЛМНОПРСТУФХЦЧШЩЪЬЭЮЯЫабвгдежзиклмнопрстуфхцчшщъыьэюя' + +i2c: + sda: GPIO0 + scl: GPIO2 + frequency: 400kHz + +display: + - platform: ssd1306_i2c + model: "SSD1306 128x32" + address: 0x3C + rotation: 180 + id: oled_disp + lambda: |- + if (id(${device_name}_wifi).is_connected()){ + auto time = id(my_time).now(); + id(oled_disp).strftime(0, 0, id(font2), "%H:%M", time); + id(oled_disp).line(0, 11, 127, 11); + + if (id(${device_name}climate).mode == CLIMATE_MODE_OFF){ + id(oled_disp).print(0, 14, id(font1), "Выключен"); + } else if (id(${device_name}climate).mode == CLIMATE_MODE_AUTO){ + id(oled_disp).print(0, 14, id(font1), "Авторежим"); + } else if (id(${device_name}climate).mode == CLIMATE_MODE_COOL){ + id(oled_disp).print(0, 14, id(font1), "Охлаждение"); + } else if (id(${device_name}climate).mode == CLIMATE_MODE_HEAT){ + id(oled_disp).print(0, 14, id(font1), "Нагревание"); + } else if (id(${device_name}climate).mode == CLIMATE_MODE_DRY){ + id(oled_disp).print(0, 14, id(font1), "Осушение"); + } else if (id(${device_name}climate).mode == CLIMATE_MODE_FAN_ONLY){ + id(oled_disp).print(0, 14, id(font1), "Вентилятор"); + } + + if (id(beep_mode).state){ + id(oled_disp).print(60, 0, id(font2), "П"); + } + if (id(display_mode).state){ + id(oled_disp).print(80, 0, id(font2), "Д"); + } + if (id(wifi_strenght).state > 20){ + id(oled_disp).filled_rectangle(109, 6, 3, 2); + } + if (id(wifi_strenght).state > 50){ + id(oled_disp).filled_rectangle(114, 4, 3, 4); + } + if (id(wifi_strenght).state > 70){ + id(oled_disp).filled_rectangle(119, 2, 3, 6); + } + if (id(wifi_strenght).state > 85){ + id(oled_disp).filled_rectangle(124, 0, 3, 8); + } + } + else { + id(oled_disp).print(0, 0, id(font1), "Ready!"); + } \ No newline at end of file