1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-29 08:32:26 +01:00

Merge branch 'dev' into dashboard_dns_lookup_delay

This commit is contained in:
J. Nick Koston
2025-09-14 17:37:29 -05:00
committed by GitHub
24 changed files with 1489 additions and 93 deletions

View File

@@ -27,9 +27,6 @@ service APIConnection {
rpc subscribe_logs (SubscribeLogsRequest) returns (void) {}
rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {}
rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {}
rpc get_time (GetTimeRequest) returns (GetTimeResponse) {
option (needs_authentication) = false;
}
rpc execute_service (ExecuteServiceRequest) returns (void) {}
rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {}
@@ -809,12 +806,12 @@ message HomeAssistantStateResponse {
// ==================== IMPORT TIME ====================
message GetTimeRequest {
option (id) = 36;
option (source) = SOURCE_BOTH;
option (source) = SOURCE_SERVER;
}
message GetTimeResponse {
option (id) = 37;
option (source) = SOURCE_BOTH;
option (source) = SOURCE_CLIENT;
option (no_delay) = true;
fixed32 epoch_seconds = 1;

View File

@@ -42,6 +42,8 @@ static constexpr uint8_t MAX_PING_RETRIES = 60;
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
static const char *const TAG = "api.connection";
#ifdef USE_CAMERA
static const int CAMERA_STOP_STREAM = 5000;
@@ -1081,12 +1083,6 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
}
#endif
bool APIConnection::send_get_time_response(const GetTimeRequest &msg) {
GetTimeResponse resp;
resp.epoch_seconds = ::time(nullptr);
return this->send_message(resp, GetTimeResponse::MESSAGE_TYPE);
}
#ifdef USE_BLUETOOTH_PROXY
void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) {
bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags);
@@ -1376,9 +1372,8 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 12;
// Temporary string for concatenation - will be valid during send_message call
std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
resp.set_server_info(StringRef(server_info));
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.set_server_info(ESPHOME_VERSION_REF);
resp.set_name(StringRef(App.get_name()));
#ifdef USE_API_PASSWORD
@@ -1425,8 +1420,6 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
std::string mac_address = get_mac_address_pretty();
resp.set_mac_address(StringRef(mac_address));
// Compile-time StringRef constants
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
resp.set_esphome_version(ESPHOME_VERSION_REF);
resp.set_compilation_time(App.get_compilation_time_ref());

View File

@@ -219,7 +219,6 @@ class APIConnection final : public APIServerConnection {
#ifdef USE_API_HOMEASSISTANT_STATES
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif
bool send_get_time_response(const GetTimeRequest &msg) override;
#ifdef USE_API_SERVICES
void execute_service(const ExecuteServiceRequest &msg) override;
#endif

View File

@@ -921,14 +921,6 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
}
return true;
}
void GetTimeResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->epoch_seconds);
buffer.encode_string(2, this->timezone_ref_);
}
void GetTimeResponse::calculate_size(ProtoSize &size) const {
size.add_fixed32(1, this->epoch_seconds);
size.add_length(1, this->timezone_ref_.size());
}
#ifdef USE_API_SERVICES
void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->name_ref_);

View File

