1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 08:41:59 +00:00

Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2026-01-14 12:46:10 -10:00
11 changed files with 153 additions and 62 deletions

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.1.0-dev
PROJECT_NUMBER = 2026.2.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -61,6 +61,21 @@ DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["network"]
LOGGER = logging.getLogger(__name__)
# Key for tracking IP state listener count in CORE.data
ETHERNET_IP_STATE_LISTENERS_KEY = "ethernet_ip_state_listeners"
def request_ethernet_ip_state_listener() -> None:
"""Request an IP state listener slot.
Components that implement EthernetIPStateListener should call this
in their to_code() to register for IP state notifications.
"""
CORE.data[ETHERNET_IP_STATE_LISTENERS_KEY] = (
CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0) + 1
)
# RMII pins that are hardcoded on ESP32 classic and cannot be changed
# These pins are used by the internal Ethernet MAC when using RMII PHYs
ESP32_RMII_FIXED_PINS = {
@@ -411,6 +426,8 @@ async def to_code(config):
if CORE.using_arduino:
cg.add_library("WiFi", None)
CORE.add_job(final_step)
def _final_validate_rmii_pins(config: ConfigType) -> None:
"""Validate that RMII pins are not used by other components."""
@@ -467,3 +484,11 @@ def _final_validate(config: ConfigType) -> ConfigType:
FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure optional Ethernet features."""
if ip_state_count := CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0):
cg.add_define("USE_ETHERNET_IP_STATE_LISTENERS")
cg.add_define("ESPHOME_ETHERNET_IP_STATE_LISTENERS", ip_state_count)

View File

@@ -472,6 +472,12 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
break;
case ETHERNET_EVENT_CONNECTED:
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
if (global_eth_component->manual_ip_.has_value()) {
global_eth_component->notify_ip_state_listeners_();
}
#endif
break;
case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected";
@@ -498,6 +504,9 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#if USE_NETWORK_IPV6
@@ -514,9 +523,23 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::notify_ip_state_listeners_() {
auto ips = this->get_ip_addresses();
auto dns1 = this->get_dns_address(0);
auto dns2 = this->get_dns_address(1);
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(ips, dns1, dns2);
}
}
#endif // USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::finish_connect_() {
#if USE_NETWORK_IPV6
// Retry IPv6 link-local setup if it failed during initial connect

View File

@@ -17,6 +17,22 @@
namespace esphome {
namespace ethernet {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
/** Listener interface for Ethernet IP state changes.
*
* Components can implement this interface to receive IP address updates
* without the overhead of std::function callbacks or polling.
*
* @note Components must call ethernet.request_ethernet_ip_state_listener() in their
* Python to_code() to register for this listener type.
*/
class EthernetIPStateListener {
public:
virtual void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) = 0;
};
#endif // USE_ETHERNET_IP_STATE_LISTENERS
enum EthernetType : uint8_t {
ETHERNET_TYPE_UNKNOWN = 0,
ETHERNET_TYPE_LAN8720,
@@ -99,12 +115,19 @@ class EthernetComponent : public Component {
eth_speed_t get_link_speed();
bool powerdown();
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); }
#endif
protected:
static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
#if LWIP_IPV6
static void got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
#endif /* LWIP_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void notify_ip_state_listeners_();
#endif
void start_connect_();
void finish_connect_();
@@ -163,6 +186,10 @@ class EthernetComponent : public Component {
esp_eth_phy_t *phy_{nullptr};
optional<std::array<uint8_t, 6>> fixed_mac_;
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
StaticVector<EthernetIPStateListener *, ESPHOME_ETHERNET_IP_STATE_LISTENERS> ip_state_listeners_;
#endif
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.

View File

@@ -7,8 +7,44 @@ namespace esphome::ethernet_info {
static const char *const TAG = "ethernet_info";
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void IPAddressEthernetInfo::setup() { ethernet::global_eth_component->add_ip_state_listener(this); }
void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IPAddress", this); }
void IPAddressEthernetInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) {
char buf[network::IP_ADDRESS_BUFFER_SIZE];
ips[0].str_to(buf);
this->publish_state(buf);
uint8_t sensor = 0;
for (const auto &ip : ips) {
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
}
}
}
void DNSAddressEthernetInfo::setup() { ethernet::global_eth_component->add_ip_state_listener(this); }
void DNSAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo DNS Address", this); }
void DNSAddressEthernetInfo::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) {
// IP_ADDRESS_BUFFER_SIZE (40) = max IP (39) + null; space reuses first null's slot
char buf[network::IP_ADDRESS_BUFFER_SIZE * 2];
dns1.str_to(buf);
size_t len1 = strlen(buf);
buf[len1] = ' ';
dns2.str_to(buf + len1 + 1);
this->publish_state(buf);
}
#endif // USE_ETHERNET_IP_STATE_LISTENERS
void MACAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo MAC Address", this); }
} // namespace esphome::ethernet_info

View File

@@ -8,64 +8,37 @@
namespace esphome::ethernet_info {
class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
class IPAddressEthernetInfo final : public Component,
public text_sensor::TextSensor,
public ethernet::EthernetIPStateListener {
public:
void update() override {
auto ips = ethernet::global_eth_component->get_ip_addresses();
if (ips != this->last_ips_) {
this->last_ips_ = ips;
char buf[network::IP_ADDRESS_BUFFER_SIZE];
ips[0].str_to(buf);
this->publish_state(buf);
uint8_t sensor = 0;
for (auto &ip : ips) {
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
}
}
}
}
float get_setup_priority() const override { return setup_priority::ETHERNET; }
void setup() override;
void dump_config() override;
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
// EthernetIPStateListener interface
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
protected:
network::IPAddresses last_ips_;
std::array<text_sensor::TextSensor *, 5> ip_sensors_;
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
};
class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor {
class DNSAddressEthernetInfo final : public Component,
public text_sensor::TextSensor,
public ethernet::EthernetIPStateListener {
public:
void update() override {
auto dns1 = ethernet::global_eth_component->get_dns_address(0);
auto dns2 = ethernet::global_eth_component->get_dns_address(1);
if (dns1 != this->last_dns1_ || dns2 != this->last_dns2_) {
this->last_dns1_ = dns1;
this->last_dns2_ = dns2;
// IP_ADDRESS_BUFFER_SIZE (40) = max IP (39) + null; space reuses first null's slot
char buf[network::IP_ADDRESS_BUFFER_SIZE * 2];
dns1.str_to(buf);
size_t len1 = strlen(buf);
buf[len1] = ' ';
dns2.str_to(buf + len1 + 1);
this->publish_state(buf);
}
}
float get_setup_priority() const override { return setup_priority::ETHERNET; }
void setup() override;
void dump_config() override;
protected:
network::IPAddress last_dns1_;
network::IPAddress last_dns2_;
// EthernetIPStateListener interface
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
};
#endif // USE_ETHERNET_IP_STATE_LISTENERS
class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor {
class MACAddressEthernetInfo final : public Component, public text_sensor::TextSensor {
public:
void setup() override {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import text_sensor
from esphome.components import ethernet, text_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_DNS_ADDRESS,
@@ -13,24 +13,22 @@ DEPENDENCIES = ["ethernet"]
ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info")
IPAddressEthernetInfo = ethernet_info_ns.class_(
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.Component
)
DNSAddressEthernetInfo = ethernet_info_ns.class_(
"DNSAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent
"DNSAddressEthernetInfo", text_sensor.TextSensor, cg.Component
)
MACAddressEthernetInfo = ethernet_info_ns.class_(
"MACAddressEthernetInfo", text_sensor.TextSensor, cg.PollingComponent
"MACAddressEthernetInfo", text_sensor.TextSensor, cg.Component
)
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema(
IPAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
)
.extend(cv.polling_component_schema("1s"))
.extend(
).extend(
{
cv.Optional(f"address_{x}"): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
@@ -40,7 +38,7 @@ CONFIG_SCHEMA = cv.Schema(
),
cv.Optional(CONF_DNS_ADDRESS): text_sensor.text_sensor_schema(
DNSAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
).extend(cv.polling_component_schema("1s")),
),
cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema(
MACAddressEthernetInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
@@ -49,6 +47,12 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
# Request Ethernet IP state listener slots - one per sensor type
if CONF_IP_ADDRESS in config:
ethernet.request_ethernet_ip_state_listener()
if CONF_DNS_ADDRESS in config:
ethernet.request_ethernet_ip_state_listener()
if conf := config.get(CONF_IP_ADDRESS):
ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
await cg.register_component(ip_info, config[CONF_IP_ADDRESS])
@@ -57,8 +61,8 @@ async def to_code(config):
sens = await text_sensor.new_text_sensor(sensor_conf)
cg.add(ip_info.add_ip_sensors(x, sens))
if conf := config.get(CONF_DNS_ADDRESS):
dns_info = await text_sensor.new_text_sensor(config[CONF_DNS_ADDRESS])
await cg.register_component(dns_info, config[CONF_DNS_ADDRESS])
dns_info = await text_sensor.new_text_sensor(conf)
await cg.register_component(dns_info, conf)
if conf := config.get(CONF_MAC_ADDRESS):
mac_info = await text_sensor.new_text_sensor(config[CONF_MAC_ADDRESS])
await cg.register_component(mac_info, config[CONF_MAC_ADDRESS])
mac_info = await text_sensor.new_text_sensor(conf)
await cg.register_component(mac_info, conf)

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.1.0-dev"
__version__ = "2026.2.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -238,6 +238,8 @@
#define USE_ETHERNET
#define USE_ETHERNET_KSZ8081
#define USE_ETHERNET_MANUAL_IP
#define USE_ETHERNET_IP_STATE_LISTENERS
#define ESPHOME_ETHERNET_IP_STATE_LISTENERS 2
#endif
#ifdef USE_ESP32

View File

@@ -115,8 +115,9 @@ inline bool operator!=(const char *lhs, const StringRef &rhs) { return !(rhs ==
inline bool operator==(const StringRef &lhs, const __FlashStringHelper *rhs) {
PGM_P p = reinterpret_cast<PGM_P>(rhs);
size_t rhs_len = strlen_P(p);
if (lhs.size() != rhs_len)
if (lhs.size() != rhs_len) {
return false;
}
return memcmp_P(lhs.c_str(), p, rhs_len) == 0;
}

View File

@@ -19,7 +19,7 @@ ruamel.yaml==0.19.1 # dashboard_import
ruamel.yaml.clib==0.2.15 # dashboard_import
esphome-glyphsets==0.2.0
pillow==11.3.0
resvg-py==0.2.5
resvg-py==0.2.6
freetype-py==2.5.1
jinja2==3.1.6
bleak==2.1.1