From 577eac89c8e4c53b193687bc7ba41330e09547d1 Mon Sep 17 00:00:00 2001 From: GrKoR Date: Tue, 25 Nov 2025 18:59:00 -0800 Subject: [PATCH] unfinished --- .gitignore | 1 + components/aux_ac/aux_frame.h | 148 +++++++++++++++++++++++++++++++ components/aux_ac/aux_logger.cpp | 100 +++++++++++++++++++++ components/aux_ac/aux_logger.h | 15 ++++ components/aux_ac/aux_uart.cpp | 68 ++++++++++++++ components/aux_ac/aux_uart.h | 30 +++++++ components/aux_ac/climate.py | 122 +++++++++++++++++++++++++ 7 files changed, 484 insertions(+) create mode 100644 components/aux_ac/aux_frame.h create mode 100644 components/aux_ac/aux_logger.cpp create mode 100644 components/aux_ac/aux_logger.h create mode 100644 components/aux_ac/aux_uart.cpp create mode 100644 components/aux_ac/aux_uart.h diff --git a/.gitignore b/.gitignore index 0140cd9..009cfef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # You can modify this file to suit your needs. **/.vscode/ **/.esphome/ +**/esphome/ **/.pioenvs/ **/.piolibdeps/ **/lib/ diff --git a/components/aux_ac/aux_frame.h b/components/aux_ac/aux_frame.h new file mode 100644 index 0000000..cafdd05 --- /dev/null +++ b/components/aux_ac/aux_frame.h @@ -0,0 +1,148 @@ +#pragma once + +#include +#include // for memcpy and memset, move to .cpp later if needed + +namespace esphome +{ + namespace aux_ac + { + + class AuxFrame + { + private: + // CRC of the AUX frame + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_crc + union frame_crc_t + { + uint16_t crc16; + uint8_t crc[2]; + }; + + // frame header + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_header + struct frame_header_t + { + uint8_t startByte; + uint8_t _unknown1; + uint8_t frameType; + uint8_t wifi; + uint8_t pingAnswer; + uint8_t _unknown2; + uint8_t bodyLength; + uint8_t _unknown3; + }; + + uint8_t *_rawData = nullptr; + frame_header_t *_header = nullptr; + uint8_t *_body = nullptr; + frame_crc_t *_crc = nullptr; + + bool _isValid = false; + + uint8_t const AC_PACKET_START_BYTE = 0xBB; + uint8_t const AC_HEADER_SIZE = 8; + uint8_t const AC_BODY_LENGTH_OFFSET = 6; + + uint8_t const AC_BUFFER_SIZE = 35; // TODO: integrate it with aux_uart.h + + uint16_t _CRC16(uint8_t *data, uint8_t len) + { + uint32_t crc = 0; + + uint8_t _crcBuffer[AC_BUFFER_SIZE]; + memset(_crcBuffer, 0, AC_BUFFER_SIZE); + memcpy(_crcBuffer, data, len); + + if ((len % 2) == 1) + len++; + + uint32_t word = 0; + for (uint8_t i = 0; i < len; i += 2) + { + word = (_crcBuffer[i] << 8) + _crcBuffer[i + 1]; + crc += word; + } + crc = (crc >> 16) + (crc & 0xFFFF); + crc = ~crc; + + return crc & 0xFFFF; + } + + bool _checkCRC() + { + frame_crc_t crc; + crc.crc16 = _CRC16(this->_rawData, AC_HEADER_SIZE + this->_rawData[AC_BODY_LENGTH_OFFSET]); + + return ((this->_crc->crc[0] == crc.crc[1]) && (this->_crc->crc[1] == crc.crc[0])); + } + + void _checkFrame() + { + this->_isValid = false; + if (this->_rawData == nullptr) + return; + + if (this->_header->startByte != AC_PACKET_START_BYTE) + return; + + if (!this->_checkCRC()) + return; + + this->_isValid = true; + } + + public: + AuxFrame() = default; + ~AuxFrame() {}; + + void set_data(uint8_t *data) + { + clearData(); + if (data == nullptr) + return; + + this->_rawData = data; + this->_header = (frame_header_t *)this->_rawData; + this->_crc = (frame_crc_t *)(this->_rawData + AC_HEADER_SIZE + this->_header->bodyLength); + if (this->_header->bodyLength > 0) + this->_body = this->_rawData + AC_HEADER_SIZE; + else + this->_body = nullptr; + this->_checkFrame(); + } + + void clearData() + { + this->_rawData = nullptr; + this->_header = nullptr; + this->_crc = nullptr; + this->_body = nullptr; + this->_isValid = false; + } + + bool isValid() const + { + return this->_isValid; + }; + + uint8_t frameSize() const + { + if (!this->isValid()) + return 0; + + return AC_HEADER_SIZE + this->_header->bodyLength + 2; + } + + uint8_t bodyLength() const + { + if (!this->isValid()) + return 0; + + return this->_header->bodyLength; + } + }; + + } // namespace aux_ac + +} // namespace esphome diff --git a/components/aux_ac/aux_logger.cpp b/components/aux_ac/aux_logger.cpp new file mode 100644 index 0000000..0bf2ad3 --- /dev/null +++ b/components/aux_ac/aux_logger.cpp @@ -0,0 +1,100 @@ +#include "aux_logger.h" + +namespace esphome +{ + namespace aux_ac + { + static const char *const TAG = "AirCon"; // TODO: verify if this tag is appropriate + + /** вывод отладочной информации в лог + * + * dbgLevel - уровень сообщения, определен в ESPHome. За счет его использования можно из ESPHome управлять полнотой сведений в логе. + * msg - сообщение, выводимое в лог + * line - строка, на которой произошел вызов (удобно при отладке) + */ + void debugMsg(const /*String &*/ char *msg, uint8_t dbgLevel, unsigned int line, ...) + { + if (dbgLevel < ESPHOME_LOG_LEVEL_NONE) + dbgLevel = ESPHOME_LOG_LEVEL_NONE; + if (dbgLevel > ESPHOME_LOG_LEVEL_VERY_VERBOSE) + dbgLevel = ESPHOME_LOG_LEVEL_VERY_VERBOSE; + + if (line == 0) + line = __LINE__; // если строка не передана, берем текущую строку + + va_list vl; + va_start(vl, line); + esp_log_vprintf_(dbgLevel, TAG, line, msg, vl); + va_end(vl); + }; + + /** выводим данные пакета в лог для отладки + * + * dbgLevel - уровень сообщения, определен в ESPHome. За счет его использования можно из ESPHome управлять полнотой сведений в логе. + * packet - указатель на пакет для вывода; + * если указатель на crc равен nullptr или первый байт в буфере не AC_PACKET_START_BYTE, то считаем, что передан битый пакет + * или не пакет вовсе. Для такого выводим только массив байт. + * Для нормального пакета данные выводятся с форматированием. + * line - строка, на которой произошел вызов (удобно при отладке) + **/ + void debugPrintPacket(packet_t *packet, uint8_t dbgLevel, unsigned int line) + { + // определяем, полноценный ли пакет нам передан + bool notAPacket = false; + /* + // указатель заголовка всегда установден на начало буфера + notAPacket = notAPacket || (packet->crc == nullptr); + notAPacket = notAPacket || (packet->data[0] != AC_PACKET_START_BYTE); + + String st = ""; + char textBuf[11]; + + // заполняем время получения пакета + memset(textBuf, 0, 11); + sprintf(textBuf, "%010" PRIu32, packet->msec); + st = st + textBuf + ": "; + + // формируем преамбулы + if (packet == &_inPacket) + { + st += "[<=] "; // преамбула входящего пакета + } + else if (packet == &_outPacket) + { + st += "[=>] "; // преамбула исходящего пакета + } + else + { + st += "[--] "; // преамбула для "непакета" + } + + // формируем данные + for (int i = 0; i < packet->bytesLoaded; i++) + { + // для заголовков нормальных пакетов надо отработать скобки (если они есть) + if ((!notAPacket) && (i == 0)) + st += HOLMES_HEADER_BRACKET_OPEN; + // для CRC нормальных пакетов надо отработать скобки (если они есть) + if ((!notAPacket) && (i == packet->header->body_length + AC_HEADER_SIZE)) + st += HOLMES_CRC_BRACKET_OPEN; + + memset(textBuf, 0, 11); + sprintf(textBuf, HOLMES_BYTE_FORMAT, packet->data[i]); + st += textBuf; + + // для заголовков нормальных пакетов надо отработать скобки (если они есть) + if ((!notAPacket) && (i == AC_HEADER_SIZE - 1)) + st += HOLMES_HEADER_BRACKET_CLOSE; + // для CRC нормальных пакетов надо отработать скобки (если они есть) + if ((!notAPacket) && (i == packet->header->body_length + AC_HEADER_SIZE + 2 - 1)) + st += HOLMES_CRC_BRACKET_CLOSE; + + st += HOLMES_DELIMITER; + } + + _debugMsg(st, dbgLevel, line); + */ + } + + } // namespace aux_ac +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/aux_logger.h b/components/aux_ac/aux_logger.h new file mode 100644 index 0000000..b24a289 --- /dev/null +++ b/components/aux_ac/aux_logger.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "esphome/core/log.h" + +namespace esphome +{ + namespace aux_ac + { + using packet_t = uint8_t; // TODO: Replace with actual packet_t definition + + void debugMsg(const char *msg, uint8_t dbgLevel, unsigned int line = 0, ...); + void debugPrintPacket(packet_t *packet, uint8_t dbgLevel = ESPHOME_LOG_LEVEL_DEBUG, unsigned int line = __LINE__); + } // namespace aux_ac +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/aux_uart.cpp b/components/aux_ac/aux_uart.cpp new file mode 100644 index 0000000..dac737c --- /dev/null +++ b/components/aux_ac/aux_uart.cpp @@ -0,0 +1,68 @@ +#include "aux_uart.h" + +namespace esphome +{ + namespace aux_ac + { + void AuxUart::send_frame(const std::vector &frame) + { + this->write_array(frame); + } + + bool AuxUart::read_frame(std::vector &frame) + { + // Check if enough data is available + if (this->available() < 3) + return false; + + // Peek at the first 3 bytes to determine the frame length + uint8_t header[3]; + if (!this->peek_array(header, 3)) + return false; + + // Validate start byte + if (header[0] != 0xAA) + { + // Invalid start byte, discard one byte and return false + uint8_t discard; + this->read_byte(&discard); + return false; + } + + // Determine frame length from the second byte + size_t frame_length = header[1]; + if (frame_length < 3 || frame_length > AC_BUFFER_SIZE) + { + // Invalid length, discard one byte and return false + uint8_t discard; + this->read_byte(&discard); + return false; + } + + // Check if the full frame is available + if (this->available() < frame_length) + return false; + + // Read the full frame into the internal buffer + if (!this->read_array(this->_data, frame_length)) + return false; + + // Validate checksum + uint8_t checksum = 0; + for (size_t i = 0; i < frame_length - 1; i++) + { + checksum += this->_data[i]; + } + if (checksum != this->_data[frame_length - 1]) + { + // Invalid checksum, discard the frame and return false + return false; + } + + // Copy the valid frame to the output vector + frame.assign(this->_data, this->_data + frame_length); + return true; + } + + } // namespace aux_ac +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/aux_uart.h b/components/aux_ac/aux_uart.h new file mode 100644 index 0000000..a0e52fd --- /dev/null +++ b/components/aux_ac/aux_uart.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +#define AC_BUFFER_SIZE 35 + +using namespace esphome::uart; + +namespace esphome +{ + namespace aux_ac + { + class AuxUart : public UARTDevice + { + public: + AuxUart() = delete; + explicit AuxUart(UARTComponent *parent) : UARTDevice(parent) {} + ~AuxUart() = default; + + void send_frame(const std::vector &frame); + bool read_frame(std::vector &frame); + + protected: + // Internal buffer for incoming data + uint8_t _data[AC_BUFFER_SIZE]; + }; + + } // namespace aux_ac +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/climate.py b/components/aux_ac/climate.py index e69de29..9862e88 100644 --- a/components/aux_ac/climate.py +++ b/components/aux_ac/climate.py @@ -0,0 +1,122 @@ +import logging +from esphome.core import CORE, Define +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.components import uart, climate + +from esphome.const import ( + CONF_DATA, + CONF_ID, + CONF_UART_ID, +) + +AUX_AC_FIRMWARE_VERSION = '0.2.17' + +_LOGGER = logging.getLogger(__name__) + +CODEOWNERS = ["@GrKoR"] +DEPENDENCIES = ["uart"] + +aux_ac_ns = cg.esphome_ns.namespace("aux_ac") + +AuxUart = aux_ac_ns.class_("AuxUart", uart.UARTDevice) + +def output_info(config): + _LOGGER.info("AUX_AC firmware version: %s", AUX_AC_FIRMWARE_VERSION) + return config + +CONFIG_SCHEMA = cv.All( + uart.UART_DEVICE_SCHEMA + .extend( + { + cv.GenerateID(): cv.declare_id(AuxUart), + } + ), + output_info, +) + +async def to_code(config): + CORE.add_define( + Define("AUX_AC_FIRMWARE_VERSION", '"'+AUX_AC_FIRMWARE_VERSION+'"') + ) + var = cg.new_Pvariable(config[CONF_ID]) + # await cg.register_component(var, config) + + parent = await cg.get_variable(config[CONF_UART_ID]) + cg.add(var.initAC(parent)) + + if CONF_INDOOR_TEMPERATURE in config: + conf = config[CONF_INDOOR_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_indoor_temperature_sensor(sens)) + + if CONF_OUTDOOR_TEMPERATURE in config: + conf = config[CONF_OUTDOOR_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_outdoor_temperature_sensor(sens)) + + if CONF_OUTBOUND_TEMPERATURE in config: + conf = config[CONF_OUTBOUND_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_outbound_temperature_sensor(sens)) + + if CONF_INBOUND_TEMPERATURE in config: + conf = config[CONF_INBOUND_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_inbound_temperature_sensor(sens)) + + if CONF_COMPRESSOR_TEMPERATURE in config: + conf = config[CONF_COMPRESSOR_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_compressor_temperature_sensor(sens)) + + if CONF_VLOUVER_STATE in config: + conf = config[CONF_VLOUVER_STATE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_vlouver_state_sensor(sens)) + + if CONF_DISPLAY_STATE in config: + conf = config[CONF_DISPLAY_STATE] + sens = await binary_sensor.new_binary_sensor(conf) + cg.add(var.set_display_sensor(sens)) + + if CONF_DEFROST_STATE in config: + conf = config[CONF_DEFROST_STATE] + sens = await binary_sensor.new_binary_sensor(conf) + cg.add(var.set_defrost_state(sens)) + + if CONF_INVERTER_POWER in config: + conf = config[CONF_INVERTER_POWER] + sens = await sensor.new_sensor(conf) + cg.add(var.set_inverter_power_sensor(sens)) + + if CONF_PRESET_REPORTER in config: + conf = config[CONF_PRESET_REPORTER] + sens = await text_sensor.new_text_sensor(conf) + cg.add(var.set_preset_reporter_sensor(sens)) + + if CONF_INVERTER_POWER_LIMIT_VALUE in config: + conf = config[CONF_INVERTER_POWER_LIMIT_VALUE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_inverter_power_limit_value_sensor(sens)) + + if CONF_INVERTER_POWER_LIMIT_STATE in config: + conf = config[CONF_INVERTER_POWER_LIMIT_STATE] + sens = await binary_sensor.new_binary_sensor(conf) + cg.add(var.set_inverter_power_limit_state_sensor(sens)) + + cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds)) + cg.add(var.set_show_action(config[CONF_SHOW_ACTION])) + cg.add(var.set_display_inverted(config[CONF_DISPLAY_INVERTED])) + cg.add(var.set_packet_timeout(config[CONF_TIMEOUT])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + if CONF_SUPPORTED_SWING_MODES in config: + cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + if CONF_SUPPORTED_PRESETS in config: + cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) + if CONF_CUSTOM_PRESETS in config: + cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS])) + if CONF_CUSTOM_FAN_MODES in config: + cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))