@@ -1180,10 +1180,6 @@ class GetTimeResponse final : public ProtoDecodableMessage {
#endif
uint32_t epoch_seconds{0};
std::string timezone{};
StringRef timezone_ref_{};
void set_timezone(const StringRef &ref) { this->timezone_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif

View File

@@ -1113,13 +1113,7 @@ void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeReques
void GetTimeResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "GetTimeResponse");
dump_field(out, "epoch_seconds", this->epoch_seconds);
out.append(" timezone: ");
if (!this->timezone_ref_.empty()) {
out.append("'").append(this->timezone_ref_.c_str()).append("'");
} else {
out.append("'").append(this->timezone).append("'");
}
out.append("\n");
dump_field(out, "timezone", this->timezone);
}
#ifdef USE_API_SERVICES
void ListEntitiesServicesArgument::dump_to(std::string &out) const {

View File

@@ -160,15 +160,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break;
}
#endif
case GetTimeRequest::MESSAGE_TYPE: {
GetTimeRequest msg;
// Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str());
#endif
this->on_get_time_request(msg);
break;
}
case GetTimeResponse::MESSAGE_TYPE: {
GetTimeResponse msg;
msg.decode(msg_data, msg_size);
@@ -656,11 +647,6 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
}
}
#endif
void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
if (this->check_connection_setup_() && !this->send_get_time_response(msg)) {
this->on_fatal_error();
}
}
#ifdef USE_API_SERVICES
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
if (this->check_authenticated_()) {

View File

@@ -71,7 +71,7 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
#endif
virtual void on_get_time_request(const GetTimeRequest &value){};
virtual void on_get_time_response(const GetTimeResponse &value){};
#ifdef USE_API_SERVICES
@@ -226,7 +226,6 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
#endif
virtual bool send_get_time_response(const GetTimeRequest &msg) = 0;
#ifdef USE_API_SERVICES
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#endif
@@ -348,7 +347,6 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif
void on_get_time_request(const GetTimeRequest &msg) override;
#ifdef USE_API_SERVICES
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#endif

View File

@@ -300,6 +300,7 @@ void EthernetComponent::loop() {
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
} else {
this->finish_connect_();
// When connected and stable, disable the loop to save CPU cycles
this->disable_loop();
}
@@ -486,10 +487,35 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
}
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::finish_connect_() {
#if USE_NETWORK_IPV6
// Retry IPv6 link-local setup if it failed during initial connect
// This handles the case where min_ipv6_addr_count is NOT set (or is 0),
// allowing us to reach CONNECTED state with just IPv4.
// If IPv6 setup failed in start_connect_() because the interface wasn't ready:
// - Bootup timing issues (#10281)
// - Cable unplugged/network interruption (#10705)
// We can now retry since we're in CONNECTED state and the interface is definitely up.
if (!this->ipv6_setup_done_) {
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err == ESP_OK) {
ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
}
// Always set the flag to prevent continuous retries
// If IPv6 setup fails here with the interface up and stable, it's
// likely a persistent issue (IPv6 disabled at router, hardware
// limitation, etc.) that won't be resolved by further retries.
// The device continues to work with IPv4.
this->ipv6_setup_done_ = true;
}
#endif /* USE_NETWORK_IPV6 */
}
void EthernetComponent::start_connect_() {
global_eth_component->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
global_eth_component->ipv6_count_ = 0;
this->ipv6_setup_done_ = false;
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning(LOG_STR("waiting for IP configuration"));
@@ -545,9 +571,27 @@ void EthernetComponent::start_connect_() {
}
}
#if USE_NETWORK_IPV6
// Attempt to create IPv6 link-local address
// We MUST attempt this here, not just in finish_connect_(), because with
// min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6.
// However, this may fail with ESP_FAIL if the interface is not up yet:
// - At bootup when link isn't ready (#10281)
// - After disconnection/cable unplugged (#10705)
// We'll retry in finish_connect_() if it fails here.
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err != ESP_OK) {
ESPHL_ERROR_CHECK(err, "Enable IPv6 link local failed");
if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
// This is a programming error, not a transient failure
ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters");
} else {
// ESP_FAIL means the interface isn't up yet
// This is expected and non-fatal, happens in multiple scenarios:
// - During reconnection after network interruptions (#10705)
// - At bootup when the link isn't ready yet (#10281)
// We'll retry once we reach CONNECTED state and the interface is up
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err));
// Don't mark component as failed - this is a transient error
}
}
#endif /* USE_NETWORK_IPV6 */
@@ -638,7 +682,9 @@ void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
std::string EthernetComponent::get_eth_mac_address_pretty() {
uint8_t mac[6];
get_eth_mac_address_raw(mac);
return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
char buf[18];
format_mac_addr_upper(mac, buf);
return std::string(buf);
}
eth_duplex_t EthernetComponent::get_duplex_mode() {

View File

@@ -102,6 +102,7 @@ class EthernetComponent : public Component {
#endif /* LWIP_IPV6 */
void start_connect_();
void finish_connect_();
void dump_connect_params_();
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
@@ -144,6 +145,7 @@ class EthernetComponent : public Component {
bool got_ipv4_address_{false};
#if LWIP_IPV6
uint8_t ipv6_count_{0};
bool ipv6_setup_done_{false};
#endif /* LWIP_IPV6 */
// Pointers at the end (naturally aligned)

View File

@@ -18,6 +18,7 @@ from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_CELSIUS,
UNIT_VOLT,
@@ -162,7 +163,7 @@ INA2XX_SCHEMA = cv.Schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=8,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_MEASUREMENT,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
key=CONF_NAME,
),
@@ -170,7 +171,8 @@ INA2XX_SCHEMA = cv.Schema(
sensor.sensor_schema(
unit_of_measurement=UNIT_JOULE,
accuracy_decimals=8,
state_class=STATE_CLASS_MEASUREMENT,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
key=CONF_NAME,
),

View File

@@ -1,4 +1,3 @@
#include <cstdio>
#include <cstring>
#include "md5.h"
#ifdef USE_MD5
@@ -44,7 +43,9 @@ void MD5Digest::get_bytes(uint8_t *output) { memcpy(output, this->digest_, 16);
void MD5Digest::get_hex(char *output) {
for (size_t i = 0; i < 16; i++) {
sprintf(output + i * 2, "%02x", this->digest_[i]);
uint8_t byte = this->digest_[i];
output[i * 2] = format_hex_char(byte >> 4);
output[i * 2 + 1] = format_hex_char(byte & 0x0F);
}
}

View File

@@ -593,7 +593,7 @@ void WiFiComponent::check_scanning_finished() {
for (auto &res : this->scan_result_) {
char bssid_s[18];
auto bssid = res.get_bssid();
sprintf(bssid_s, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
format_mac_addr_upper(bssid.data(), bssid_s);
if (res.get_matches()) {
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),

View File

@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/wifi/wifi_component.h"
#ifdef USE_WIFI
@@ -106,8 +107,8 @@ class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor {
wifi::bssid_t bssid = wifi::global_wifi_component->wifi_bssid();
if (memcmp(bssid.data(), last_bssid_.data(), 6) != 0) {
std::copy(bssid.begin(), bssid.end(), last_bssid_.begin());
char buf[30];
sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]);
char buf[18];
format_mac_addr_upper(bssid.data(), buf);
this->publish_state(buf);
}
}

View File

@@ -255,23 +255,22 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
}
std::string format_mac_address_pretty(const uint8_t *mac) {
return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
char buf[18];
format_mac_addr_upper(mac, buf);
return std::string(buf);
}
static char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; }
std::string format_hex(const uint8_t *data, size_t length) {
std::string ret;
ret.resize(length * 2);
for (size_t i = 0; i < length; i++) {
ret[2 * i] = format_hex_char((data[i] & 0xF0) >> 4);
ret[2 * i] = format_hex_char(data[i] >> 4);
ret[2 * i + 1] = format_hex_char(data[i] & 0x0F);
}
return ret;
}
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
// Shared implementation for uint8_t and string hex formatting
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
if (data == nullptr || length == 0)
@@ -280,7 +279,7 @@ static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, c
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
ret.resize(multiple * length - (separator ? 1 : 0));
for (size_t i = 0; i < length; i++) {
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
ret[multiple * i] = format_hex_pretty_char(data[i] >> 4);
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
if (separator && i != length - 1)
ret[multiple * i + 2] = separator;
@@ -591,7 +590,9 @@ bool HighFrequencyLoopRequester::is_high_frequency() { return num_requests > 0;
std::string get_mac_address() {
uint8_t mac[6];
get_mac_address_raw(mac);
return str_snprintf("%02x%02x%02x%02x%02x%02x", 12, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
char buf[13];
format_mac_addr_lower_no_sep(mac, buf);
return std::string(buf);
}
std::string get_mac_address_pretty() {

View File

@@ -380,6 +380,35 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> optional<
return parse_hex<T>(str.c_str(), str.length());
}
/// Convert a nibble (0-15) to lowercase hex char
inline char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; }
/// Convert a nibble (0-15) to uppercase hex char (used for pretty printing)
/// This always uses uppercase (A-F) for pretty/human-readable output
inline char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
/// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase)
inline void format_mac_addr_upper(const uint8_t *mac, char *output) {
for (size_t i = 0; i < 6; i++) {
uint8_t byte = mac[i];
output[i * 3] = format_hex_pretty_char(byte >> 4);
output[i * 3 + 1] = format_hex_pretty_char(byte & 0x0F);
if (i < 5)
output[i * 3 + 2] = ':';
}
output[17] = '\0';
}
/// Format MAC address as xxxxxxxxxxxxxx (lowercase, no separators)
inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) {
for (size_t i = 0; i < 6; i++) {
uint8_t byte = mac[i];
output[i * 2] = format_hex_char(byte >> 4);
output[i * 2 + 1] = format_hex_char(byte & 0x0F);
}
output[12] = '\0';
}
/// Format the six-byte array \p mac into a MAC address.
std::string format_mac_address_pretty(const uint8_t mac[6]);
/// Format the byte array \p data of length \p len in lowercased hex.

View File

@@ -345,7 +345,7 @@ void HOT Scheduler::call(uint32_t now) {
// Execute callback without holding lock to prevent deadlocks
// if the callback tries to call defer() again
if (!this->should_skip_item_(item.get())) {
this->execute_item_(item.get(), now);
now = this->execute_item_(item.get(), now);
}
// Recycle the defer item after execution
this->recycle_item_(std::move(item));
@@ -483,7 +483,7 @@ void HOT Scheduler::call(uint32_t now) {
// Warning: During callback(), a lot of stuff can happen, including:
// - timeouts/intervals get added, potentially invalidating vector pointers
// - timeouts/intervals get cancelled
this->execute_item_(item.get(), now);
now = this->execute_item_(item.get(), now);
LockGuard guard{this->lock_};
@@ -568,11 +568,11 @@ void HOT Scheduler::pop_raw_() {
}
// Helper to execute a scheduler item
void HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
App.set_current_component(item->component);
WarnIfComponentBlockingGuard guard{item->component, now};
item->callback();
guard.finish();
return guard.finish();
}
// Common implementation for cancel operations

View File

@@ -254,7 +254,7 @@ class Scheduler {
}
// Helper to execute a scheduler item
void execute_item_(SchedulerItem *item, uint32_t now);
uint32_t execute_item_(SchedulerItem *item, uint32_t now);
// Helper to check if item should be skipped
bool should_skip_item_(SchedulerItem *item) const {