From aceec8dce6851c5b70e0200d5557afc3d12d888e Mon Sep 17 00:00:00 2001 From: GrKoR Date: Sun, 22 May 2022 23:53:03 +0300 Subject: [PATCH] new send packet action. It for engineers only --- components/aux_ac/automation.h | 34 ++++- components/aux_ac/aux_ac.h | 88 +++++++++++- components/aux_ac/climate.py | 53 ++++++- tests/ac_send_packet_for_engineer.py | 134 ++++++++++++++++++ ...st-ext.yaml => test-ext-for-engineer.yaml} | 18 +++ 5 files changed, 321 insertions(+), 6 deletions(-) create mode 100644 tests/ac_send_packet_for_engineer.py rename tests/{test-ext.yaml => test-ext-for-engineer.yaml} (75%) diff --git a/components/aux_ac/automation.h b/components/aux_ac/automation.h index d8d9565..a62fb27 100644 --- a/components/aux_ac/automation.h +++ b/components/aux_ac/automation.h @@ -17,7 +17,7 @@ namespace aux_ac { protected: AirCon *ac_; - }; + }; template class AirConDisplayOnAction : public Action @@ -29,7 +29,37 @@ namespace aux_ac { protected: AirCon *ac_; - }; + }; + + template + class AirConSendTestPacketAction : public Action + { + public: + explicit AirConSendTestPacketAction(AirCon *ac) : ac_(ac) {} + void set_data_template(std::function(Ts...)> func) { + this->data_func_ = func; + this->static_ = false; + } + void set_data_static(const std::vector &data) { + this->data_static_ = data; + this->static_ = true; + } + + void play(Ts... x) override { + if (this->static_) { + this->ac_->sendTestPacket(this->data_static_); + } else { + auto val = this->data_func_(x...); + this->ac_->sendTestPacket(val); + } + } + + protected: + AirCon *ac_; + bool static_{false}; + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; + }; } // namespace aux_ac } // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/aux_ac.h b/components/aux_ac/aux_ac.h index 935cd58..fcb3fd2 100644 --- a/components/aux_ac/aux_ac.h +++ b/components/aux_ac/aux_ac.h @@ -513,6 +513,9 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { packet_t _inPacket; packet_t _outPacket; + // пакет для тестирования всякой фигни + packet_t _outTestPacket; + // последовательность пакетов текущий шаг в последовательности sequence_item_t _sequence[AC_SEQUENCE_MAX_LEN]; uint8_t _sequence_current_step; @@ -1576,6 +1579,24 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { return relevant; } + // отправка запроса с тестовым пакетом + bool sq_requestTestPacket(){ + // если исходящий пакет не пуст, то выходим и ждем освобождения + if (_outPacket.bytesLoaded > 0) return true; + + _copyPacket(&_outPacket, &_outTestPacket); + _copyPacket(&_sequence[_sequence_current_step].packet, &_outTestPacket); + _sequence[_sequence_current_step].packet_type = AC_SPT_SENT_PACKET; + + // Отчитываемся в лог + _debugMsg(F("Sequence [step %u]: Test Packet request generated:"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); + _debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); + + // увеличиваем текущий шаг + _sequence_current_step++; + return true; + } + // сенсоры, отображающие параметры сплита //esphome::sensor::Sensor *sensor_indoor_temperature = new esphome::sensor::Sensor(); esphome::sensor::Sensor *sensor_indoor_temperature_ = nullptr; @@ -1613,6 +1634,10 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { _clearInPacket(); _clearOutPacket(); + _clearPacket(&_outTestPacket); + _outTestPacket.header->start_byte = AC_PACKET_START_BYTE; + _outTestPacket.header->wifi = AC_PACKET_ANSWER; + _setStateMachineState(ACSM_IDLE); _ac_serial = parent; _hw_initialized = (_ac_serial != nullptr); @@ -2435,6 +2460,67 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { return _displaySequence(dsp); } + // отправляет сплиту заданный набор байт + // Перед отправкой проверяет пакет на корректность структуры. CRC16 рассчитывает самостоятельно и перезаписывает. + bool sendTestPacket(const std::vector &data){ + //bool sendTestPacket(uint8_t *data = nullptr, uitn8_t data_length = 0){ + //if (data == nullptr) return false; + //if (data_length == 0) return false; + if (data.size() == 0) return false; + //if (data_length > AC_BUFFER_SIZE) return false; + if (data.size() > AC_BUFFER_SIZE) return false; + + // нет смысла в отправке, если нет коннекта с кондиционером + if (!get_has_connection()) { + _debugMsg(F("sendTestPacket: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); + return false; + } + // очищаем пакет + _clearPacket(&_outTestPacket); + + // копируем данные в пакет + //memcpy(_outTestPacket.data, data, data_length); + uint8_t i = 0; + for (uint8_t n : data) { + _outTestPacket.data[i] = n; + i++; + } + + // на всякий случай указываем правильные некоторые байты + _outTestPacket.header->start_byte = AC_PACKET_START_BYTE; + //_outTestPacket.header->wifi = AC_PACKET_ANSWER; + + _outTestPacket.msec = millis(); + _outTestPacket.body = &(_outTestPacket.data[AC_HEADER_SIZE]); + _outTestPacket.bytesLoaded = AC_HEADER_SIZE + _outTestPacket.header->body_length + 2; + + // рассчитываем и записываем в пакет CRC + _outTestPacket.crc = (packet_crc_t *) &(_outTestPacket.data[AC_HEADER_SIZE + _outTestPacket.header->body_length]); + _setCRC16(&_outTestPacket); + + _debugMsg(F("sendTestPacket: test packet loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__); + _debugPrintPacket(&_outTestPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__); + + // ниже блок добавления отправки пакета в последовательность команд + //***************************************************************** + // есть ли место на запрос в последовательности команд? + if (_getFreeSequenceSpace() < 1) { + _debugMsg(F("sendTestPacket: not enough space in command sequence. Sequence steps doesn't loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__); + return false; + } + + /*************************************** sendTestPacket request ***********************************************/ + if (!_addSequenceFuncStep(&AirCon::sq_requestTestPacket)) { + _debugMsg(F("sendTestPacket: sendTestPacket request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); + return false; + } + /**************************************************************************************/ + + _debugMsg(F("sendTestPacket: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); + + return true; + } + void set_period(uint32_t ms) { this->_update_period = ms; } uint32_t get_period() { return this->_update_period; } @@ -2485,7 +2571,7 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // обычный wifi-модуль запрашивает маленький пакет статуса // но нам никто не мешает запрашивать и большой и маленький, чтобы чаще обновлять комнатную температуру - // делаем этот запросом только в случае, если есть коннект с кондиционером + // делаем этот запрос только в случае, если есть коннект с кондиционером if (get_has_connection()) getStatusBigAndSmall(); } diff --git a/components/aux_ac/climate.py b/components/aux_ac/climate.py index 532b08a..c6ba1d1 100644 --- a/components/aux_ac/climate.py +++ b/components/aux_ac/climate.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_CUSTOM_FAN_MODES, CONF_CUSTOM_PRESETS, CONF_INTERNAL, + CONF_DATA, UNIT_CELSIUS, ICON_THERMOMETER, DEVICE_CLASS_TEMPERATURE, @@ -44,6 +45,7 @@ Capabilities = aux_ac_ns.namespace("Constants") AirConDisplayOffAction = aux_ac_ns.class_("AirConDisplayOffAction", automation.Action) AirConDisplayOnAction = aux_ac_ns.class_("AirConDisplayOnAction", automation.Action) +AirConSendTestPacketAction = aux_ac_ns.class_("AirConSendTestPacketAction", automation.Action) ALLOWED_CLIMATE_MODES = { "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, @@ -80,6 +82,15 @@ CUSTOM_PRESETS = { } validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True) + +def validate_raw_data(value): + if isinstance(value, list): + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid( + "data must be a list of bytes" + ) + + def output_info(config): """_LOGGER.info(config)""" return config @@ -164,11 +175,47 @@ DISPLAY_ACTION_SCHEMA = maybe_simple_id( ) @automation.register_action("aux_ac.display_off", AirConDisplayOffAction, DISPLAY_ACTION_SCHEMA) -async def switch_toggle_to_code(config, action_id, template_arg, args): +async def display_off_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(action_id, template_arg, paren) @automation.register_action("aux_ac.display_on", AirConDisplayOnAction, DISPLAY_ACTION_SCHEMA) -async def switch_toggle_to_code(config, action_id, template_arg, args): +async def display_on_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) \ No newline at end of file + return cg.new_Pvariable(action_id, template_arg, paren) + + +SEND_TEST_PACKET_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(AirCon), + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + } +) + + +# ********************************************************************************************************* +# ВАЖНО! Только для инженеров! +# Вызывайте метод aux_ac.send_packet только если понимаете, что делаете! Он не проверяет данные, а передаёт +# кондиционеру всё как есть. Какой эффект получится от передачи кондиционеру рандомных байт, никто не знает. +# Вы действуете на свой страх и риск. +# ********************************************************************************************************* +@automation.register_action( + "aux_ac.send_packet", + AirConSendTestPacketAction, + SEND_TEST_PACKET_ACTION_SCHEMA +) +async def send_packet_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) + + data = config[CONF_DATA] + if isinstance(data, bytes): + data = list(data) + + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + cg.add(var.set_data_static(data)) + + return var \ No newline at end of file diff --git a/tests/ac_send_packet_for_engineer.py b/tests/ac_send_packet_for_engineer.py new file mode 100644 index 0000000..ccc1571 --- /dev/null +++ b/tests/ac_send_packet_for_engineer.py @@ -0,0 +1,134 @@ +import time +import aioesphomeapi +import asyncio +import re +import sys +import argparse +from aioesphomeapi.api_pb2 import (LOG_LEVEL_NONE, + LOG_LEVEL_ERROR, + LOG_LEVEL_WARN, + LOG_LEVEL_INFO, + LOG_LEVEL_DEBUG, + LOG_LEVEL_VERBOSE, + LOG_LEVEL_VERY_VERBOSE) + +def createParser (): + parser = argparse.ArgumentParser( + description='''This script is used for collecting logs from ac_aux ESPHome component. + For more info, see https://github.com/GrKoR/ac_python_logger''', + add_help = False) + parent_group = parser.add_argument_group (title='Params') + parent_group.add_argument ('--help', '-h', action='help', help='show this help message and exit') + parent_group.add_argument ('-i', '--ip', nargs=1, required=True, help='IP address of the esphome device') + parent_group.add_argument ('-p', '--pwd', nargs=1, required=True, help='native API password for the esphome device') + parent_group.add_argument ('-n', '--name', nargs=1, default=['noname'], help='name of this devace in the log') + parent_group.add_argument ('-l', '--logfile', nargs=1, default=['%4d-%02d-%02d %02d-%02d-%02d log.csv' % time.localtime()[0:6]], help='log file name') + return parser + +async def main(): + """Connect to an ESPHome device and wait for state changes.""" + api = aioesphomeapi.APIClient(namespace.ip[0], 6053, namespace.pwd[0]) + + try: + await api.connect(login=True) + except aioesphomeapi.InvalidAuthAPIError as e: + return print(e) + + print(api.api_version) + + def log_AC(isAirConLog): + parts = re.search("(\d{10}): (\[\S{2}\]) \[([0-9A-F ]{23})\]\s?((?:[0-9A-F]{2}\s*)*) \[([0-9A-F ]{5})\]", isAirConLog.group(1)) + packString = '\n' + namespace.name[0] + packString += ";" + "%4d-%02d-%02d %02d:%02d:%02d" % time.localtime()[0:6] + """millis of message""" + packString += ";" + parts.group(1) + """direction""" + packString += ";" + parts.group(2) + """header""" + packString += ";" + ';'.join(parts.group(3).split(" ")) + """body (may be void)""" + if len(parts.group(4)) > 0: + packString += ";" + ';'.join(parts.group(4).split(" ")) + """crc""" + packString += ";" + ';'.join(parts.group(5).split(" ")) + print(packString) + with open(namespace.logfile[0], 'a+') as file: + file.write( packString ) + + def log_Dallas(isDallasLog): + parts = re.search("'([\w ]+)': Got Temperature=([-]?\d+\.\d+)°C", isDallasLog.group(1)) + packString = '\n' + parts.group(1) + packString += ";" + "%4d-%02d-%02d %02d:%02d:%02d" % time.localtime()[0:6] + """millis of message always empty""" + packString += ";" + """direction""" + packString += ";[<=]" + """additional data flag""" + packString += ";AA" + """dallas temperature""" + packString += ";" + parts.group(2) + print(packString) + with open(namespace.logfile[0], 'a+') as file: + file.write( packString ) + + def log_callback(log): + """Print the log for AirCon""" + isAirConLog = re.search("\[AirCon:\d+\]: (.+\])", log.message.decode('utf-8')) + if isAirConLog: + log_AC(isAirConLog) + if namespace.logdallas: + isDallasLog = re.search("\[dallas.sensor:\d+\]: (.+C)", log.message.decode('utf-8')) + if isDallasLog: + log_Dallas(isDallasLog) + + + # Subscribe to the log + # await api.subscribe_logs(log_callback, LOG_LEVEL_DEBUG) + + # print(await api.device_info()) + print(f"%s" % (await api.list_entities_services(),)) + + # key надо искать в выводе list_entities_services + service = aioesphomeapi.UserService( + name="send_data", + key=311254518, + args=[ + aioesphomeapi.UserServiceArg(name="data_buf", type=aioesphomeapi.UserServiceArgType.INT_ARRAY), + ], + ) + + await api.execute_service( + service, + data={ + # display off + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x7F, 0xE0, 0x00, 0x20, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00], + } + ) + + time.sleep(3) + + await api.execute_service( + service, + data={ + # display on + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x7F, 0xE0, 0x00, 0x20, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + } + ) + +parser = createParser() +namespace = parser.parse_args() +print(namespace.name[0], namespace.ip[0]) + + +loop = asyncio.get_event_loop() +try: + #asyncio.ensure_future(main()) + #loop.run_forever() + loop.run_until_complete(main()) +except aioesphomeapi.InvalidAuthAPIError as e: + print(e) +except KeyboardInterrupt: + pass +finally: + loop.close() + pass \ No newline at end of file diff --git a/tests/test-ext.yaml b/tests/test-ext-for-engineer.yaml similarity index 75% rename from tests/test-ext.yaml rename to tests/test-ext-for-engineer.yaml index 1211d76..4561a52 100644 --- a/tests/test-ext.yaml +++ b/tests/test-ext-for-engineer.yaml @@ -29,6 +29,24 @@ logger: api: password: !secret api_pass reboot_timeout: 0s + services: + # этот сервис можно вызвать из Home Assistant или Python. Он отправляет полученные байты в кондиционер + - service: send_data + variables: + data_buf: int[] + then: + # ВАЖНО! Только для инженеров! + # Вызывайте метод aux_ac.send_packet только если понимаете, что делаете! Он не проверяет данные, а передаёт + # кондиционеру всё как есть. Какой эффект получится от передачи кондиционеру рандомных байт, никто не знает. + # Вы действуете на свой страх и риск. + - aux_ac.send_packet: + id: aux_id + data: !lambda |- + std::vector data{}; + for (int n : data_buf) { + data.push_back( (uint8_t) n ); + } + return data; ota: password: !secret ota_pass