mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
This commit is contained in:
		@@ -5,7 +5,7 @@ namespace dashboard_import {
 | 
			
		||||
 | 
			
		||||
static std::string g_package_import_url;  // NOLINT
 | 
			
		||||
 | 
			
		||||
std::string get_package_import_url() { return g_package_import_url; }
 | 
			
		||||
const std::string &get_package_import_url() { return g_package_import_url; }
 | 
			
		||||
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
 | 
			
		||||
 | 
			
		||||
}  // namespace dashboard_import
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace dashboard_import {
 | 
			
		||||
 | 
			
		||||
std::string get_package_import_url();
 | 
			
		||||
const std::string &get_package_import_url();
 | 
			
		||||
void set_package_import_url(std::string url);
 | 
			
		||||
 | 
			
		||||
}  // namespace dashboard_import
 | 
			
		||||
 
 | 
			
		||||
@@ -386,6 +386,10 @@ def _check_versions(value):
 | 
			
		||||
        value[CONF_SOURCE] = value.get(
 | 
			
		||||
            CONF_SOURCE, _format_framework_arduino_version(version)
 | 
			
		||||
        )
 | 
			
		||||
        if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
            value[CONF_SOURCE] = (
 | 
			
		||||
                f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
 | 
			
		||||
            )
 | 
			
		||||
    else:
 | 
			
		||||
        if version < cv.Version(5, 0, 0):
 | 
			
		||||
            raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
 | 
			
		||||
@@ -395,6 +399,8 @@ def _check_versions(value):
 | 
			
		||||
            CONF_SOURCE,
 | 
			
		||||
            _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
 | 
			
		||||
        )
 | 
			
		||||
        if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
            value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
 | 
			
		||||
 | 
			
		||||
    if CONF_PLATFORM_VERSION not in value:
 | 
			
		||||
        if platform_lookup is None:
 | 
			
		||||
 
 | 
			
		||||
@@ -58,14 +58,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mdns_txt_record(key: str, value: str):
 | 
			
		||||
    return cg.StructInitializer(
 | 
			
		||||
        MDNSTXTRecord,
 | 
			
		||||
        ("key", cg.RawExpression(f"MDNS_STR({cg.safe_exp(key)})")),
 | 
			
		||||
        ("value", value),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mdns_service(
 | 
			
		||||
    service: str, proto: str, port: int, txt_records: list[dict[str, str]]
 | 
			
		||||
):
 | 
			
		||||
@@ -107,23 +99,51 @@ async def to_code(config):
 | 
			
		||||
    # Ensure at least 1 service (fallback service)
 | 
			
		||||
    cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count))
 | 
			
		||||
 | 
			
		||||
    # Calculate compile-time dynamic TXT value count
 | 
			
		||||
    # Dynamic values are those that cannot be stored in flash at compile time
 | 
			
		||||
    dynamic_txt_count = 0
 | 
			
		||||
    if "api" in CORE.config:
 | 
			
		||||
        # Always: get_mac_address()
 | 
			
		||||
        dynamic_txt_count += 1
 | 
			
		||||
    # User-provided templatable TXT values (only lambdas, not static strings)
 | 
			
		||||
    dynamic_txt_count += sum(
 | 
			
		||||
        1
 | 
			
		||||
        for service in config[CONF_SERVICES]
 | 
			
		||||
        for txt_value in service[CONF_TXT].values()
 | 
			
		||||
        if cg.is_template(txt_value)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Ensure at least 1 to avoid zero-size array
 | 
			
		||||
    cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count))
 | 
			
		||||
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    for service in config[CONF_SERVICES]:
 | 
			
		||||
        txt = [
 | 
			
		||||
            cg.StructInitializer(
 | 
			
		||||
                MDNSTXTRecord,
 | 
			
		||||
                ("key", cg.RawExpression(f"MDNS_STR({cg.safe_exp(txt_key)})")),
 | 
			
		||||
                ("value", await cg.templatable(txt_value, [], cg.std_string)),
 | 
			
		||||
            )
 | 
			
		||||
            for txt_key, txt_value in service[CONF_TXT].items()
 | 
			
		||||
        ]
 | 
			
		||||
        # Build the txt records list for the service
 | 
			
		||||
        txt_records = []
 | 
			
		||||
        for txt_key, txt_value in service[CONF_TXT].items():
 | 
			
		||||
            if cg.is_template(txt_value):
 | 
			
		||||
                # It's a lambda - evaluate and store using helper
 | 
			
		||||
                templated_value = await cg.templatable(txt_value, [], cg.std_string)
 | 
			
		||||
                txt_records.append(
 | 
			
		||||
                    cg.RawExpression(
 | 
			
		||||
                        f"{{MDNS_STR({cg.safe_exp(txt_key)}), MDNS_STR({var}->add_dynamic_txt_value(({templated_value})()))}}"
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                # It's a static string - use directly in flash, no need to store in vector
 | 
			
		||||
                txt_records.append(
 | 
			
		||||
                    cg.RawExpression(
 | 
			
		||||
                        f"{{MDNS_STR({cg.safe_exp(txt_key)}), MDNS_STR({cg.safe_exp(txt_value)})}}"
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        exp = mdns_service(
 | 
			
		||||
            service[CONF_SERVICE],
 | 
			
		||||
            service[CONF_PROTOCOL],
 | 
			
		||||
            await cg.templatable(service[CONF_PORT], [], cg.uint16),
 | 
			
		||||
            txt,
 | 
			
		||||
            txt_records,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        cg.add(var.add_extra_service(exp))
 | 
			
		||||
 
 | 
			
		||||
@@ -9,21 +9,9 @@
 | 
			
		||||
#include <pgmspace.h>
 | 
			
		||||
// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms
 | 
			
		||||
#define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value
 | 
			
		||||
// Helper to convert PROGMEM string to std::string for TemplatableValue
 | 
			
		||||
// Only define this function if we have services that will use it
 | 
			
		||||
#if defined(USE_API) || defined(USE_PROMETHEUS) || defined(USE_WEBSERVER) || defined(USE_MDNS_EXTRA_SERVICES)
 | 
			
		||||
static std::string mdns_str_value(PGM_P str) {
 | 
			
		||||
  char buf[64];
 | 
			
		||||
  strncpy_P(buf, str, sizeof(buf) - 1);
 | 
			
		||||
  buf[sizeof(buf) - 1] = '\0';
 | 
			
		||||
  return std::string(buf);
 | 
			
		||||
}
 | 
			
		||||
#define MDNS_STR_VALUE(name) mdns_str_value(name)
 | 
			
		||||
#endif
 | 
			
		||||
#else
 | 
			
		||||
// On non-ESP8266 platforms, use regular const char*
 | 
			
		||||
#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value
 | 
			
		||||
#define MDNS_STR_VALUE(name) std::string(name)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API
 | 
			
		||||
@@ -68,6 +56,14 @@ MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
 | 
			
		||||
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
 | 
			
		||||
MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
 | 
			
		||||
 | 
			
		||||
// Wrap build-time defines into flash storage
 | 
			
		||||
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
 | 
			
		||||
MDNS_STATIC_CONST_CHAR(VALUE_BOARD, ESPHOME_BOARD);
 | 
			
		||||
#ifdef ESPHOME_PROJECT_NAME
 | 
			
		||||
MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_NAME, ESPHOME_PROJECT_NAME);
 | 
			
		||||
MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_VERSION, ESPHOME_PROJECT_VERSION);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void MDNSComponent::compile_records_() {
 | 
			
		||||
  this->hostname_ = App.get_name();
 | 
			
		||||
 | 
			
		||||
@@ -109,47 +105,46 @@ void MDNSComponent::compile_records_() {
 | 
			
		||||
    txt_records.reserve(txt_count);
 | 
			
		||||
 | 
			
		||||
    if (!friendly_name_empty) {
 | 
			
		||||
      txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), friendly_name});
 | 
			
		||||
      txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
 | 
			
		||||
    }
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->add_dynamic_txt_value(get_mac_address()))});
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR_VALUE(PLATFORM_ESP8266)});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)});
 | 
			
		||||
