diff --git a/Sample_conf.yaml b/Sample_conf.yaml new file mode 100644 index 0000000..7ca2612 --- /dev/null +++ b/Sample_conf.yaml @@ -0,0 +1,37 @@ +esphome: + name: klimaanlage + friendly_name: Klimaanlage + +esp32: + board: esp32dev + + +# Enable logging +logger: + +# Enable Home Assistant API +api: + encryption: + key: "glXNdaZaLVlV8p2M0sYLNr+8BPt4zOU8HAhanEuRDCU=" + + +substitutions: + device_name: tclac + humanly_name: TCL Air Conditioner + wifi_ssid: !secret wifi_ssid + wifi_password: !secret wifi_password + recovery_pass: mKSzmBdt7DAj + ota_pass: 90cf525a594c8bb2cbf917bc2c2fa058 + uart_rx: GPIO1 + uart_tx: GPIO3 + receive_led: GPIO2 + transmit_led: GPIO2 + +packages: + remote_package: + url: https://github.com/sorz2122/tclac.git + ref: master + files: + - packages/core.yaml + refresh: 30s + diff --git a/TCL-Conditioner.yaml b/TCL-Conditioner.yaml new file mode 100644 index 0000000..1f94790 --- /dev/null +++ b/TCL-Conditioner.yaml @@ -0,0 +1,61 @@ +substitutions: +# Eindeutiger Name für diese Konfiguration – verwende nur lateinische Buchstaben und Zahlen, +# keine Leerzeichen oder Sonderzeichen, z. B. "tclac". +# Wenn du mehrere Klimamodule hast, muss device_name entsprechend geändert werden, +# z. B. durch Anhängen von Zahlen: tclac1, tclac2, tclac3… + device_name: tclac +# Menschlich lesbarer Name für das Klimagerät, angezeigt in der Benutzeroberfläche: + humanly_name: TCL Air Conditioner +# WLAN-Zugangsdaten – können alternativ in der Datei "secret" in ESPHome gespeichert sein, sonst hier bearbeiten: +# Name des WLAN-Netzwerks: + wifi_ssid: !secret wifi_ssid +# WLAN-Passwort: + wifi_password: !secret wifi_password +# Passwort für den Wiederherstellungsmodus – der Hotspot heißt dann: "{device_name} Fallback Hotspot" + recovery_pass: mKSzmBdt7DAj +# Passwort für OTA-Updates – wird einmal generiert und dauerhaft verwendet: + ota_pass: 90cf525a594c8bb2cbf917bc2c2fa058 +# API-Schlüssel zur Verbindung mit Home Assistant – neuen Schlüssel hier generieren: https://esphome.io/components/api.html + api_key: lmUoWsWs1oqSfJqLFsA6PisHSx4Y1G+iu7V3PDIqpXk= +# GPIO-Pins des Moduls, an die UART angeschlossen ist: + uart_rx: GPIO3 + uart_tx: GPIO1 +# GPIO-Pins für Status-LEDs (Empfang/Senden). Funktionieren nur, wenn "leds" in "packages" aktiviert ist. +# Falls du keine LEDs verwenden möchtest, kannst du hier beliebige Werte eintragen. + receive_led: GPIO6 + transmit_led: GPIO4 + +# Eingebundene Dateien – werden automatisch aus dem Internet geladen und aktualisiert. +# Kommentiere die gewünschten Optionen ein. +# Beschreibung der Optionen: +# - packages/leds.yaml – LEDs zur Anzeige von Senden/Empfangen; Pins über receive_led / transmit_led definieren +# +# Alle eingetragenen Zeilen müssen exakt auf dieser Einrückungsebene stehen – sonst kommt es zu Fehlern! +packages: + remote_package: + url: https://github.com/sorz2122/tclac.git + ref: master + files: + # v – Zeilen müssen exakt hier ausgerichtet sein, sonst gibt’s Probleme! + - packages/core.yaml # Das Herzstück des Ganzen + # - packages/leds.yaml + refresh: 30s + +# Ausgewähltes Modul, das mit dieser Firmware geflasht wird. +# Vorbereitete Konfigurationen können durch Entfernen des Kommentars verwendet werden: + +# ESP-01S +esp8266: + board: esp01_1m + +# Hommyn HDN/WFN-02-01 (aus dem ersten Artikel) +#esp32: +# board: esp32-c3-devkitm-1 +# framework: +# type: arduino + +# Status-LED – optional aktivieren, wenn freie Pins vorhanden sind +#status_led: +# pin: +# number: GPIO5 +# inverted: false 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..88ee1a5 --- /dev/null +++ b/components/tclac/automation.h @@ -0,0 +1,134 @@ +#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_; +}; + +// Шаблон действия: включение принудительного применения настроек +template class ForceOnAction : public Action { + public: + ForceOnAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_force_mode_state(true); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: выключение принудительного применения настроек +template class ForceOffAction : public Action { + public: + ForceOffAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_force_mode_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..cc0f6c4 --- /dev/null +++ b/components/tclac/climate.py @@ -0,0 +1,342 @@ +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_VISUAL, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_SUPPORTED_MODES, + CONF_TEMPERATURE_STEP, + CONF_SUPPORTED_PRESETS, + CONF_TARGET_TEMPERATURE, + CONF_SUPPORTED_FAN_MODES, + CONF_SUPPORTED_SWING_MODES, +) + +from esphome.components.climate import ( + ClimateMode, + ClimatePreset, + ClimateSwingMode, + CONF_CURRENT_TEMPERATURE, +) + +AUTO_LOAD = ["climate"] +CODEOWNERS = ["@I-am-nightingale", "@xaxexa", "@junkfix"] +DEPENDENCIES = ["climate", "uart"] + +TCLAC_MIN_TEMPERATURE = 16.0 +TCLAC_MAX_TEMPERATURE = 31.0 +TCLAC_TARGET_TEMPERATURE_STEP = 1.0 +TCLAC_CURRENT_TEMPERATURE_STEP = 1.0 + +CONF_RX_LED = "rx_led" +CONF_TX_LED = "tx_led" +CONF_DISPLAY = "show_display" +CONF_FORCE_MODE = "force_mode" +CONF_VERTICAL_AIRFLOW = "vertical_airflow" +CONF_MODULE_DISPLAY = "show_module_display" +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) + +SUPPORTED_FAN_MODES_OPTIONS = { + "AUTO": ClimateMode.CLIMATE_FAN_AUTO, # Доступен всегда + "QUIET": ClimateMode.CLIMATE_FAN_QUIET, + "LOW": ClimateMode.CLIMATE_FAN_LOW, + "MIDDLE": ClimateMode.CLIMATE_FAN_MIDDLE, + "MEDIUM": ClimateMode.CLIMATE_FAN_MEDIUM, + "HIGH": ClimateMode.CLIMATE_FAN_HIGH, + "FOCUS": ClimateMode.CLIMATE_FAN_FOCUS, + "DIFFUSE": ClimateMode.CLIMATE_FAN_DIFFUSE, +} + +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, # Доступен всегда + "AUTO": ClimateMode.CLIMATE_MODE_AUTO, # Доступен всегда + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + +SUPPORTED_CLIMATE_PRESETS_OPTIONS = { + "NONE": ClimatePreset.CLIMATE_PRESET_NONE, # Доступен всегда + "ECO": ClimatePreset.CLIMATE_PRESET_ECO, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, + "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT, +} + +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, +} + +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, +} + +# Проверка конфигурации интерфейса и принятие значений по умолчанию +def validate_visual(config): + if CONF_VISUAL in config: + visual_config = config[CONF_VISUAL] + if CONF_MIN_TEMPERATURE in visual_config: + min_temp = visual_config[CONF_MIN_TEMPERATURE] + if min_temp < TCLAC_MIN_TEMPERATURE: + raise cv.Invalid(f"Указанная интерфейсная минимальная температура в {min_temp} ниже допустимой {TCLAC_MIN_TEMPERATURE} для кондиционера") + else: + config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = TCLAC_MIN_TEMPERATURE + if CONF_MAX_TEMPERATURE in visual_config: + max_temp = visual_config[CONF_MAX_TEMPERATURE] + if max_temp > TCLAC_MAX_TEMPERATURE: + raise cv.Invalid(f"Указанная интерфейсная максимальная температура в {max_temp} выше допустимой {TCLAC_MAX_TEMPERATURE} для кондиционера") + else: + config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = TCLAC_MAX_TEMPERATURE + if CONF_TEMPERATURE_STEP in visual_config: + temp_step = config[CONF_VISUAL][CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE] + if ((int)(temp_step * 2)) / 2 != temp_step: + raise cv.Invalid(f"Указанный шаг температуры {temp_step} не корректен, должен быть кратен 1") + else: + config[CONF_VISUAL][CONF_TEMPERATURE_STEP] = {CONF_TARGET_TEMPERATURE: TCLAC_TARGET_TEMPERATURE_STEP,CONF_CURRENT_TEMPERATURE: TCLAC_CURRENT_TEMPERATURE_STEP,} + else: + config[CONF_VISUAL] = {CONF_MIN_TEMPERATURE: TCLAC_MIN_TEMPERATURE,CONF_MAX_TEMPERATURE: TCLAC_MAX_TEMPERATURE,CONF_TEMPERATURE_STEP: {CONF_TARGET_TEMPERATURE: TCLAC_TARGET_TEMPERATURE_STEP,CONF_CURRENT_TEMPERATURE: TCLAC_CURRENT_TEMPERATURE_STEP,},} + return config + +# Проверка конфигурации компонента и принятие значений по умолчанию +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_FORCE_MODE, default=True): cv.boolean, + cv.Optional(CONF_MODULE_DISPLAY, default=True): cv.boolean, + cv.Optional(CONF_VERTICAL_AIRFLOW, default="CENTER"): cv.ensure_list(cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True)), + cv.Optional(CONF_VERTICAL_SWING_MODE, default="UP_DOWN"): cv.ensure_list(cv.enum(VERTICAL_SWING_DIRECTION_OPTIONS, upper=True)), + cv.Optional(CONF_HORIZONTAL_AIRFLOW, default="CENTER"): cv.ensure_list(cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True)), + cv.Optional(CONF_HORIZONTAL_SWING_MODE, default="LEFT_RIGHT"): cv.ensure_list(cv.enum(HORIZONTAL_SWING_DIRECTION_OPTIONS, upper=True)), + cv.Optional(CONF_SUPPORTED_PRESETS,default=["NONE","ECO","SLEEP","COMFORT",],): cv.ensure_list(cv.enum(SUPPORTED_CLIMATE_PRESETS_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)), + cv.Optional(CONF_SUPPORTED_MODES,default=["OFF","AUTO","COOL","HEAT","DRY","FAN_ONLY",],): cv.ensure_list(cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)), + cv.Optional(CONF_SUPPORTED_FAN_MODES,default=["AUTO","QUIET","LOW","MIDDLE","MEDIUM","HIGH","FOCUS","DIFFUSE",],): cv.ensure_list(cv.enum(SUPPORTED_FAN_MODES_OPTIONS, upper=True)), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + validate_visual, +) + +ForceOnAction = tclac_ns.class_("ForceOnAction", automation.Action) +ForceOffAction = tclac_ns.class_("ForceOffAction", automation.Action) +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) +VerticalAirflowAction = tclac_ns.class_("VerticalAirflowAction", automation.Action) +ModuleDisplayOffAction = tclac_ns.class_("ModuleDisplayOffAction", 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 = 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 + +# Регистрация событий включения и отключения принудительного применения настроек +@automation.register_action( + "climate.tclac.force_mode_on", ForceOnAction, cv.Schema +) +@automation.register_action( + "climate.tclac.force_mode_off", ForceOffAction, cv.Schema +) +async def force_mode_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.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 + + +# Регистрация события установки горизонтальной фиксации заслонок +@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_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_FORCE_MODE in config: + cg.add(var.set_force_mode_state(config[CONF_FORCE_MODE])) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + if CONF_SUPPORTED_PRESETS in config: + cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) + if CONF_MODULE_DISPLAY in config: + cg.add(var.set_module_display_state(config[CONF_MODULE_DISPLAY])) + if CONF_SUPPORTED_FAN_MODES in config: + cg.add(var.set_supported_fan_modes(config[CONF_SUPPORTED_FAN_MODES])) + if CONF_SUPPORTED_SWING_MODES in config: + cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + + 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)) + 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)) \ No newline at end of file diff --git a/components/tclac/tclac.cpp b/components/tclac/tclac.cpp new file mode 100644 index 0000000..6389ad2 --- /dev/null +++ b/components/tclac/tclac.cpp @@ -0,0 +1,773 @@ +/** +// * Create by Miguel Ángel López on 20/07/19 +* and modify by xaxexa +* Refactoring & component making: +// * Соловей с паяльником 15.03.2024 +**/ +#include "esphome.h" +#include "esphome/core/defines.h" +#include "tclac.h" +#include +#include + + +namespace esphome{ +namespace tclac{ + +constexpr uint8_t TCL_FRAME_TYPE_STATUS = 0x04; + +ClimateTraits tclacClimate::traits() { + auto traits = climate::ClimateTraits(); + + + if (this->supported_modes_.empty()) { + traits.add_supported_mode(climate::CLIMATE_MODE_OFF); + traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); + } else { + for (auto mode : this->supported_modes_) + traits.add_supported_mode(mode); + } + if (this->supported_presets_.empty()) { + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE); + } else { + for (auto preset : this->supported_presets_) + traits.add_supported_preset(preset); + } + if (this->supported_fan_modes_.empty()) { + traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO); + } else { + for (auto fan_mode : this->supported_fan_modes_) + traits.add_supported_fan_mode(fan_mode); + } + if (this->supported_swing_modes_.empty()) { + traits.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); + } else { + for (auto swing_mode : this->supported_swing_modes_) + traits.add_supported_swing_mode(swing_mode); + } + + return traits; +} + + +void tclacClimate::setup() { + +#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); + size_t skipped = 0; + bool header_found = false; + while (esphome::uart::UARTDevice::available() > 0) { + uint8_t byte = esphome::uart::UARTDevice::read(); + if (byte == 0xBB) { + dataRX[0] = byte; + header_found = true; + break; + } + skipped++; + } + if (!header_found) { + if (skipped > 0) + ESP_LOGV("TCL", "Skipped %u byte(s) waiting for header", (unsigned) skipped); + dataShow(0,0); + return; + } + if (skipped > 0) + ESP_LOGV("TCL", "Resynced after skipping %u byte(s)", (unsigned) skipped); + // А вот если совпал заголовок (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 байт нам нужен пятый- он содержит длину сообщения + size_t payload_with_checksum = static_cast(dataRX[4]) + 1; + size_t frame_size = payload_with_checksum + 5; + if (frame_size > sizeof(dataRX)) { + ESP_LOGW("TCL", "Frame size %u exceeds buffer", (unsigned) frame_size); + tclacClimate::dataShow(0,0); + return; + } + esphome::uart::UARTDevice::read_array(dataRX + 5, payload_with_checksum); + + uint8_t check = getChecksum(dataRX, frame_size); + + //raw = getHex(dataRX, sizeof(dataRX)); + + //ESP_LOGD("TCL", "RX full : %s ", raw.c_str()); + + // Проверяем контрольную сумму + if (check != dataRX[frame_size - 1]) { + ESP_LOGD("TCL", "Invalid checksum %x", check); + tclacClimate::dataShow(0,0); + return; + } else { + //ESP_LOGD("TCL", "checksum OK %x", check); + } + tclacClimate::dataShow(0,0); + uint8_t frame_type = dataRX[3]; + if (frame_type != TCL_FRAME_TYPE_STATUS) { + ESP_LOGV("TCL", "Ignoring frame type 0x%02X", frame_type); + return; + } + // Прочитав все из буфера приступаем к разбору данны + + tclacClimate::readData(); + } +} + +void tclacClimate::update() { + tclacClimate::dataShow(1,1); + this->esphome::uart::UARTDevice::write_array(poll_message_, sizeof(poll_message_)); + //auto raw = tclacClimate::getHex(poll_message_, sizeof(poll_message_)); + //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); + + bool device_is_on = (dataRX[MODE_POS] & MODE_POWER_FLAG) != 0; + if (device_is_on) { + // Если кондиционер включен, то разбираем данные для отображения + // ESP_LOGD("TCL", "AC is on"); + 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; + } + + // Обработка данны + preset = ClimatePreset::CLIMATE_PRESET_NONE; + if (dataRX[7] & (1 << 6)){ + preset = ClimatePreset::CLIMATE_PRESET_ECO; + } else if (dataRX[9] & (1 << 2)){ + preset = ClimatePreset::CLIMATE_PRESET_COMFORT; + } else if (dataRX[19] & (1 << 0)){ + preset = ClimatePreset::CLIMATE_PRESET_SLEEP; + } + + } else { + // Если кондиционер выключен, то все режимы показываются, как выключенные + mode = climate::CLIMATE_MODE_OFF; + //fan_mode = climate::CLIMATE_FAN_OFF; + swing_mode = climate::CLIMATE_SWING_OFF; + preset = ClimatePreset::CLIMATE_PRESET_NONE; + } + // Публикуем данные + this->publish_state(); + allow_take_control = true; + } + +// Climate control +void tclacClimate::control(const ClimateCall &call) { + // Запрашиваем данные из переключателя режимов работы кондиционера + if (call.get_mode().has_value()){ + switch_climate_mode = call.get_mode().value(); + ESP_LOGD("TCL", "Get MODE from call"); + } else { + switch_climate_mode = mode; + ESP_LOGD("TCL", "Get MODE from AC"); + } + + // Запрашиваем данные из переключателя предустановок кондиционера + if (call.get_preset().has_value()){ + switch_preset = call.get_preset().value(); + } else { + switch_preset = preset.value(); + } + + // Запрашиваем данные из переключателя режимов вентилятора + if (call.get_fan_mode().has_value()){ + switch_fan_mode = call.get_fan_mode().value(); + } else { + switch_fan_mode = fan_mode.value(); + } + + // Запрашиваем данные из переключателя режимов качания заслонок + if (call.get_swing_mode().has_value()){ + switch_swing_mode = call.get_swing_mode().value(); + } else { + // А если в переключателе пусто- заполняем значением из последнего опроса состояния. Типа, ничего не поменялось. + switch_swing_mode = swing_mode; + } + + // Расчет температуры + if (call.get_target_temperature().has_value()) { + target_temperature_set = 31-(int)call.get_target_temperature().value(); + } else { + target_temperature_set = 31-(int)target_temperature; + } + + is_call_control = true; + takeControl(); + allow_take_control = true; +} + + +void tclacClimate::takeControl() { + + dataTX[7] = 0b00000000; + dataTX[8] = 0b00000000; + dataTX[9] = 0b00000000; + dataTX[10] = 0b00000000; + dataTX[11] = 0b00000000; + dataTX[19] = 0b00000000; + dataTX[32] = 0b00000000; + dataTX[33] = 0b00000000; + + if (is_call_control != true){ + ESP_LOGD("TCL", "Get MODE from AC for force config"); + switch_climate_mode = mode; + switch_preset = preset.value(); + switch_fan_mode = fan_mode.value(); + switch_swing_mode = swing_mode; + target_temperature_set = 31-(int)target_temperature; + } + + // Включаем или отключаем пищалку в зависимости от переключателя в настройка + + 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_) && (switch_climate_mode != climate::CLIMATE_MODE_OFF)){ + ESP_LOGD("TCL", "Dispaly turn ON"); + dataTX[7] += 0b01000000; + } else { + ESP_LOGD("TCL", "Dispaly turn OFF"); + dataTX[7] += 0b00000000; + } + + // Настраиваем режим работы кондиционера + switch (switch_climate_mode) { + case climate::CLIMATE_MODE_OFF: + dataTX[7] += 0b00000000; + dataTX[8] += 0b00000000; + break; + case climate::CLIMATE_MODE_AUTO: + dataTX[7] += MODE_POWER_FLAG; + dataTX[8] += 0b00001000; + break; + case climate::CLIMATE_MODE_COOL: + dataTX[7] += MODE_POWER_FLAG; + dataTX[8] += 0b00000011; + break; + case climate::CLIMATE_MODE_DRY: + dataTX[7] += MODE_POWER_FLAG; + dataTX[8] += 0b00000010; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + dataTX[7] += MODE_POWER_FLAG; + dataTX[8] += 0b00000111; + break; + case climate::CLIMATE_MODE_HEAT: + dataTX[7] += MODE_POWER_FLAG; + dataTX[8] += 0b00000001; + break; + } + + // Настраиваем режим вентилятора + switch(switch_fan_mode) { + 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; + } + + // Устанавливаем режим качания заслонок + switch(switch_swing_mode) { + 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(switch_preset) { + case ClimatePreset::CLIMATE_PRESET_NONE: + break; + case ClimatePreset::CLIMATE_PRESET_ECO: + dataTX[7] += 0b10000000; + break; + case ClimatePreset::CLIMATE_PRESET_SLEEP: + dataTX[19] += 0b00000001; + break; + case ClimatePreset::CLIMATE_PRESET_COMFORT: + dataTX[8] += 0b00010000; + break; + } + + //Режим заслонок + // Вертикальная заслонка + // Качание вертикальной заслонки [10 байт, маска 00111000]: + // 000 - Качание отключено, заслонка в последней позиции или в фиксации + // 111 - Качание включено в выбранном режиме + // Режим качания вертикальной заслонки (режим фиксации заслонки роли не играет, если качание включено) [32 байт, маска 00011000]: + // 01 - качание свер +// у вниз, ПО УМОЛЧАНИЮ + // 10 - качание в вер +// ней половине + // 11 - качание в нижней половине + // Режим фиксации заслонки (режим качания заслонки роли не играет, если качание выключено) [32 байт, маска 00000111]: + // 000 - нет фиксации, ПО УМОЛЧАНИЮ + // 001 - фиксация ввер +// у + // 010 - фиксация между вер +// ом и серединой + // 011 - фиксация в середине + // 100 - фиксация между серединой и низом + // 101 - фиксация внизу + // Горизонтальные заслонки + // Качание горизонтальны + // 0 - Качание отключено, заслонки в последней позиции или в фиксации + // 1 - Качание включено в выбранном режиме + // Режим качания горизонтальны + // 001 - качание слева направо, ПО УМОЛЧАНИЮ + // 010 - качание слева + // 011 - качание по середине + // 100 - качание справа + // Режим фиксации горизонтальны + // 000 - нет фиксации, ПО УМОЛЧАНИЮ + // 001 - фиксация слева + // 010 - фиксация между левой стороной и серединой + // 011 - фиксация в середине + // 100 - фиксация между серединой и правой стороной + // 101 - фиксация справа + + + // Устанавливаем режим для качания вертикальной заслонки + 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(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(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; + } + + // Установка температуры + dataTX[9] = target_temperature_set; + + // Собираем массив байт для отправки в кондиционер + dataTX[0] = 0xBB; // frame header + dataTX[1] = 0x00; + dataTX[2] = 0x01; + dataTX[3] = 0x03; // 0x03 - control frame, 0x04 - status frame + dataTX[4] = 0x20; // payload length without header + 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)); + allow_take_control = false; + is_call_control = false; +} + +// Отправка данны +void tclacClimate::sendData(uint8_t * message, uint8_t 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); +} + +// Преобразование байта в читабельный формат +std::string tclacClimate::getHex(const byte *message, size_t size) { + std::ostringstream oss; + for (size_t i = 0; i < size; ++i) { + oss << std::hex + << std::uppercase + << std::setw(2) + << std::setfill('0') + << static_cast(message[i]); + if (i + 1 < size) + oss << ' '; + } + std::string s = oss.str(); + std::transform(s.begin(), s.end(), s.begin(), ::toupper); + return s; +} + +// Вычисление контрольной суммы +uint8_t tclacClimate::getChecksum(const byte * message, size_t size) { + uint8_t position = size - 1; + uint8_t 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_beeper_state(bool state) { + this->beeper_status_ = state; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение состояния дисплея кондиционера +void tclacClimate::set_display_state(bool state) { + this->display_status_ = state; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение состояния режима принудительного применения настроек +void tclacClimate::set_force_mode_state(bool state) { + this->force_mode_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; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение режима фиксации горизонтальны +void tclacClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { + this->horizontal_direction_ = direction; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение режима качания вертикальной заслонки +void tclacClimate::set_vertical_swing_direction(VerticalSwingDirection direction) { + this->vertical_swing_direction_ = direction; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение доступны +void tclacClimate::set_supported_modes(const std::set &modes) { + this->supported_modes_ = modes; +} +// Получение режима качания горизонтальны +void tclacClimate::set_horizontal_swing_direction(HorizontalSwingDirection direction) { + horizontal_swing_direction_ = direction; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение доступны +void tclacClimate::set_supported_fan_modes(const std::set &modes){ + this->supported_fan_modes_ = modes; +} +// Получение доступны +void tclacClimate::set_supported_swing_modes(const std::set &modes) { + this->supported_swing_modes_ = modes; +} +// Получение доступны +void tclacClimate::set_supported_presets(const std::set &presets) { + this->supported_presets_ = presets; +} + +} +} diff --git a/components/tclac/tclac.h b/components/tclac/tclac.h new file mode 100644 index 0000000..fbb6959 --- /dev/null +++ b/components/tclac/tclac.h @@ -0,0 +1,165 @@ +/** +* Create by Miguel Ángel López on 20/07/19 +* and modify by xaxexa +* Refactoring & component making: +* Соловей с паяльником 15.03.2024 +**/ + +#ifndef TCL_ESP_TCL_H +#define TCL_ESP_TCL_H + +#include "esphome.h" +#include "esphome/core/defines.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/climate/climate.h" +#include +#include + +namespace esphome { +namespace tclac { + +#if !defined(ESPHOME_TCLAC_BYTE_TYPE_DEFINED) +using byte = uint8_t; +#define ESPHOME_TCLAC_BYTE_TYPE_DEFINED +#endif + + +#define SET_TEMP_MASK 0b00001111 + +#define MODE_POS 7 +#define MODE_MASK 0b00111111 +#define MODE_POWER_FLAG 0b00000100 + +#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 + +using climate::ClimateCall; +using climate::ClimateMode; +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateFanMode; +using climate::ClimateSwingMode; + +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, +}; +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, +}; + +class tclacClimate : public climate::Climate, public esphome::uart::UARTDevice, public PollingComponent { + + private: + uint8_t checksum; + // dataTX с управлением состоит из 38 байт + uint8_t dataTX[38]; + // А dataRX по прежнему из 61 байта + uint8_t dataRX[61]; + // Команда запроса состояния + uint8_t poll_message_[8] = {0xBB,0x00,0x01,0x04,0x02,0x01,0x00,0xBD}; + // Инициализация и начальное наполнение переменных состоянй переключателей + bool beeper_status_; + bool display_status_; + bool force_mode_status_; + uint8_t switch_preset = 0; + bool module_display_status_; + uint8_t switch_fan_mode = 0; + bool is_call_control = false; + uint8_t switch_swing_mode = 0; + int target_temperature_set = 0; + uint8_t switch_climate_mode = 0; + bool allow_take_control = false; + + esphome::climate::ClimateTraits traits_; + + public: + + tclacClimate() : PollingComponent(5 * 1000), checksum(0) { + } + + void readData(); + void takeControl(); + void loop() override; + void setup() override; + void update() override; + void set_beeper_state(bool state); + void set_display_state(bool state); + void dataShow(bool flow, bool shine); + void set_force_mode_state(bool state); + void set_rx_led_pin(GPIOPin *rx_led_pin); + void set_tx_led_pin(GPIOPin *tx_led_pin); + void sendData(uint8_t * message, uint8_t size); + void set_module_display_state(bool state); + static std::string getHex(const byte *message, size_t size); + void control(const ClimateCall &call) override; + static uint8_t getChecksum(const byte * message, size_t size); + 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_presets(const std::set &presets); + void set_supported_modes(const std::set &modes); + void set_supported_fan_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; + std::set supported_modes_{}; + std::set supported_presets_{}; + AirflowVerticalDirection vertical_direction_; + std::set supported_fan_modes_{}; + AirflowHorizontalDirection horizontal_direction_; + VerticalSwingDirection vertical_swing_direction_; + std::set supported_swing_modes_{}; + HorizontalSwingDirection horizontal_swing_direction_; +}; +} +} + +#endif //TCL_ESP_TCL_H diff --git a/esphome_components/components/tclac/__init__.py b/esphome_components/components/tclac/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/esphome_components/components/tclac/automation.h b/esphome_components/components/tclac/automation.h new file mode 100644 index 0000000..88ee1a5 --- /dev/null +++ b/esphome_components/components/tclac/automation.h @@ -0,0 +1,134 @@ +#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_; +}; + +// Шаблон действия: включение принудительного применения настроек +template class ForceOnAction : public Action { + public: + ForceOnAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_force_mode_state(true); } + + protected: + tclacClimate *parent_; +}; + +// Шаблон действия: выключение принудительного применения настроек +template class ForceOffAction : public Action { + public: + ForceOffAction(tclacClimate *parent) : parent_(parent) {} + void play(Ts... x) { this->parent_->set_force_mode_state(false); } + + protected: + tclacClimate *parent_; +}; + +} // namespace tclac +} // namespace esphome diff --git a/esphome_components/components/tclac/climate.py b/esphome_components/components/tclac/climate.py new file mode 100644 index 0000000..cc0f6c4 --- /dev/null +++ b/esphome_components/components/tclac/climate.py @@ -0,0 +1,342 @@ +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_VISUAL, + CONF_MAX_TEMPERATURE, + CONF_MIN_TEMPERATURE, + CONF_SUPPORTED_MODES, + CONF_TEMPERATURE_STEP, + CONF_SUPPORTED_PRESETS, + CONF_TARGET_TEMPERATURE, + CONF_SUPPORTED_FAN_MODES, + CONF_SUPPORTED_SWING_MODES, +) + +from esphome.components.climate import ( + ClimateMode, + ClimatePreset, + ClimateSwingMode, + CONF_CURRENT_TEMPERATURE, +) + +AUTO_LOAD = ["climate"] +CODEOWNERS = ["@I-am-nightingale", "@xaxexa", "@junkfix"] +DEPENDENCIES = ["climate", "uart"] + +TCLAC_MIN_TEMPERATURE = 16.0 +TCLAC_MAX_TEMPERATURE = 31.0 +TCLAC_TARGET_TEMPERATURE_STEP = 1.0 +TCLAC_CURRENT_TEMPERATURE_STEP = 1.0 + +CONF_RX_LED = "rx_led" +CONF_TX_LED = "tx_led" +CONF_DISPLAY = "show_display" +CONF_FORCE_MODE = "force_mode" +CONF_VERTICAL_AIRFLOW = "vertical_airflow" +CONF_MODULE_DISPLAY = "show_module_display" +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) + +SUPPORTED_FAN_MODES_OPTIONS = { + "AUTO": ClimateMode.CLIMATE_FAN_AUTO, # Доступен всегда + "QUIET": ClimateMode.CLIMATE_FAN_QUIET, + "LOW": ClimateMode.CLIMATE_FAN_LOW, + "MIDDLE": ClimateMode.CLIMATE_FAN_MIDDLE, + "MEDIUM": ClimateMode.CLIMATE_FAN_MEDIUM, + "HIGH": ClimateMode.CLIMATE_FAN_HIGH, + "FOCUS": ClimateMode.CLIMATE_FAN_FOCUS, + "DIFFUSE": ClimateMode.CLIMATE_FAN_DIFFUSE, +} + +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, # Доступен всегда + "AUTO": ClimateMode.CLIMATE_MODE_AUTO, # Доступен всегда + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, +} + +SUPPORTED_CLIMATE_PRESETS_OPTIONS = { + "NONE": ClimatePreset.CLIMATE_PRESET_NONE, # Доступен всегда + "ECO": ClimatePreset.CLIMATE_PRESET_ECO, + "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, + "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT, +} + +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, +} + +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, +} + +# Проверка конфигурации интерфейса и принятие значений по умолчанию +def validate_visual(config): + if CONF_VISUAL in config: + visual_config = config[CONF_VISUAL] + if CONF_MIN_TEMPERATURE in visual_config: + min_temp = visual_config[CONF_MIN_TEMPERATURE] + if min_temp < TCLAC_MIN_TEMPERATURE: + raise cv.Invalid(f"Указанная интерфейсная минимальная температура в {min_temp} ниже допустимой {TCLAC_MIN_TEMPERATURE} для кондиционера") + else: + config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = TCLAC_MIN_TEMPERATURE + if CONF_MAX_TEMPERATURE in visual_config: + max_temp = visual_config[CONF_MAX_TEMPERATURE] + if max_temp > TCLAC_MAX_TEMPERATURE: + raise cv.Invalid(f"Указанная интерфейсная максимальная температура в {max_temp} выше допустимой {TCLAC_MAX_TEMPERATURE} для кондиционера") + else: + config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = TCLAC_MAX_TEMPERATURE + if CONF_TEMPERATURE_STEP in visual_config: + temp_step = config[CONF_VISUAL][CONF_TEMPERATURE_STEP][CONF_TARGET_TEMPERATURE] + if ((int)(temp_step * 2)) / 2 != temp_step: + raise cv.Invalid(f"Указанный шаг температуры {temp_step} не корректен, должен быть кратен 1") + else: + config[CONF_VISUAL][CONF_TEMPERATURE_STEP] = {CONF_TARGET_TEMPERATURE: TCLAC_TARGET_TEMPERATURE_STEP,CONF_CURRENT_TEMPERATURE: TCLAC_CURRENT_TEMPERATURE_STEP,} + else: + config[CONF_VISUAL] = {CONF_MIN_TEMPERATURE: TCLAC_MIN_TEMPERATURE,CONF_MAX_TEMPERATURE: TCLAC_MAX_TEMPERATURE,CONF_TEMPERATURE_STEP: {CONF_TARGET_TEMPERATURE: TCLAC_TARGET_TEMPERATURE_STEP,CONF_CURRENT_TEMPERATURE: TCLAC_CURRENT_TEMPERATURE_STEP,},} + return config + +# Проверка конфигурации компонента и принятие значений по умолчанию +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_FORCE_MODE, default=True): cv.boolean, + cv.Optional(CONF_MODULE_DISPLAY, default=True): cv.boolean, + cv.Optional(CONF_VERTICAL_AIRFLOW, default="CENTER"): cv.ensure_list(cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True)), + cv.Optional(CONF_VERTICAL_SWING_MODE, default="UP_DOWN"): cv.ensure_list(cv.enum(VERTICAL_SWING_DIRECTION_OPTIONS, upper=True)), + cv.Optional(CONF_HORIZONTAL_AIRFLOW, default="CENTER"): cv.ensure_list(cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True)), + cv.Optional(CONF_HORIZONTAL_SWING_MODE, default="LEFT_RIGHT"): cv.ensure_list(cv.enum(HORIZONTAL_SWING_DIRECTION_OPTIONS, upper=True)), + cv.Optional(CONF_SUPPORTED_PRESETS,default=["NONE","ECO","SLEEP","COMFORT",],): cv.ensure_list(cv.enum(SUPPORTED_CLIMATE_PRESETS_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)), + cv.Optional(CONF_SUPPORTED_MODES,default=["OFF","AUTO","COOL","HEAT","DRY","FAN_ONLY",],): cv.ensure_list(cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)), + cv.Optional(CONF_SUPPORTED_FAN_MODES,default=["AUTO","QUIET","LOW","MIDDLE","MEDIUM","HIGH","FOCUS","DIFFUSE",],): cv.ensure_list(cv.enum(SUPPORTED_FAN_MODES_OPTIONS, upper=True)), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + validate_visual, +) + +ForceOnAction = tclac_ns.class_("ForceOnAction", automation.Action) +ForceOffAction = tclac_ns.class_("ForceOffAction", automation.Action) +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) +VerticalAirflowAction = tclac_ns.class_("VerticalAirflowAction", automation.Action) +ModuleDisplayOffAction = tclac_ns.class_("ModuleDisplayOffAction", 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 = 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 + +# Регистрация событий включения и отключения принудительного применения настроек +@automation.register_action( + "climate.tclac.force_mode_on", ForceOnAction, cv.Schema +) +@automation.register_action( + "climate.tclac.force_mode_off", ForceOffAction, cv.Schema +) +async def force_mode_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.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 + + +# Регистрация события установки горизонтальной фиксации заслонок +@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_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_FORCE_MODE in config: + cg.add(var.set_force_mode_state(config[CONF_FORCE_MODE])) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + if CONF_SUPPORTED_PRESETS in config: + cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) + if CONF_MODULE_DISPLAY in config: + cg.add(var.set_module_display_state(config[CONF_MODULE_DISPLAY])) + if CONF_SUPPORTED_FAN_MODES in config: + cg.add(var.set_supported_fan_modes(config[CONF_SUPPORTED_FAN_MODES])) + if CONF_SUPPORTED_SWING_MODES in config: + cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + + 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)) + 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)) \ No newline at end of file diff --git a/esphome_components/components/tclac/tclac.cpp b/esphome_components/components/tclac/tclac.cpp new file mode 100644 index 0000000..82a2b5d --- /dev/null +++ b/esphome_components/components/tclac/tclac.cpp @@ -0,0 +1,750 @@ +/** +* Create by Miguel Ángel López on 20/07/19 +* and modify by xaxexa +* Refactoring & component making: +* Соловей с паяльником 15.03.2024 +**/ +#include "esphome.h" +#include "esphome/core/defines.h" +#include "tclac.h" +#include +#include + + +namespace esphome{ +namespace tclac{ + + +ClimateTraits tclacClimate::traits() { + auto traits = climate::ClimateTraits(); + + + if (!this->supported_modes_.empty()) + traits.set_supported_modes(this->supported_modes_); + if (!this->supported_presets_.empty()) + traits.set_supported_presets(this->supported_presets_); + if (!this->supported_fan_modes_.empty()) + traits.set_supported_fan_modes(this->supported_fan_modes_); + if (!this->supported_swing_modes_.empty()) + traits.set_supported_swing_modes(this->supported_swing_modes_); + + traits.add_supported_mode(climate::CLIMATE_MODE_OFF); // Выключенный режим кондиционера доступен всегда + traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); // Автоматический режим кондиционера тоже + traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO); // Автоматический режим вентилятора доступен всегда + traits.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); // Выключенный режим качания заслонок доступен всегда + traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);// На всякий случай без предустановок + + return traits; +} + + +void tclacClimate::setup() { + +#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); + + uint8_t 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); + this->esphome::uart::UARTDevice::write_array(poll_message_, sizeof(poll_message_)); + //auto raw = tclacClimate::getHex(poll_message_, sizeof(poll_message_)); + //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)) { + // Если кондиционер включен, то разбираем данные для отображения + // ESP_LOGD("TCL", "AC is on"); + 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; + } + + // Обработка данны + о пресете + preset = ClimatePreset::CLIMATE_PRESET_NONE; + if (dataRX[7] & (1 << 6)){ + preset = ClimatePreset::CLIMATE_PRESET_ECO; + } else if (dataRX[9] & (1 << 2)){ + preset = ClimatePreset::CLIMATE_PRESET_COMFORT; + } else if (dataRX[19] & (1 << 0)){ + preset = ClimatePreset::CLIMATE_PRESET_SLEEP; + } + + } else { + // Если кондиционер выключен, то все режимы показываются, как выключенные + mode = climate::CLIMATE_MODE_OFF; + //fan_mode = climate::CLIMATE_FAN_OFF; + swing_mode = climate::CLIMATE_SWING_OFF; + preset = ClimatePreset::CLIMATE_PRESET_NONE; + } + // Публикуем данные + this->publish_state(); + allow_take_control = true; + } + +// Climate control +void tclacClimate::control(const ClimateCall &call) { + // Запрашиваем данные из переключателя режимов работы кондиционера + if (call.get_mode().has_value()){ + switch_climate_mode = call.get_mode().value(); + ESP_LOGD("TCL", "Get MODE from call"); + } else { + switch_climate_mode = mode; + ESP_LOGD("TCL", "Get MODE from AC"); + } + + // Запрашиваем данные из переключателя предустановок кондиционера + if (call.get_preset().has_value()){ + switch_preset = call.get_preset().value(); + } else { + switch_preset = preset.value(); + } + + // Запрашиваем данные из переключателя режимов вентилятора + if (call.get_fan_mode().has_value()){ + switch_fan_mode = call.get_fan_mode().value(); + } else { + switch_fan_mode = fan_mode.value(); + } + + // Запрашиваем данные из переключателя режимов качания заслонок + if (call.get_swing_mode().has_value()){ + switch_swing_mode = call.get_swing_mode().value(); + } else { + // А если в переключателе пусто- заполняем значением из последнего опроса состояния. Типа, ничего не поменялось. + switch_swing_mode = swing_mode; + } + + // Расчет температуры + if (call.get_target_temperature().has_value()) { + target_temperature_set = 31-(int)call.get_target_temperature().value(); + } else { + target_temperature_set = 31-(int)target_temperature; + } + + is_call_control = true; + takeControl(); + allow_take_control = true; +} + + +void tclacClimate::takeControl() { + + dataTX[7] = 0b00000000; + dataTX[8] = 0b00000000; + dataTX[9] = 0b00000000; + dataTX[10] = 0b00000000; + dataTX[11] = 0b00000000; + dataTX[19] = 0b00000000; + dataTX[32] = 0b00000000; + dataTX[33] = 0b00000000; + + if (is_call_control != true){ + ESP_LOGD("TCL", "Get MODE from AC for force config"); + switch_climate_mode = mode; + switch_preset = preset.value(); + switch_fan_mode = fan_mode.value(); + switch_swing_mode = swing_mode; + target_temperature_set = 31-(int)target_temperature; + } + + // Включаем или отключаем пищалку в зависимости от переключателя в настройка + + 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_) && (switch_climate_mode != climate::CLIMATE_MODE_OFF)){ + ESP_LOGD("TCL", "Dispaly turn ON"); + dataTX[7] += 0b01000000; + } else { + ESP_LOGD("TCL", "Dispaly turn OFF"); + dataTX[7] += 0b00000000; + } + + // Настраиваем режим работы кондиционера + switch (switch_climate_mode) { + 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; + } + + // Настраиваем режим вентилятора + switch(switch_fan_mode) { + 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; + } + + // Устанавливаем режим качания заслонок + switch(switch_swing_mode) { + 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(switch_preset) { + case ClimatePreset::CLIMATE_PRESET_NONE: + break; + case ClimatePreset::CLIMATE_PRESET_ECO: + dataTX[7] += 0b10000000; + break; + case ClimatePreset::CLIMATE_PRESET_SLEEP: + dataTX[19] += 0b00000001; + break; + case ClimatePreset::CLIMATE_PRESET_COMFORT: + dataTX[8] += 0b00010000; + break; + } + + //Режим заслонок + // Вертикальная заслонка + // Качание вертикальной заслонки [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 - фиксация справа + + + // Устанавливаем режим для качания вертикальной заслонки + 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(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(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; + } + + // Установка температуры + dataTX[9] = target_temperature_set; + + // Собираем массив байт для отправки в кондиционер + 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)); + allow_take_control = false; + is_call_control = false; +} + +// Отправка данны + в кондиционер +void tclacClimate::sendData(uint8_t * message, uint8_t 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); +} + +// Преобразование байта в читабельный формат +std::string tclacClimate::getHex(const byte *message, size_t size) { + std::ostringstream oss; + for (size_t i = 0; i < size; ++i) { + oss << std::hex + << std::uppercase + << std::setw(2) + << std::setfill('0') + << static_cast(message[i]); + if (i + 1 < size) + oss << ' '; + } + std::string s = oss.str(); + std::transform(s.begin(), s.end(), s.begin(), ::toupper); + return s; +} + +// Вычисление контрольной суммы +uint8_t tclacClimate::getChecksum(const byte * message, size_t size) { + uint8_t position = size - 1; + uint8_t 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_beeper_state(bool state) { + this->beeper_status_ = state; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение состояния дисплея кондиционера +void tclacClimate::set_display_state(bool state) { + this->display_status_ = state; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение состояния режима принудительного применения настроек +void tclacClimate::set_force_mode_state(bool state) { + this->force_mode_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; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение режима фиксации горизонтальны + заслонок +void tclacClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { + this->horizontal_direction_ = direction; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение режима качания вертикальной заслонки +void tclacClimate::set_vertical_swing_direction(VerticalSwingDirection direction) { + this->vertical_swing_direction_ = direction; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение доступны + режимов работы кондиционера +void tclacClimate::set_supported_modes(const std::set &modes) { + this->supported_modes_ = modes; +} +// Получение режима качания горизонтальны + заслонок +void tclacClimate::set_horizontal_swing_direction(HorizontalSwingDirection direction) { + horizontal_swing_direction_ = direction; + if (force_mode_status_){ + if (allow_take_control){ + tclacClimate::takeControl(); + } + } +} +// Получение доступны + скоростей вентилятора +void tclacClimate::set_supported_fan_modes(const std::set &modes){ + this->supported_fan_modes_ = modes; +} +// Получение доступны + режимов качания заслонок +void tclacClimate::set_supported_swing_modes(const std::set &modes) { + this->supported_swing_modes_ = modes; +} +// Получение доступны + предустановок +void tclacClimate::set_supported_presets(const std::set &presets) { + this->supported_presets_ = presets; +} + +} +} diff --git a/esphome_components/components/tclac/tclac.h b/esphome_components/components/tclac/tclac.h new file mode 100644 index 0000000..b703448 --- /dev/null +++ b/esphome_components/components/tclac/tclac.h @@ -0,0 +1,164 @@ +/** +* Create by Miguel Ángel López on 20/07/19 +* and modify by xaxexa +* Refactoring & component making: +* Соловей с паяльником 15.03.2024 +**/ + +#ifndef TCL_ESP_TCL_H +#define TCL_ESP_TCL_H + +#include "esphome.h" +#include "esphome/core/defines.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/climate/climate.h" +#include +#include + +namespace esphome { +namespace tclac { + +#if !defined(ESPHOME_TCLAC_BYTE_TYPE_DEFINED) +using byte = uint8_t; +#define ESPHOME_TCLAC_BYTE_TYPE_DEFINED +#endif + + +#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 + +using climate::ClimateCall; +using climate::ClimateMode; +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateFanMode; +using climate::ClimateSwingMode; + +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, +}; +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, +}; + +class tclacClimate : public climate::Climate, public esphome::uart::UARTDevice, public PollingComponent { + + private: + uint8_t checksum; + // dataTX с управлением состоит из 38 байт + uint8_t dataTX[38]; + // А dataRX по прежнему из 61 байта + uint8_t dataRX[61]; + // Команда запроса состояния + uint8_t poll_message_[8] = {0xBB,0x00,0x01,0x04,0x02,0x01,0x00,0xBD}; + // Инициализация и начальное наполнение переменных состоянй переключателей + bool beeper_status_; + bool display_status_; + bool force_mode_status_; + uint8_t switch_preset = 0; + bool module_display_status_; + uint8_t switch_fan_mode = 0; + bool is_call_control = false; + uint8_t switch_swing_mode = 0; + int target_temperature_set = 0; + uint8_t switch_climate_mode = 0; + bool allow_take_control = false; + + esphome::climate::ClimateTraits traits_; + + public: + + tclacClimate() : PollingComponent(5 * 1000), checksum(0) { + } + + void readData(); + void takeControl(); + void loop() override; + void setup() override; + void update() override; + void set_beeper_state(bool state); + void set_display_state(bool state); + void dataShow(bool flow, bool shine); + void set_force_mode_state(bool state); + void set_rx_led_pin(GPIOPin *rx_led_pin); + void set_tx_led_pin(GPIOPin *tx_led_pin); + void sendData(uint8_t * message, uint8_t size); + void set_module_display_state(bool state); + static std::string getHex(const byte *message, size_t size); + void control(const ClimateCall &call) override; + static uint8_t getChecksum(const byte * message, size_t size); + 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_presets(const std::set &presets); + void set_supported_modes(const std::set &modes); + void set_supported_fan_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; + std::set supported_modes_{}; + std::set supported_presets_{}; + AirflowVerticalDirection vertical_direction_; + std::set supported_fan_modes_{}; + 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..2aaa9c7 --- /dev/null +++ b/packages/core.yaml @@ -0,0 +1,312 @@ +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # NICHT BEARBEITEN!! # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +# Компонент климата +external_components: + - source: + url: https://github.com/christoph5180/tclac.git + type: git + ref: master + components: [ tclac ] + refresh: 30s + +# Конфигурация ESPHome +esphome: + name: ${device_name} + min_version: 2023.3.0 + friendly_name: ${humanly_name} + on_boot: + priority: -100 + then: + lambda: !lambda |- + id(${device_name}climate).set_beeper_state(false); + id(${device_name}climate).set_force_mode_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() == 0){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::UP_DOWN); + } else if (id(vswing).active_index() == 1){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::UPSIDE); + } else if (id(vswing).active_index() == 2){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::DOWNSIDE); + } + + if (id(hswing).active_index() == 0){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::LEFT_RIGHT); + } else if (id(hswing).active_index() == 1){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::LEFTSIDE); + } else if (id(hswing).active_index() == 2){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::CENTER); + } else if (id(hswing).active_index() == 3){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::RIGHTSIDE); + } + + if (id(vfixing).active_index() == 0){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::LAST); + } else if (id(vfixing).active_index() == 1){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::MAX_UP); + } else if (id(vfixing).active_index() == 2){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::UP); + } else if (id(vfixing).active_index() == 3){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::CENTER); + } else if (id(vfixing).active_index() == 4){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::DOWN); + } else if (id(vfixing).active_index() == 5){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::MAX_DOWN); + } + + if (id(hfixing).active_index() == 0){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::LAST); + } else if (id(hfixing).active_index() == 1){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::MAX_LEFT); + } else if (id(hfixing).active_index() == 2){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::LEFT); + } else if (id(hfixing).active_index() == 3){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::CENTER); + } else if (id(hfixing).active_index() == 4){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::RIGHT); + } else if (id(hfixing).active_index() == 5){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::MAX_RIGHT); + } + if (id(beep_mode).state){ + id(${device_name}climate).set_beeper_state(true); + } else { + id(${device_name}climate).set_beeper_state(false); + } + if (id(force_mode).state){ + id(${device_name}climate).set_force_mode_state(true); + } else { + id(${device_name}climate).set_force_mode_state(false); + } + +uart: + baud_rate: 9600 + data_bits: 8 + parity: EVEN + stop_bits: 1 + rx_pin: ${uart_rx} + tx_pin: ${uart_tx} + +api: + encryption: + key: "${api_key}" + +ota: +- platform: esphome + password: "${ota_pass}" + +wifi: + id: "${device_name}_wifi" + ssid: ${wifi_ssid} + password: ${wifi_password} + ap: + ssid: "${device_name} Fallback Hotspot" + password: "${recovery_pass}" + +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: Display on module + 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); + + - name: Force config + platform: template + device_class: switch + id: force_mode + entity_category: config + restore_mode: RESTORE_DEFAULT_ON + optimistic: true + on_turn_on: + then: + lambda: !lambda |- + id(${device_name}climate).set_force_mode_state(true); + on_turn_off: + then: + lambda: !lambda |- + id(${device_name}climate).set_force_mode_state(false); + +# - Выпадающие списки +select: + # Настройка вертикального качания + - platform: template + name: Vertical swing + id: vswing + entity_category: config + options: + - "Von oben nach unten" + - "In der oberen Hälfte" + - "In der unteren Hälfte" + optimistic: true + restore_value: true + on_value: + then: + lambda: !lambda |- + if (id(vswing).active_index() == 0){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::UP_DOWN); + } else if (id(vswing).active_index() == 1){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::UPSIDE); + } else if (id(vswing).active_index() == 2){ + id(${device_name}climate).set_vertical_swing_direction(esphome::tclac::VerticalSwingDirection::DOWNSIDE); + } + + # Настройка горизонтального качания + - platform: template + name: Horizontal swing + id: hswing + entity_category: config + options: + - "Von links nach rechts" + - "Im linken Bereich" + - "Im Zentrum" + - "Im rechten Bereich" + optimistic: true + restore_value: true + on_value: + then: + lambda: !lambda |- + if (id(hswing).active_index() == 0){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::LEFT_RIGHT); + } else if (id(hswing).active_index() == 1){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::LEFTSIDE); + } else if (id(hswing).active_index() == 2){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::CENTER); + } else if (id(hswing).active_index() == 3){ + id(${device_name}climate).set_horizontal_swing_direction(esphome::tclac::HorizontalSwingDirection::RIGHTSIDE); + } + + # Настройка фиксации вертикальной заслонки + - platform: template + name: Vertical fixing + id: vfixing + entity_category: config + options: + - "Letzte Position" + - "Ganz nach oben" + - "In der oberen Hälfte" + - "In der Mitte" + - "In der unteren Hälfte" + - "Ganz nach unten" + optimistic: true + restore_value: true + on_value: + then: + lambda: !lambda |- + if (id(vfixing).active_index() == 0){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::LAST); + } else if (id(vfixing).active_index() == 1){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::MAX_UP); + } else if (id(vfixing).active_index() == 2){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::UP); + } else if (id(vfixing).active_index() == 3){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::CENTER); + } else if (id(vfixing).active_index() == 4){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::DOWN); + } else if (id(vfixing).active_index() == 5){ + id(${device_name}climate).set_vertical_airflow(esphome::tclac::AirflowVerticalDirection::MAX_DOWN); + } + + # Настройка фиксации горизонатальных заслонок + - platform: template + name: Horizontal fixing + id: hfixing + entity_category: config + options: + - "Letzte Position" + - "Ganz nach links" + - "In der linken Hälfte" + - "In der Mitte" + - "In der rechten Hälfte" + - "Ganz nach rechts" + optimistic: true + restore_value: true + on_value: + then: + lambda: !lambda |- + if (id(hfixing).active_index() == 0){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::LAST); + } else if (id(hfixing).active_index() == 1){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::MAX_LEFT); + } else if (id(hfixing).active_index() == 2){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::LEFT); + } else if (id(hfixing).active_index() == 3){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::CENTER); + } else if (id(hfixing).active_index() == 4){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::RIGHT); + } else if (id(hfixing).active_index() == 5){ + id(${device_name}climate).set_horizontal_airflow(esphome::tclac::AirflowHorizontalDirection::MAX_RIGHT); + } + +climate: + - platform: tclac + name: "${device_name} Climate" + id: ${device_name}climate 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..cb124bb --- /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: 20s + 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: 0 + 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), "Готов!"); + } \ No newline at end of file