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:
2
Doxyfile
2
Doxyfile
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user