#elif defined(USE_ESP32)
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR_VALUE(PLATFORM_ESP32)});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)});
 | 
			
		||||
#elif defined(USE_RP2040)
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR_VALUE(PLATFORM_RP2040)});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)});
 | 
			
		||||
#elif defined(USE_LIBRETINY)
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PLATFORM), lt_cpu_get_model_name()});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(lt_cpu_get_model_name())});
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_BOARD), ESPHOME_BOARD});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
 | 
			
		||||
 | 
			
		||||
#if defined(USE_WIFI)
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR_VALUE(NETWORK_WIFI)});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
 | 
			
		||||
#elif defined(USE_ETHERNET)
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR_VALUE(NETWORK_ETHERNET)});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)});
 | 
			
		||||
#elif defined(USE_OPENTHREAD)
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR_VALUE(NETWORK_THREAD)});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)});
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
    MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
 | 
			
		||||
    if (api::global_api_server->get_noise_ctx()->has_psk()) {
 | 
			
		||||
      txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR_VALUE(NOISE_ENCRYPTION)});
 | 
			
		||||
    } else {
 | 
			
		||||
      txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR_VALUE(NOISE_ENCRYPTION)});
 | 
			
		||||
    }
 | 
			
		||||
    txt_records.push_back({MDNS_STR(api::global_api_server->get_noise_ctx()->has_psk() ? TXT_API_ENCRYPTION
 | 
			
		||||
                                                                                       : TXT_API_ENCRYPTION_SUPPORTED),
 | 
			
		||||
                           MDNS_STR(NOISE_ENCRYPTION)});
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef ESPHOME_PROJECT_NAME
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), MDNS_STR(VALUE_PROJECT_NAME)});
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), MDNS_STR(VALUE_PROJECT_VERSION)});
 | 
			
		||||
#endif  // ESPHOME_PROJECT_NAME
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DASHBOARD_IMPORT
 | 
			
		||||
    txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), dashboard_import::get_package_import_url()});
 | 
			
		||||
    txt_records.push_back(
 | 
			
		||||
        {MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())});
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
#endif  // USE_API
 | 
			
		||||
