Merge branch 'runtime_use_address' into integration

This commit is contained in:
J. Nick Koston
2026-07-06 21:18:30 -05:00
15 changed files with 154 additions and 34 deletions
+2 -1
View File
@@ -240,12 +240,13 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
}
void APIServer::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Server:\n"
" Address: %s:%u\n"
" Listen backlog: %u\n"
" Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
network::get_use_address_to(addr_buf), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
@@ -94,11 +94,12 @@ void ESPHomeOTAComponent::setup() {
}
void ESPHomeOTAComponent::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Over-The-Air updates:\n"
" Address: %s:%u\n"
" Version: %d",
network::get_use_address(), this->port_, USE_OTA_VERSION);
network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION);
#ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) {
ESP_LOGCONFIG(TAG, " Password configured");
+2 -2
View File
@@ -4,7 +4,7 @@ import logging
from esphome import automation, pins
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.network import ip_address_literal
from esphome.components.network import add_use_address, ip_address_literal
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -543,7 +543,7 @@ async def to_code(config):
await _to_code_rp2040(var, config)
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
add_use_address(var, config[CONF_USE_ADDRESS])
# enable_on_boot defaults to true in C++ - only set if false
if not config[CONF_ENABLE_ON_BOOT]:
cg.add(var.set_enable_on_boot(False))
@@ -145,6 +145,8 @@ class EthernetComponent final : public Component {
network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num);
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
void get_eth_mac_address_raw(uint8_t *mac);
@@ -346,7 +348,7 @@ class EthernetComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{""};
const char *use_address_{nullptr};
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
+13
View File
@@ -59,6 +59,19 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj:
return IPAddress(str(ip))
def add_use_address(var: cg.MockObj, use_address: str) -> None:
"""Generate a set_use_address() call only when the address must be baked in.
The default "<name>.local" is not stored in the firmware; it is rebuilt at
runtime from the device name (see network::get_use_address_to()), which also
picks up the MAC suffix when name_add_mac_suffix is enabled. A compile-time
string could never include that suffix, so baking it in would log the wrong
address.
"""
if use_address != f"{CORE.name}.local":
cg.add(var.set_use_address(use_address))
def require_high_performance_networking() -> None:
"""Request high performance networking for network and WiFi.
+23
View File
@@ -1,5 +1,7 @@
#include "util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_NETWORK
namespace esphome::network {
@@ -20,6 +22,27 @@ bool is_disabled() {
return false;
}
const char *get_use_address_to(std::span<char, USE_ADDRESS_BUFFER_SIZE> buf) {
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
const char *addr = nullptr;
#if defined(USE_ETHERNET)
addr = ethernet::global_eth_component->get_use_address();
#elif defined(USE_MODEM)
addr = modem::global_modem_component->get_use_address();
#elif defined(USE_WIFI)
addr = wifi::global_wifi_component->get_use_address();
#elif defined(USE_OPENTHREAD)
addr = openthread::global_openthread_component->get_use_address();
#endif
if (addr != nullptr && addr[0] != '\0')
return addr;
// No explicit use_address configured: the address is the runtime device name
// (which includes the MAC suffix when name_add_mac_suffix is enabled) plus ".local"
const auto &name = App.get_name();
make_name_with_suffix_to(buf.data(), buf.size(), name.c_str(), name.size(), '.', "local", 5);
return buf.data();
}
network::IPAddresses get_ip_addresses() {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr)
+7 -24
View File
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#include <span>
#include <string>
#include "esphome/core/helpers.h"
#include "ip_address.h"
@@ -53,30 +54,12 @@ ESPHOME_ALWAYS_INLINE inline bool is_connected() {
/// Return whether the network is disabled (only wifi for now)
bool is_disabled();
/// Get the active network hostname
ESPHOME_ALWAYS_INLINE inline const char *get_use_address() {
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
#ifdef USE_ETHERNET
return ethernet::global_eth_component->get_use_address();
#endif
#ifdef USE_MODEM
return modem::global_modem_component->get_use_address();
#endif
#ifdef USE_WIFI
return wifi::global_wifi_component->get_use_address();
#endif
#ifdef USE_OPENTHREAD
return openthread::global_openthread_component->get_use_address();
#endif
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD)
// Fallback when no network component is defined (e.g., host platform)
return "";
#endif
}
/// Buffer size for get_use_address_to(): 63-char DNS label + ".local" + null terminator
static constexpr size_t USE_ADDRESS_BUFFER_SIZE = 70;
/// Get the active network address for logging. Returns the explicitly configured
/// use_address when one was set, otherwise formats "<name>.local" from the runtime
/// device name into buf (so it includes the MAC suffix from name_add_mac_suffix).
const char *get_use_address_to(std::span<char, USE_ADDRESS_BUFFER_SIZE> buf);
IPAddresses get_ip_addresses();
} // namespace esphome::network
+2 -1
View File
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
require_vfs_select,
)
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
from esphome.components.network import add_use_address
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
@@ -288,7 +289,7 @@ async def to_code(config):
enable_mdns_storage()
ot = cg.new_Pvariable(config[CONF_ID])
cg.add(ot.set_use_address(config[CONF_USE_ADDRESS]))
add_use_address(ot, config[CONF_USE_ADDRESS])
await cg.register_component(ot, config)
if (poll_period := config.get(CONF_POLL_PERIOD)) is not None:
cg.add(ot.set_poll_period(poll_period))
+3 -1
View File
@@ -39,6 +39,8 @@ class OpenThreadComponent final : public Component {
void on_factory_reset(std::function<void()> callback);
void defer_factory_reset_external_callback();
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
#if CONFIG_OPENTHREAD_MTD
@@ -76,7 +78,7 @@ class OpenThreadComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{""};
const char *use_address_{nullptr};
};
extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+2 -1
View File
@@ -421,10 +421,11 @@ void WebServer::on_log(uint8_t level, const char *tag, const char *message, size
#endif
void WebServer::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
"Web Server:\n"
" Address: %s:%u",
network::get_use_address(), this->base_->get_port());
network::get_use_address_to(addr_buf), this->base_->get_port());
}
float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
+2 -1
View File
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
request_wifi,
)
from esphome.components.network import (
add_use_address,
has_high_performance_networking,
ip_address_literal,
)
@@ -585,7 +586,7 @@ def wifi_network(config, ap, static_ip):
@coroutine_with_priority(CoroPriority.COMMUNICATION)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
add_use_address(var, config[CONF_USE_ADDRESS])
# Track if any network uses Enterprise authentication
has_eap = False
+3 -1
View File
@@ -501,6 +501,8 @@ class WiFiComponent final : public Component {
network::IPAddress get_dns_address(int num);
network::IPAddresses get_ip_addresses();
/// Returns nullptr when no explicit use_address is configured and the address is
/// derived at runtime from the device name (see network::get_use_address_to()).
const char *get_use_address() const { return this->use_address_; }
void set_use_address(const char *use_address) { this->use_address_ = use_address; }
@@ -996,7 +998,7 @@ class WiFiComponent final : public Component {
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{""};
const char *use_address_{nullptr};
};
extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -0,0 +1,8 @@
esphome:
name: use-address-runtime
host:
api:
logger:
@@ -0,0 +1,9 @@
esphome:
name: use-address-mac
name_add_mac_suffix: true
host:
api:
logger:
@@ -0,0 +1,73 @@
"""Integration tests for the runtime-built use_address.
The default "<name>.local" address is no longer stored as a compile-time string;
it is built at runtime from the device name. This also fixes the logged address
when name_add_mac_suffix is enabled: the baked string used to miss the MAC
suffix, so it never matched the actual mDNS hostname.
"""
from __future__ import annotations
import asyncio
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
# Host platform default MAC: 98:35:69:ab:f6:79 -> suffix "abf679"
MAC_SUFFIX = "abf679"
@pytest.mark.asyncio
async def test_use_address_runtime(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""The API dump_config logs "<name>.local" built from the device name."""
address_seen = asyncio.Event()
def check_output(line: str) -> None:
if "Address: use-address-runtime.local:" in line:
address_seen.set()
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "use-address-runtime"
try:
await asyncio.wait_for(address_seen.wait(), timeout=10.0)
except TimeoutError:
pytest.fail("Did not log 'Address: use-address-runtime.local:'")
@pytest.mark.asyncio
async def test_use_address_runtime_mac_suffix(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""With name_add_mac_suffix the logged address includes the MAC suffix."""
address_seen = asyncio.Event()
expected = f"Address: use-address-mac-{MAC_SUFFIX}.local:"
def check_output(line: str) -> None:
if expected in line:
address_seen.set()
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == f"use-address-mac-{MAC_SUFFIX}"
try:
await asyncio.wait_for(address_seen.wait(), timeout=10.0)
except TimeoutError:
pytest.fail(f"Did not log '{expected}'")