From b975caef1e960d50ba12045b10a224d85faf852b Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sat, 19 Oct 2019 12:47:24 -0700 Subject: [PATCH] Add new component for Tuya dimmers (#743) * Add new component for Tuya dimmers * Update code * Class naming * Log output * Fixes * Lint * Format * Fix test * log setting datapoint values * remove in_setup_ and fix datapoint handling Co-authored-by: Samuel Sieb Co-authored-by: Otto Winter --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 23 +- esphome/components/tuya/__init__.py | 20 ++ esphome/components/tuya/light/__init__.py | 38 +++ esphome/components/tuya/light/tuya_light.cpp | 85 +++++ esphome/components/tuya/light/tuya_light.h | 36 +++ esphome/components/tuya/tuya.cpp | 294 ++++++++++++++++++ esphome/components/tuya/tuya.h | 73 +++++ esphome/core/helpers.cpp | 16 + esphome/core/helpers.h | 3 + 9 files changed, 571 insertions(+), 17 deletions(-) create mode 100644 esphome/components/tuya/__init__.py create mode 100644 esphome/components/tuya/light/__init__.py create mode 100644 esphome/components/tuya/light/tuya_light.cpp create mode 100644 esphome/components/tuya/light/tuya_light.h create mode 100644 esphome/components/tuya/tuya.cpp create mode 100644 esphome/components/tuya/tuya.h diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index ff1fe59668..c578acdaea 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -1,6 +1,7 @@ #include "esp32_ble_tracker.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #ifdef ARDUINO_ARCH_ESP32 @@ -202,20 +203,8 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res } } -std::string hexencode(const std::string &raw_data) { - char buf[20]; - std::string res; - for (size_t i = 0; i < raw_data.size(); i++) { - if (i + 1 != raw_data.size()) { - sprintf(buf, "0x%02X.", static_cast(raw_data[i])); - } else { - sprintf(buf, "0x%02X ", static_cast(raw_data[i])); - } - res += buf; - } - sprintf(buf, "(%zu)", raw_data.size()); - res += buf; - return res; +std::string hexencode_string(const std::string &raw_data) { + return hexencode(reinterpret_cast(raw_data.c_str()), raw_data.size()); } ESPBTUUID::ESPBTUUID() : uuid_() {} @@ -327,15 +316,15 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e for (auto uuid : this->service_uuids_) { ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } - ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(this->manufacturer_data_).c_str()); - ESP_LOGVV(TAG, " Service data: %s", hexencode(this->service_data_).c_str()); + ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode_string(this->manufacturer_data_).c_str()); + ESP_LOGVV(TAG, " Service data: %s", hexencode_string(this->service_data_).c_str()); if (this->service_data_uuid_.has_value()) { ESP_LOGVV(TAG, " Service Data UUID: %s", this->service_data_uuid_->to_string().c_str()); } ESP_LOGVV(TAG, "Adv data: %s", - hexencode(std::string(reinterpret_cast(param.ble_adv), param.adv_data_len)).c_str()); + hexencode_string(std::string(reinterpret_cast(param.ble_adv), param.adv_data_len)).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py new file mode 100644 index 0000000000..541f10f862 --- /dev/null +++ b/esphome/components/tuya/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +DEPENDENCIES = ['uart'] + +tuya_ns = cg.esphome_ns.namespace('tuya') +Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice) + +CONF_TUYA_ID = 'tuya_id' +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Tuya), +}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py new file mode 100644 index 0000000000..ec588d6cc9 --- /dev/null +++ b/esphome/components/tuya/light/__init__.py @@ -0,0 +1,38 @@ +from esphome.components import light +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_DIMMER_DATAPOINT = "dimmer_datapoint" +CONF_SWITCH_DATAPOINT = "switch_datapoint" + +TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component) + +CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_DIMMER_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_MIN_VALUE): cv.int_, + cv.Optional(CONF_MAX_VALUE): cv.int_, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + yield cg.register_component(var, config) + yield light.register_light(var, config) + + if CONF_DIMMER_DATAPOINT in config: + cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT])) + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_MIN_VALUE in config: + cg.add(var.set_min_value(config[CONF_MIN_VALUE])) + if CONF_MAX_VALUE in config: + cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp new file mode 100644 index 0000000000..9696252049 --- /dev/null +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -0,0 +1,85 @@ +#include "esphome/core/log.h" +#include "tuya_light.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.light"; + +void TuyaLight::setup() { + if (this->dimmer_id_.has_value()) { + this->parent_->register_listener(*this->dimmer_id_, [this](TuyaDatapoint datapoint) { + auto call = this->state_->make_call(); + call.set_brightness(float(datapoint.value_uint) / this->max_value_); + call.perform(); + }); + } + if (switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + auto call = this->state_->make_call(); + call.set_state(datapoint.value_bool); + call.perform(); + }); + } +} + +void TuyaLight::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Dimmer:"); + if (this->dimmer_id_.has_value()) + ESP_LOGCONFIG(TAG, " Dimmer has datapoint ID %u", *this->dimmer_id_); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); +} + +light::LightTraits TuyaLight::get_traits() { + auto traits = light::LightTraits(); + traits.set_supports_brightness(this->dimmer_id_.has_value()); + return traits; +} + +void TuyaLight::setup_state(light::LightState *state) { state_ = state; } + +void TuyaLight::write_state(light::LightState *state) { + float brightness; + state->current_values_as_brightness(&brightness); + + if (brightness == 0.0f) { + // turning off, first try via switch (if exists), then dimmer + if (switch_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = false; + + parent_->set_datapoint_value(datapoint); + } else if (dimmer_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->dimmer_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = 0; + parent_->set_datapoint_value(datapoint); + } + return; + } + + auto brightness_int = static_cast(brightness * this->max_value_); + brightness_int = std::max(brightness_int, this->min_value_); + + if (this->dimmer_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->dimmer_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = brightness_int; + parent_->set_datapoint_value(datapoint); + } + if (this->switch_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = true; + parent_->set_datapoint_value(datapoint); + } +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h new file mode 100644 index 0000000000..581512c29c --- /dev/null +++ b/esphome/components/tuya/light/tuya_light.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/light/light_output.h" + +namespace esphome { +namespace tuya { + +class TuyaLight : public Component, public light::LightOutput { + public: + void setup() override; + void dump_config() override; + void set_dimmer_id(uint8_t dimmer_id) { this->dimmer_id_ = dimmer_id; } + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + void set_min_value(uint32_t min_value) { min_value_ = min_value; } + void set_max_value(uint32_t max_value) { max_value_ = max_value; } + light::LightTraits get_traits() override; + void setup_state(light::LightState *state) override; + void write_state(light::LightState *state) override; + + protected: + void update_dimmer_(uint32_t value); + void update_switch_(uint32_t value); + + Tuya *parent_; + optional dimmer_id_{}; + optional switch_id_{}; + uint32_t min_value_ = 0; + uint32_t max_value_ = 255; + light::LightState *state_{nullptr}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp new file mode 100644 index 0000000000..ae3e7eed43 --- /dev/null +++ b/esphome/components/tuya/tuya.cpp @@ -0,0 +1,294 @@ +#include "tuya.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya"; + +void Tuya::setup() { + this->send_empty_command_(TuyaCommandType::MCU_CONF); + this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); +} + +void Tuya::loop() { + while (this->available()) { + uint8_t c; + this->read_byte(&c); + this->handle_char_(c); + } +} + +void Tuya::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya:"); + if ((gpio_status_ != -1) || (gpio_reset_ != -1)) + ESP_LOGCONFIG(TAG, " GPIO MCU configuration not supported!"); + for (auto &info : this->datapoints_) { + if (info.type == TuyaDatapointType::BOOLEAN) + ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool)); + else if (info.type == TuyaDatapointType::INTEGER) + ESP_LOGCONFIG(TAG, " Datapoint %d: int value (value: %d)", info.id, info.value_int); + else if (info.type == TuyaDatapointType::ENUM) + ESP_LOGCONFIG(TAG, " Datapoint %d: enum (value: %d)", info.id, info.value_enum); + else if (info.type == TuyaDatapointType::BITMASK) + ESP_LOGCONFIG(TAG, " Datapoint %d: bitmask (value: %x)", info.id, info.value_bitmask); + else + ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); + } +} + +bool Tuya::validate_message_() { + uint32_t at = this->rx_message_.size() - 1; + auto *data = &this->rx_message_[0]; + uint8_t new_byte = data[at]; + + // Byte 0: HEADER1 (always 0x55) + if (at == 0) + return new_byte == 0x55; + // Byte 1: HEADER2 (always 0xAA) + if (at == 1) + return new_byte == 0xAA; + + // Byte 2: VERSION + // no validation for the following fields: + uint8_t version = data[2]; + if (at == 2) + return true; + // Byte 3: COMMAND + uint8_t command = data[3]; + if (at == 3) + return true; + + // Byte 4: LENGTH1 + // Byte 5: LENGTH2 + if (at <= 5) + // no validation for these fields + return true; + + uint16_t length = (uint16_t(data[4]) << 8) | (uint16_t(data[5])); + + // wait until all data is read + if (at - 6 < length) + return true; + + // Byte 6+LEN: CHECKSUM - sum of all bytes (including header) modulo 256 + uint8_t rx_checksum = new_byte; + uint8_t calc_checksum = 0; + for (uint32_t i = 0; i < 6 + length; i++) + calc_checksum += data[i]; + + if (rx_checksum != calc_checksum) { + ESP_LOGW(TAG, "Tuya Received invalid message checksum %02X!=%02X", rx_checksum, calc_checksum); + return false; + } + + // valid message + const uint8_t *message_data = data + 6; + ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s]", command, version, + hexencode(message_data, length).c_str()); + this->handle_command_(command, version, message_data, length); + + // return false to reset rx buffer + return false; +} + +void Tuya::handle_char_(uint8_t c) { + this->rx_message_.push_back(c); + if (!this->validate_message_()) { + this->rx_message_.clear(); + } +} + +void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { + uint8_t c; + switch ((TuyaCommandType) command) { + case TuyaCommandType::HEARTBEAT: + ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); + if (buffer[0] == 0) { + ESP_LOGI(TAG, "MCU restarted"); + this->send_empty_command_(TuyaCommandType::QUERY_STATE); + } + break; + case TuyaCommandType::QUERY_PRODUCT: { + // check it is a valid string + bool valid = false; + for (int i = 0; i < len; i++) { + if (buffer[i] == 0x00) { + valid = true; + break; + } + } + if (valid) { + ESP_LOGD(TAG, "Tuya Product Code: %s", reinterpret_cast(buffer)); + } + break; + } + case TuyaCommandType::MCU_CONF: + if (len >= 2) { + gpio_status_ = buffer[0]; + gpio_reset_ = buffer[1]; + } + // set wifi state LED to off or on depending on the MCU firmware + // but it shouldn't be blinking + c = 0x3; + this->send_command_(TuyaCommandType::WIFI_STATE, &c, 1); + this->send_empty_command_(TuyaCommandType::QUERY_STATE); + break; + case TuyaCommandType::WIFI_STATE: + break; + case TuyaCommandType::WIFI_RESET: + ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled"); + break; + case TuyaCommandType::WIFI_SELECT: + ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled"); + break; + case TuyaCommandType::SET_DATAPOINT: + break; + case TuyaCommandType::STATE: { + this->handle_datapoint_(buffer, len); + break; + } + case TuyaCommandType::QUERY_STATE: + break; + default: + ESP_LOGE(TAG, "invalid command (%02x) received", command); + } +} + +void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { + if (len < 2) + return; + + TuyaDatapoint datapoint{}; + datapoint.id = buffer[0]; + datapoint.type = (TuyaDatapointType) buffer[1]; + datapoint.value_uint = 0; + + size_t data_size = (buffer[2] << 8) + buffer[3]; + const uint8_t *data = buffer + 4; + size_t data_len = len - 4; + if (data_size != data_len) { + ESP_LOGW(TAG, "invalid datapoint update"); + return; + } + + switch (datapoint.type) { + case TuyaDatapointType::BOOLEAN: + if (data_len != 1) + return; + datapoint.value_bool = data[0]; + break; + case TuyaDatapointType::INTEGER: + if (data_len != 4) + return; + datapoint.value_uint = + (uint32_t(data[0]) << 24) | (uint32_t(data[1]) << 16) | (uint32_t(data[2]) << 8) | (uint32_t(data[3]) << 0); + break; + case TuyaDatapointType::ENUM: + if (data_len != 1) + return; + datapoint.value_enum = data[0]; + break; + case TuyaDatapointType::BITMASK: + if (data_len != 2) + return; + datapoint.value_bitmask = (uint16_t(data[0]) << 8) | (uint16_t(data[1]) << 0); + break; + default: + return; + } + ESP_LOGV(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint); + + // Update internal datapoints + bool found = false; + for (auto &other : this->datapoints_) { + if (other.id == datapoint.id) { + other = datapoint; + found = true; + } + } + if (!found) { + this->datapoints_.push_back(datapoint); + // New datapoint found, reprint dump_config after a delay. + this->set_timeout("datapoint_dump", 100, [this] { this->dump_config(); }); + } + + // Run through listeners + for (auto &listener : this->listeners_) + if (listener.datapoint_id == datapoint.id) + listener.on_datapoint(datapoint); +} + +void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) { + uint8_t len_hi = len >> 8; + uint8_t len_lo = len >> 0; + this->write_array({0x55, 0xAA, + 0x00, // version + (uint8_t) command, len_hi, len_lo}); + if (len != 0) + this->write_array(buffer, len); + + uint8_t checksum = 0x55 + 0xAA + (uint8_t) command + len_hi + len_lo; + for (int i = 0; i < len; i++) + checksum += buffer[i]; + this->write_byte(checksum); +} + +void Tuya::set_datapoint_value(TuyaDatapoint datapoint) { + std::vector buffer; + ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint); + for (auto &other : this->datapoints_) { + if (other.id == datapoint.id) { + if (other.value_uint == datapoint.value_uint) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + } + } + buffer.push_back(datapoint.id); + buffer.push_back(static_cast(datapoint.type)); + + std::vector data; + switch (datapoint.type) { + case TuyaDatapointType::BOOLEAN: + data.push_back(datapoint.value_bool); + break; + case TuyaDatapointType::INTEGER: + data.push_back(datapoint.value_uint >> 24); + data.push_back(datapoint.value_uint >> 16); + data.push_back(datapoint.value_uint >> 8); + data.push_back(datapoint.value_uint >> 0); + break; + case TuyaDatapointType::ENUM: + data.push_back(datapoint.value_enum); + break; + case TuyaDatapointType::BITMASK: + data.push_back(datapoint.value_bitmask >> 8); + data.push_back(datapoint.value_bitmask >> 0); + break; + default: + return; + } + + buffer.push_back(data.size() >> 8); + buffer.push_back(data.size() >> 0); + buffer.insert(buffer.end(), data.begin(), data.end()); + this->send_command_(TuyaCommandType::SET_DATAPOINT, buffer.data(), buffer.size()); +} + +void Tuya::register_listener(uint8_t datapoint_id, const std::function &func) { + auto listener = TuyaDatapointListener{ + .datapoint_id = datapoint_id, + .on_datapoint = func, + }; + this->listeners_.push_back(listener); + + // Run through existing datapoints + for (auto &datapoint : this->datapoints_) + if (datapoint.id == datapoint_id) + func(datapoint); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h new file mode 100644 index 0000000000..6bc6d92da0 --- /dev/null +++ b/esphome/components/tuya/tuya.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace tuya { + +enum class TuyaDatapointType : uint8_t { + RAW = 0x00, // variable length + BOOLEAN = 0x01, // 1 byte (0/1) + INTEGER = 0x02, // 4 byte + STRING = 0x03, // variable length + ENUM = 0x04, // 1 byte + BITMASK = 0x05, // 2 bytes +}; + +struct TuyaDatapoint { + uint8_t id; + TuyaDatapointType type; + union { + bool value_bool; + int value_int; + uint32_t value_uint; + uint8_t value_enum; + uint16_t value_bitmask; + }; +}; + +struct TuyaDatapointListener { + uint8_t datapoint_id; + std::function on_datapoint; +}; + +enum class TuyaCommandType : uint8_t { + HEARTBEAT = 0x00, + QUERY_PRODUCT = 0x01, + MCU_CONF = 0x02, + WIFI_STATE = 0x03, + WIFI_RESET = 0x04, + WIFI_SELECT = 0x05, + SET_DATAPOINT = 0x06, + STATE = 0x07, + QUERY_STATE = 0x08, +}; + +class Tuya : public Component, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void setup() override; + void loop() override; + void dump_config() override; + void register_listener(uint8_t datapoint_id, const std::function &func); + void set_datapoint_value(TuyaDatapoint datapoint); + + protected: + void handle_char_(uint8_t c); + void handle_datapoint_(const uint8_t *buffer, size_t len); + bool validate_message_(); + + void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len); + void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); + void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } + + int gpio_status_ = -1; + int gpio_reset_ = -1; + std::vector listeners_; + std::vector datapoints_; + std::vector rx_message_; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c65ca919ba..6d6aa80b66 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -314,4 +314,20 @@ std::array decode_uint16(uint16_t value) { return {msb, lsb}; } +std::string hexencode(const uint8_t *data, uint32_t len) { + char buf[20]; + std::string res; + for (size_t i = 0; i < len; i++) { + if (i + 1 != len) { + sprintf(buf, "%02X.", data[i]); + } else { + sprintf(buf, "%02X ", data[i]); + } + res += buf; + } + sprintf(buf, "(%u)", len); + res += buf; + return res; +} + } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 5670d136a3..88f0d587e5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -156,6 +156,9 @@ enum ParseOnOffState { ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); +// Encode raw data to a human-readable string (for debugging) +std::string hexencode(const uint8_t *data, uint32_t len); + // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 template struct seq {}; // NOLINT template struct gens : gens {}; // NOLINT