@@ -175,7 +170,7 @@ void MDNSComponent::compile_records_() {
 | 
			
		||||
  fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
 | 
			
		||||
  fallback_service.proto = MDNS_STR(SERVICE_TCP);
 | 
			
		||||
  fallback_service.port = USE_WEBSERVER_PORT;
 | 
			
		||||
  fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION});
 | 
			
		||||
  fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -190,8 +185,7 @@ void MDNSComponent::dump_config() {
 | 
			
		||||
    ESP_LOGV(TAG, "  - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto),
 | 
			
		||||
             const_cast<TemplatableValue<uint16_t> &>(service.port).value());
 | 
			
		||||
    for (const auto &record : service.txt_records) {
 | 
			
		||||
      ESP_LOGV(TAG, "    TXT: %s = %s", MDNS_STR_ARG(record.key),
 | 
			
		||||
               const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
 | 
			
		||||
      ESP_LOGV(TAG, "    TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ struct MDNSString;
 | 
			
		||||
 | 
			
		||||
struct MDNSTXTRecord {
 | 
			
		||||
  const MDNSString *key;
 | 
			
		||||
  TemplatableValue<std::string> value;
 | 
			
		||||
  const MDNSString *value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct MDNSService {
 | 
			
		||||
@@ -59,6 +59,17 @@ class MDNSComponent : public Component {
 | 
			
		||||
 | 
			
		||||
  void on_shutdown() override;
 | 
			
		||||
 | 
			
		||||
  /// Add a dynamic TXT value and return pointer to it for use in MDNSTXTRecord
 | 
			
		||||
  const char *add_dynamic_txt_value(const std::string &value) {
 | 
			
		||||
    this->dynamic_txt_values_.push_back(value);
 | 
			
		||||
    return this->dynamic_txt_values_.back().c_str();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Storage for runtime-generated TXT values (MAC address, user lambdas)
 | 
			
		||||
  /// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
 | 
			
		||||
  /// Static/compile-time values (version, board, etc.) are stored directly in flash and don't use this.
 | 
			
		||||
  StaticVector<std::string, MDNS_DYNAMIC_TXT_COUNT> dynamic_txt_values_;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
 | 
			
		||||
  std::string hostname_;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@
 | 
			
		||||
#if defined(USE_ESP32) && defined(USE_MDNS)
 | 
			
		||||
 | 
			
		||||
#include <mdns.h>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "mdns_component.h"
 | 
			
		||||
@@ -29,21 +28,16 @@ void MDNSComponent::setup() {
 | 
			
		||||
    std::vector<mdns_txt_item_t> txt_records;
 | 
			
		||||
    for (const auto &record : service.txt_records) {
 | 
			
		||||
      mdns_txt_item_t it{};
 | 
			
		||||
      // key is a compile-time string literal in flash, no need to strdup
 | 
			
		||||
      // key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
 | 
			
		||||
      // Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
 | 
			
		||||
      it.key = MDNS_STR_ARG(record.key);
 | 
			
		||||
      // value is a temporary from TemplatableValue, must strdup to keep it alive
 | 
			
		||||
      it.value = strdup(const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
 | 
			
		||||
      it.value = MDNS_STR_ARG(record.value);
 | 
			
		||||
      txt_records.push_back(it);
 | 
			
		||||
    }
 | 
			
		||||
    uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
 | 
			
		||||
    err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
 | 
			
		||||
                           txt_records.data(), txt_records.size());
 | 
			
		||||
 | 
			
		||||
    // free records
 | 
			
		||||
    for (const auto &it : txt_records) {
 | 
			
		||||
      free((void *) it.value);  // NOLINT(cppcoreguidelines-no-malloc)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ void MDNSComponent::setup() {
 | 
			
		||||
    MDNS.addService(FPSTR(service_type), FPSTR(proto), port);
 | 
			
		||||
    for (const auto &record : service.txt_records) {
 | 
			
		||||
      MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),
 | 
			
		||||
                         const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
 | 
			
		||||
                         FPSTR(MDNS_STR_ARG(record.value)));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,8 +32,7 @@ void MDNSComponent::setup() {
 | 
			
		||||
    uint16_t port_ = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
 | 
			
		||||
    MDNS.addService(service_type, proto, port_);
 | 
			
		||||
    for (const auto &record : service.txt_records) {
 | 
			
		||||
      MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key),
 | 
			
		||||
                         const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
 | 
			
		||||
      MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -32,8 +32,7 @@ void MDNSComponent::setup() {
 | 
			
		||||
    uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
 | 
			
		||||
    MDNS.addService(service_type, proto, port);
 | 
			
		||||
    for (const auto &record : service.txt_records) {
 | 
			
		||||
      MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key),
 | 
			
		||||
                         const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
 | 
			
		||||
      MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
 | 
			
		||||
  uint8_t data_offset = 3;
 | 
			
		||||
 | 
			
		||||
  // Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes
 | 
			
		||||
  if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) {
 | 
			
		||||
  if (((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT) &&
 | 
			
		||||
       (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_1_END)) ||
 | 
			
		||||
      ((function_code >= FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT) &&
 | 
			
		||||
       (function_code <= FUNCTION_CODE_USER_DEFINED_SPACE_2_END))) {
 | 
			
		||||
    // Handle user-defined function, since we don't know how big this ought to be,
 | 
			
		||||
    // ideally we should delegate the entire length detection to whatever handler is
 | 
			
		||||
    // installed, but wait, there is the CRC, and if we get a hit there is a good
 | 
			
		||||
@@ -91,10 +94,14 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
 | 
			
		||||
  } else {
 | 
			
		||||
    // data starts at 2 and length is 4 for read registers commands
 | 
			
		||||
    if (this->role == ModbusRole::SERVER) {
 | 
			
		||||
      if (function_code == 0x1 || function_code == 0x3 || function_code == 0x4 || function_code == 0x6) {
 | 
			
		||||
      if (function_code == ModbusFunctionCode::READ_COILS ||
 | 
			
		||||
          function_code == ModbusFunctionCode::READ_DISCRETE_INPUTS ||
 | 
			
		||||
          function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
 | 
			
		||||
          function_code == ModbusFunctionCode::READ_INPUT_REGISTERS ||
 | 
			
		||||
          function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
 | 
			
		||||
        data_offset = 2;
 | 
			
		||||
        data_len = 4;
 | 
			
		||||
      } else if (function_code == 0x10) {
 | 
			
		||||
      } else if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
 | 
			
		||||
        if (at < 6) {
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
@@ -104,7 +111,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // the response for write command mirrors the requests and data starts at offset 2 instead of 3 for read commands
 | 
			
		||||
      if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) {
 | 
			
		||||
      if (function_code == ModbusFunctionCode::WRITE_SINGLE_COIL ||
 | 
			
		||||
          function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
 | 
			
		||||
          function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
 | 
			
		||||
          function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
 | 
			
		||||
        data_offset = 2;
 | 
			
		||||
        data_len = 4;
 | 
			
		||||
      }
 | 
			
		||||
@@ -112,7 +122,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
 | 
			
		||||
 | 
			
		||||
    // Error ( msb indicates error )
 | 
			
		||||
    // response format:  Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] exception code, Byte[3-4] crc
 | 
			
		||||
    if ((function_code & 0x80) == 0x80) {
 | 
			
		||||
    if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) {
 | 
			
		||||
      data_offset = 2;
 | 
			
		||||
      data_len = 1;
 | 
			
		||||
    }
 | 
			
		||||
@@ -143,10 +153,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
 | 
			
		||||
    if (device->address_ == address) {
 | 
			
		||||
      found = true;
 | 
			
		||||
      // Is it an error response?
 | 
			
		||||
      if ((function_code & 0x80) == 0x80) {
 | 
			
		||||
      if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) {
 | 
			
		||||
        ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]);
 | 
			
		||||
        if (waiting_for_response != 0) {
 | 
			
		||||
          device->on_modbus_error(function_code & 0x7F, raw[2]);
 | 
			
		||||
          device->on_modbus_error(function_code & FUNCTION_CODE_MASK, raw[2]);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Ignore modbus exception not related to a pending command
 | 
			
		||||
          ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response");
 | 
			
		||||
@@ -154,12 +164,14 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->role == ModbusRole::SERVER) {
 | 
			
		||||
        if (function_code == 0x3 || function_code == 0x4) {
 | 
			
		||||
        if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
 | 
			
		||||
            function_code == ModbusFunctionCode::READ_INPUT_REGISTERS) {
 | 
			
		||||
          device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8),
 | 
			
		||||
                                           uint16_t(data[3]) | (uint16_t(data[2]) << 8));
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
        if (function_code == 0x6 || function_code == 0x10) {
 | 
			
		||||
        if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
 | 
			
		||||
            function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
 | 
			
		||||
          device->on_modbus_write_registers(function_code, data);
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
@@ -199,7 +211,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
 | 
			
		||||
 | 
			
		||||
  // Only check max number of registers for standard function codes
 | 
			
		||||
  // Some devices use non standard codes like 0x43
 | 
			
		||||
  if (number_of_entities > MAX_VALUES && function_code <= 0x10) {
 | 
			
		||||
  if (number_of_entities > MAX_VALUES && function_code <= ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
 | 
			
		||||
    ESP_LOGE(TAG, "send too many values %d max=%zu", number_of_entities, MAX_VALUES);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -210,15 +222,17 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
 | 
			
		||||
  if (this->role == ModbusRole::CLIENT) {
 | 
			
		||||
    data.push_back(start_address >> 8);
 | 
			
		||||
    data.push_back(start_address >> 0);
 | 
			
		||||
    if (function_code != 0x5 && function_code != 0x6) {
 | 
			
		||||
    if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
 | 
			
		||||
        function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
 | 
			
		||||
      data.push_back(number_of_entities >> 8);
 | 
			
		||||
      data.push_back(number_of_entities >> 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (payload != nullptr) {
 | 
			
		||||
    if (this->role == ModbusRole::SERVER || function_code == 0xF || function_code == 0x10) {  // Write multiple
 | 
			
		||||
      data.push_back(payload_len);  // Byte count is required for write
 | 
			
		||||
    if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
 | 
			
		||||
        function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {  // Write multiple
 | 
			
		||||
      data.push_back(payload_len);                                        // Byte count is required for write
 | 
			
		||||
    } else {
 | 
			
		||||
      payload_len = 2;  // Write single register or coil
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/modbus/modbus_definitions.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -65,12 +67,12 @@ class ModbusDevice {
 | 
			
		||||
    this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload);
 | 
			
		||||
  }
 | 
			
		||||
  void send_raw(const std::vector<uint8_t> &payload) { this->parent_->send_raw(payload); }
 | 
			
		||||
  void send_error(uint8_t function_code, uint8_t exception_code) {
 | 
			
		||||
  void send_error(uint8_t function_code, ModbusExceptionCode exception_code) {
 | 
			
		||||
    std::vector<uint8_t> error_response;
 | 
			
		||||
    error_response.reserve(3);
 | 
			
		||||
    error_response.push_back(this->address_);
 | 
			
		||||
    error_response.push_back(function_code | 0x80);
 | 
			
		||||
    error_response.push_back(exception_code);
 | 
			
		||||
    error_response.push_back(function_code | FUNCTION_CODE_EXCEPTION_MASK);
 | 
			
		||||
    error_response.push_back(static_cast<uint8_t>(exception_code));
 | 
			
		||||
    this->send_raw(error_response);
 | 
			
		||||
  }
 | 
			
		||||
  // If more than one device is connected block sending a new command before a response is received
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										86
									
								
								esphome/components/modbus/modbus_definitions.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								esphome/components/modbus/modbus_definitions.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace modbus {
 | 
			
		||||
 | 
			
		||||
/// Modbus definitions from specs:
 | 
			
		||||
/// https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf
 | 
			
		||||
// 5 Function Code Categories
 | 
			
		||||
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT = 65;  // 0x41
 | 
			
		||||
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_END = 72;   // 0x48
 | 
			
		||||
 | 
			
		||||
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT = 100;  // 0x64
 | 
			
		||||
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END = 110;   // 0x6E
 | 
			
		||||
 | 
			
		||||
enum class ModbusFunctionCode : uint8_t {
 | 
			
		||||
  CUSTOM = 0x00,
 | 
			
		||||
  READ_COILS = 0x01,
 | 
			
		||||
  READ_DISCRETE_INPUTS = 0x02,
 | 
			
		||||
  READ_HOLDING_REGISTERS = 0x03,
 | 
			
		||||
  READ_INPUT_REGISTERS = 0x04,
 | 
			
		||||
  WRITE_SINGLE_COIL = 0x05,
 | 
			
		||||
  WRITE_SINGLE_REGISTER = 0x06,
 | 
			
		||||
  READ_EXCEPTION_STATUS = 0x07,   // not implemented
 | 
			
		||||
  DIAGNOSTICS = 0x08,             // not implemented
 | 
			
		||||
  GET_COMM_EVENT_COUNTER = 0x0B,  // not implemented
 | 
			
		||||
  GET_COMM_EVENT_LOG = 0x0C,      // not implemented
 | 
			
		||||
  WRITE_MULTIPLE_COILS = 0x0F,
 | 
			
		||||
  WRITE_MULTIPLE_REGISTERS = 0x10,
 | 
			
		||||
  REPORT_SERVER_ID = 0x11,               // not implemented
 | 
			
		||||
  READ_FILE_RECORD = 0x14,               // not implemented
 | 
			
		||||
  WRITE_FILE_RECORD = 0x15,              // not implemented
 | 
			
		||||
  MASK_WRITE_REGISTER = 0x16,            // not implemented
 | 
			
		||||
  READ_WRITE_MULTIPLE_REGISTERS = 0x17,  // not implemented
 | 
			
		||||
  READ_FIFO_QUEUE = 0x18,                // not implemented
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/*Allow  comparison operators between ModbusFunctionCode and uint8_t*/
 | 
			
		||||
inline bool operator==(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) == rhs; }
 | 
			
		||||
inline bool operator==(uint8_t lhs, ModbusFunctionCode rhs) { return lhs == static_cast<uint8_t>(rhs); }
 | 
			
		||||
inline bool operator!=(ModbusFunctionCode lhs, uint8_t rhs) { return !(static_cast<uint8_t>(lhs) == rhs); }
 | 
			
		||||
inline bool operator!=(uint8_t lhs, ModbusFunctionCode rhs) { return !(lhs == static_cast<uint8_t>(rhs)); }
 | 
			
		||||
inline bool operator<(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) < rhs; }
 | 
			
		||||
inline bool operator<(uint8_t lhs, ModbusFunctionCode rhs) { return lhs < static_cast<uint8_t>(rhs); }
 | 
			
		||||
inline bool operator<=(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) <= rhs; }
 | 
			
		||||
inline bool operator<=(uint8_t lhs, ModbusFunctionCode rhs) { return lhs <= static_cast<uint8_t>(rhs); }
 | 
			
		||||
inline bool operator>(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) > rhs; }
 | 
			
		||||
inline bool operator>(uint8_t lhs, ModbusFunctionCode rhs) { return lhs > static_cast<uint8_t>(rhs); }
 | 
			
		||||
inline bool operator>=(ModbusFunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) >= rhs; }
 | 
			
		||||
inline bool operator>=(uint8_t lhs, ModbusFunctionCode rhs) { return lhs >= static_cast<uint8_t>(rhs); }
 | 
			
		||||
 | 
			
		||||
// 4.3 MODBUS Data model
 | 
			
		||||
enum class ModbusRegisterType : uint8_t {
 | 
			
		||||
  CUSTOM = 0x00,
 | 
			
		||||
  COIL = 0x01,
 | 
			
		||||
  DISCRETE_INPUT = 0x02,
 | 
			
		||||
  HOLDING = 0x03,
 | 
			
		||||
  READ = 0x04,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 7 MODBUS Exception Responses:
 | 
			
		||||
const uint8_t FUNCTION_CODE_MASK = 0x7F;
 | 
			
		||||
const uint8_t FUNCTION_CODE_EXCEPTION_MASK = 0x80;
 | 
			
		||||
 | 
			
		||||
enum class ModbusExceptionCode : uint8_t {
 | 
			
		||||
  ILLEGAL_FUNCTION = 0x01,
 | 
			
		||||
  ILLEGAL_DATA_ADDRESS = 0x02,
 | 
			
		||||
  ILLEGAL_DATA_VALUE = 0x03,
 | 
			
		||||
  SERVICE_DEVICE_FAILURE = 0x04,
 | 
			
		||||
  ACKNOWLEDGE = 0x05,
 | 
			
		||||
  SERVER_DEVICE_BUSY = 0x06,
 | 
			
		||||
  MEMORY_PARITY_ERROR = 0x08,
 | 
			
		||||
  GATEWAY_PATH_UNAVAILABLE = 0x0A,
 | 
			
		||||
  GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0B,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 6.12 16 (0x10) Write Multiple registers:
 | 
			
		||||
const uint8_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123;  // 0x7B
 | 
			
		||||
 | 
			
		||||
// 6.3 03 (0x03) Read Holding Registers
 | 
			
		||||
// 6.4 04 (0x04) Read Input Registers
 | 
			
		||||
const uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125;  // 0x7D
 | 
			
		||||
/// End of Modbus definitions
 | 
			
		||||
}  // namespace modbus
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -20,6 +20,7 @@ from .const import (
 | 
			
		||||
    CONF_BYTE_OFFSET,
 | 
			
		||||
    CONF_COMMAND_THROTTLE,
 | 
			
		||||
    CONF_CUSTOM_COMMAND,
 | 
			
		||||
    CONF_ENABLED,
 | 
			
		||||
    CONF_FORCE_NEW_RANGE,
 | 
			
		||||
    CONF_MAX_CMD_RETRIES,
 | 
			
		||||
    CONF_MODBUS_CONTROLLER_ID,
 | 
			
		||||
@@ -28,8 +29,11 @@ from .const import (
 | 
			
		||||
    CONF_ON_OFFLINE,
 | 
			
		||||
    CONF_ON_ONLINE,
 | 
			
		||||
    CONF_REGISTER_COUNT,
 | 
			
		||||
    CONF_REGISTER_LAST_ADDRESS,
 | 
			
		||||
    CONF_REGISTER_TYPE,
 | 
			
		||||
    CONF_REGISTER_VALUE,
 | 
			
		||||
    CONF_RESPONSE_SIZE,
 | 
			
		||||
    CONF_SERVER_COURTESY_RESPONSE,
 | 
			
		||||
    CONF_SKIP_UPDATES,
 | 
			
		||||
    CONF_VALUE_TYPE,
 | 
			
		||||
)
 | 
			
		||||
@@ -49,6 +53,7 @@ ModbusController = modbus_controller_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SensorItem = modbus_controller_ns.struct("SensorItem")
 | 
			
		||||
ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse")
 | 
			
		||||
ServerRegister = modbus_controller_ns.struct("ServerRegister")
 | 
			
		||||
 | 
			
		||||
ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode")
 | 
			
		||||
@@ -143,6 +148,14 @@ ModbusOfflineTrigger = modbus_controller_ns.class_(
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_ENABLED, default=False): cv.boolean,
 | 
			
		||||
        cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
 | 
			
		||||
        cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ModbusServerRegisterSchema = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(ServerRegister),
 | 
			
		||||
@@ -162,6 +175,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_COMMAND_THROTTLE, default="0ms"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Optional(CONF_SERVER_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
 | 
			
		||||
            cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
 | 
			
		||||
            cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
@@ -232,7 +246,7 @@ def validate_modbus_register(config):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _final_validate(config):
 | 
			
		||||
    if CONF_SERVER_REGISTERS in config:
 | 
			
		||||
    if CONF_SERVER_COURTESY_RESPONSE in config or CONF_SERVER_REGISTERS in config:
 | 
			
		||||
        return modbus.final_validate_modbus_device("modbus_controller", role="server")(
 | 
			
		||||
            config
 | 
			
		||||
        )
 | 
			
		||||
@@ -299,6 +313,20 @@ async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
 | 
			
		||||
    cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
 | 
			
		||||
    if server_courtesy_response := config.get(CONF_SERVER_COURTESY_RESPONSE):
 | 
			
		||||
        cg.add(
 | 
			
		||||
            var.set_server_courtesy_response(
 | 
			
		||||
                cg.StructInitializer(
 | 
			
		||||
                    ServerCourtesyResponse,
 | 
			
		||||
                    ("enabled", server_courtesy_response[CONF_ENABLED]),
 | 
			
		||||
                    (
 | 
			
		||||
                        "register_last_address",
 | 
			
		||||
                        server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
 | 
			
		||||
                    ),
 | 
			
		||||
                    ("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
 | 
			
		||||
    cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
 | 
			
		||||
    if CONF_SERVER_REGISTERS in config:
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands"
 | 
			
		||||
CONF_BITMASK = "bitmask"
 | 
			
		||||
CONF_BYTE_OFFSET = "byte_offset"
 | 
			
		||||
CONF_COMMAND_THROTTLE = "command_throttle"
 | 
			
		||||
CONF_ENABLED = "enabled"
 | 
			
		||||
CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates"
 | 
			
		||||
CONF_CUSTOM_COMMAND = "custom_command"
 | 
			
		||||
CONF_FORCE_NEW_RANGE = "force_new_range"
 | 
			
		||||
@@ -13,8 +14,11 @@ CONF_ON_ONLINE = "on_online"
 | 
			
		||||
CONF_ON_OFFLINE = "on_offline"
 | 
			
		||||
CONF_RAW_ENCODE = "raw_encode"
 | 
			
		||||
CONF_REGISTER_COUNT = "register_count"
 | 
			
		||||
CONF_REGISTER_LAST_ADDRESS = "register_last_address"
 | 
			
		||||
CONF_REGISTER_TYPE = "register_type"
 | 
			
		||||
CONF_REGISTER_VALUE = "register_value"
 | 
			
		||||
CONF_RESPONSE_SIZE = "response_size"
 | 
			
		||||
CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response"
 | 
			
		||||
CONF_SKIP_UPDATES = "skip_updates"
 | 
			
		||||
CONF_USE_WRITE_MULTIPLE = "use_write_multiple"
 | 
			
		||||
CONF_VALUE_TYPE = "value_type"
 | 
			
		||||
 
 | 
			
		||||
@@ -112,6 +112,12 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
 | 
			
		||||
           "0x%X.",
 | 
			
		||||
           this->address_, function_code, start_address, number_of_registers);
 | 
			
		||||
 | 
			
		||||
  if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
 | 
			
		||||
    this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint16_t> sixteen_bit_response;
 | 
			
		||||
  for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
 | 
			
		||||
    bool found = false;
 | 
			
		||||
@@ -136,9 +142,21 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!found) {
 | 
			
		||||
      ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address);
 | 
			
		||||
      send_error(function_code, 0x02);
 | 
			
		||||
      return;
 | 
			
		||||
      if (this->server_courtesy_response_.enabled &&
 | 
			
		||||
          (current_address <= this->server_courtesy_response_.register_last_address)) {
 | 
			
		||||
        ESP_LOGD(TAG,
 | 
			
		||||
                 "Could not match any register to address 0x%02X, but default allowed. "
 | 
			
		||||
                 "Returning default value: %d.",
 | 
			
		||||
                 current_address, this->server_courtesy_response_.register_value);
 | 
			
		||||
        sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
 | 
			
		||||
        current_address += 1;  // Just increment by 1, as the default response is a single register
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGW(TAG,
 | 
			
		||||
                 "Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
 | 
			
		||||
                 current_address);
 | 
			
		||||
        this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -156,27 +174,27 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st
 | 
			
		||||
  uint16_t number_of_registers;
 | 
			
		||||
  uint16_t payload_offset;
 | 
			
		||||
 | 
			
		||||
  if (function_code == 0x10) {
 | 
			
		||||
  if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
 | 
			
		||||
    number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
 | 
			
		||||
    if (number_of_registers == 0 || number_of_registers > 0x7B) {
 | 
			
		||||
    if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
 | 
			
		||||
      send_error(function_code, 3);
 | 
			
		||||
      this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    uint16_t payload_size = data[4];
 | 
			
		||||
    if (payload_size != number_of_registers * 2) {
 | 
			
		||||
      ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.",
 | 
			
		||||
               payload_size, number_of_registers);
 | 
			
		||||
      send_error(function_code, 3);
 | 
			
		||||
      this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    payload_offset = 5;
 | 
			
		||||
  } else if (function_code == 0x06) {
 | 
			
		||||
  } else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
 | 
			
		||||
    number_of_registers = 1;
 | 
			
		||||
    payload_offset = 2;
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
 | 
			
		||||
    send_error(function_code, 1);
 | 
			
		||||
    this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -211,7 +229,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st
 | 
			
		||||
  if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
 | 
			
		||||
        return server_register->write_lambda != nullptr;
 | 
			
		||||
      })) {
 | 
			
		||||
    send_error(function_code, 1);
 | 
			
		||||
    this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -220,7 +238,7 @@ void ModbusController::on_modbus_write_registers(uint8_t function_code, const st
 | 
			
		||||
        int64_t number = payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
 | 
			
		||||
        return server_register->write_lambda(number);
 | 
			
		||||
      })) {
 | 
			
		||||
    send_error(function_code, 4);
 | 
			
		||||
    this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -431,8 +449,15 @@ void ModbusController::dump_config() {
 | 
			
		||||
                "ModbusController:\n"
 | 
			
		||||
                "  Address: 0x%02X\n"
 | 
			
		||||
                "  Max Command Retries: %d\n"
 | 
			
		||||
                "  Offline Skip Updates: %d",
 | 
			
		||||
                this->address_, this->max_cmd_retries_, this->offline_skip_updates_);
 | 
			
		||||
                "  Offline Skip Updates: %d\n"
 | 
			
		||||
                "  Server Courtesy Response:\n"
 | 
			
		||||
                "    Enabled: %s\n"
 | 
			
		||||
                "    Register Last Address: 0x%02X\n"
 | 
			
		||||
                "    Register Value: %d",
 | 
			
		||||
                this->address_, this->max_cmd_retries_, this->offline_skip_updates_,
 | 
			
		||||
                this->server_courtesy_response_.enabled ? "true" : "false",
 | 
			
		||||
                this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "sensormap");
 | 
			
		||||
  for (auto &it : this->sensorset_) {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,35 +16,9 @@ namespace modbus_controller {
 | 
			
		||||
 | 
			
		||||
class ModbusController;
 | 
			
		||||
 | 
			
		||||
enum class ModbusFunctionCode {
 | 
			
		||||
  CUSTOM = 0x00,
 | 
			
		||||
  READ_COILS = 0x01,
 | 
			
		||||
  READ_DISCRETE_INPUTS = 0x02,
 | 
			
		||||
  READ_HOLDING_REGISTERS = 0x03,
 | 
			
		||||
  READ_INPUT_REGISTERS = 0x04,
 | 
			
		||||
  WRITE_SINGLE_COIL = 0x05,
 | 
			
		||||
  WRITE_SINGLE_REGISTER = 0x06,
 | 
			
		||||
  READ_EXCEPTION_STATUS = 0x07,   // not implemented
 | 
			
		||||
  DIAGNOSTICS = 0x08,             // not implemented
 | 
			
		||||
  GET_COMM_EVENT_COUNTER = 0x0B,  // not implemented
 | 
			
		||||
  GET_COMM_EVENT_LOG = 0x0C,      // not implemented
 | 
			
		||||
  WRITE_MULTIPLE_COILS = 0x0F,
 | 
			
		||||
  WRITE_MULTIPLE_REGISTERS = 0x10,
 | 
			
		||||
  REPORT_SERVER_ID = 0x11,               // not implemented
 | 
			
		||||
  READ_FILE_RECORD = 0x14,               // not implemented
 | 
			
		||||
  WRITE_FILE_RECORD = 0x15,              // not implemented
 | 
			
		||||
  MASK_WRITE_REGISTER = 0x16,            // not implemented
 | 
			
		||||
  READ_WRITE_MULTIPLE_REGISTERS = 0x17,  // not implemented
 | 
			
		||||
  READ_FIFO_QUEUE = 0x18,                // not implemented
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class ModbusRegisterType : uint8_t {
 | 
			
		||||
  CUSTOM = 0x0,
 | 
			
		||||
  COIL = 0x01,
 | 
			
		||||
  DISCRETE_INPUT = 0x02,
 | 
			
		||||
  HOLDING = 0x03,
 | 
			
		||||
  READ = 0x04,
 | 
			
		||||
};
 | 
			
		||||
using modbus::ModbusFunctionCode;
 | 
			
		||||
using modbus::ModbusRegisterType;
 | 
			
		||||
using modbus::ModbusExceptionCode;
 | 
			
		||||
 | 
			
		||||
enum class SensorValueType : uint8_t {
 | 
			
		||||
  RAW = 0x00,     // variable length
 | 
			
		||||
@@ -256,6 +230,12 @@ class SensorItem {
 | 
			
		||||
  bool force_new_range{false};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct ServerCourtesyResponse {
 | 
			
		||||
  bool enabled{false};
 | 
			
		||||
  uint16_t register_last_address{0xFFFF};
 | 
			
		||||
  uint16_t register_value{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ServerRegister {
 | 
			
		||||
  using ReadLambda = std::function<int64_t()>;
 | 
			
		||||
  using WriteLambda = std::function<bool(int64_t value)>;
 | 
			
		||||
@@ -530,6 +510,12 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
 | 
			
		||||
  void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
 | 
			
		||||
  /// get how many times a command will be (re)sent if no response is received
 | 
			
		||||
  uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
 | 
			
		||||
  /// Called by esphome generated code to set the server courtesy response object
 | 
			
		||||
  void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
 | 
			
		||||
    this->server_courtesy_response_ = server_courtesy_response;
 | 
			
		||||
  }
 | 
			
		||||
  /// Get the server courtesy response object
 | 
			
		||||
  ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// parse sensormap_ and create range of sequential addresses
 | 
			
		||||
@@ -572,6 +558,9 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
 | 
			
		||||
  CallbackManager<void(int, int)> online_callback_{};
 | 
			
		||||
  /// Server offline callback
 | 
			
		||||
  CallbackManager<void(int, int)> offline_callback_{};
 | 
			
		||||
  /// Server courtesy response
 | 
			
		||||
  ServerCourtesyResponse server_courtesy_response_{
 | 
			
		||||
      .enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Convert vector<uint8_t> response payload to float.
 | 
			
		||||
 
 | 
			
		||||
@@ -180,10 +180,12 @@ void OpenThreadSrpComponent::setup() {
 | 
			
		||||
    entry->mService.mNumTxtEntries = service.txt_records.size();
 | 
			
		||||
    for (size_t i = 0; i < service.txt_records.size(); i++) {
 | 
			
		||||
      const auto &txt = service.txt_records[i];
 | 
			
		||||
      auto value = const_cast<TemplatableValue<std::string> &>(txt.value).value();
 | 
			
		||||
      // Value is either a compile-time string literal in flash or a pointer to dynamic_txt_values_
 | 
			
		||||
      // OpenThread SRP client expects the data to persist, so we strdup it
 | 
			
		||||
      const char *value_str = MDNS_STR_ARG(txt.value);
 | 
			
		||||
      txt_entries[i].mKey = MDNS_STR_ARG(txt.key);
 | 
			
		||||
      txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value.c_str()));
 | 
			
		||||
      txt_entries[i].mValueLength = value.size();
 | 
			
		||||
      txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value_str));
 | 
			
		||||
      txt_entries[i].mValueLength = strlen(value_str);
 | 
			
		||||
    }
 | 
			
		||||
    entry->mService.mTxtEntries = txt_entries;
 | 
			
		||||
    entry->mService.mNumTxtEntries = service.txt_records.size();
 | 
			
		||||
 
 | 
			
		||||
@@ -647,7 +647,7 @@ class AddDynamicAutoLoadsValidationStep(ConfigValidationStep):
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # Has to happen after normal schema is validated and before final schema validation
 | 
			
		||||
    priority = -10.0
 | 
			
		||||
    priority = -5.0
 | 
			
		||||
 | 
			
		||||
    def __init__(self, path: ConfigPath, comp: ComponentManifest) -> None:
 | 
			
		||||
        self.path = path
 | 
			
		||||
 
 | 
			
		||||
@@ -149,6 +149,9 @@ template<typename T, size_t N> class StaticVector {
 | 
			
		||||
  T &operator[](size_t i) { return data_[i]; }
 | 
			
		||||
  const T &operator[](size_t i) const { return data_[i]; }
 | 
			
		||||
 | 
			
		||||
  T &back() { return data_[count_ - 1]; }
 | 
			
		||||
  const T &back() const { return data_[count_ - 1]; }
 | 
			
		||||
 | 
			
		||||
  // For range-based for loops
 | 
			
		||||
  iterator begin() { return data_.begin(); }
 | 
			
		||||
  iterator end() { return data_.begin() + count_; }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile
 | 
			
		||||
esptool==5.1.0
 | 
			
		||||
click==8.1.7
 | 
			
		||||
esphome-dashboard==20250904.0
 | 
			
		||||
aioesphomeapi==41.12.0
 | 
			
		||||
aioesphomeapi==41.13.0
 | 
			
		||||
zeroconf==0.148.0
 | 
			
		||||
puremagic==1.30
 | 
			
		||||
ruamel.yaml==0.18.15 # dashboard_import
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,9 @@ mdns:
 | 
			
		||||
    - service: _http
 | 
			
		||||
      protocol: _tcp
 | 
			
		||||
      port: 80
 | 
			
		||||
      txt:
 | 
			
		||||
        version: "1.0"
 | 
			
		||||
        path: "/"
 | 
			
		||||
 | 
			
		||||
# OTA should run at priority 54 (after mdns)
 | 
			
		||||
ota:
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,22 @@ modbus_controller:
 | 
			
		||||
          printf("address=%d, value=%d", x);
 | 
			
		||||
          return true;
 | 
			
		||||
    max_cmd_retries: 0
 | 
			
		||||
  - id: modbus_controller4
 | 
			
		||||
    modbus_id: mod_bus2
 | 
			
		||||
    address: 0x4
 | 
			
		||||
    server_courtesy_response:
 | 
			
		||||
      enabled: true
 | 
			
		||||
      register_last_address: 100
 | 
			
		||||
      register_value: 0
 | 
			
		||||
    server_registers:
 | 
			
		||||
      - address: 0x0001
 | 
			
		||||
        value_type: U_WORD
 | 
			
		||||
        read_lambda: |-
 | 
			
		||||
          return 0x8;
 | 
			
		||||
      - address: 0x0005
 | 
			
		||||
        value_type: U_WORD
 | 
			
		||||
        read_lambda: |-
 | 
			
		||||
          return (random_uint32() % 100);
 | 
			
		||||
binary_sensor:
 | 
			
		||||
  - platform: modbus_controller
 | 
			
		||||
    modbus_controller_id: modbus_controller1
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user