mirror of
https://github.com/esphome/esphome.git
synced 2026-10-27 08:22:25 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1031f74457 | ||
|
|
b5b4a5b642 | ||
|
|
8e5b65cf8a |
No files matched your search
@@ -1,3 +1,4 @@
|
||||
from ipaddress import IPv4Address
|
||||
import logging
|
||||
import math
|
||||
from typing import Any
|
||||
@@ -19,7 +20,6 @@ from esphome.components.network import (
|
||||
add_use_address,
|
||||
get_network_priority,
|
||||
has_high_performance_wifi,
|
||||
ip_address_literal,
|
||||
require_high_performance_wifi,
|
||||
require_ipv4,
|
||||
)
|
||||
@@ -190,7 +190,6 @@ DEFAULT_DPP_TIMEOUT = "1min"
|
||||
|
||||
wifi_ns = cg.esphome_ns.namespace("wifi")
|
||||
EAPAuth = wifi_ns.struct("EAPAuth")
|
||||
ManualIP = wifi_ns.struct("ManualIP")
|
||||
WiFiComponent = wifi_ns.class_("WiFiComponent", cg.Component)
|
||||
WiFiAP = wifi_ns.struct("WiFiAP")
|
||||
|
||||
@@ -804,24 +803,20 @@ def eap_auth(config):
|
||||
)
|
||||
|
||||
|
||||
def safe_ip(ip):
|
||||
return ip_address_literal(ip)
|
||||
|
||||
|
||||
def manual_ip(config):
|
||||
if config is None:
|
||||
return None
|
||||
return cg.StructInitializer(
|
||||
ManualIP,
|
||||
("static_ip", safe_ip(config[CONF_STATIC_IP])),
|
||||
("gateway", safe_ip(config[CONF_GATEWAY])),
|
||||
("subnet", safe_ip(config[CONF_SUBNET])),
|
||||
("dns1", safe_ip(config.get(CONF_DNS1))),
|
||||
("dns2", safe_ip(config.get(CONF_DNS2))),
|
||||
def manual_ip_entry(config: ConfigType) -> bytes:
|
||||
"""Pack manual IP settings as one MANUAL_IP_TABLE row (MANUAL_IP_ENTRY_SIZE bytes)."""
|
||||
no_dns = IPv4Address(0)
|
||||
addresses = (
|
||||
config[CONF_STATIC_IP],
|
||||
config[CONF_GATEWAY],
|
||||
config[CONF_SUBNET],
|
||||
config.get(CONF_DNS1, no_dns),
|
||||
config.get(CONF_DNS2, no_dns),
|
||||
)
|
||||
return b"".join(address.packed for address in addresses)
|
||||
|
||||
|
||||
def wifi_network(config, ap, static_ip):
|
||||
def wifi_network(config, ap, manual_ip_index: int | None):
|
||||
cg.add_optional(ap.set_ssid, config.get(CONF_SSID))
|
||||
cg.add_optional(ap.set_password, config.get(CONF_PASSWORD))
|
||||
if CONF_EAP in config:
|
||||
@@ -831,8 +826,7 @@ def wifi_network(config, ap, static_ip):
|
||||
cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts]))
|
||||
cg.add_optional(ap.set_hidden, config.get(CONF_HIDDEN))
|
||||
cg.add_optional(ap.set_channel, config.get(CONF_CHANNEL))
|
||||
if static_ip is not None:
|
||||
cg.add(ap.set_manual_ip(manual_ip(static_ip)))
|
||||
cg.add_optional(ap.set_manual_ip, manual_ip_index)
|
||||
# priority_ is 0 in C++; skip the setter when the config matches it.
|
||||
if config.get(CONF_PRIORITY, 0) != 0:
|
||||
cg.add(ap.set_priority(config[CONF_PRIORITY]))
|
||||
@@ -851,8 +845,17 @@ async def to_code(config):
|
||||
|
||||
# Track if any network uses Enterprise authentication
|
||||
has_eap = False
|
||||
# Track if any network uses manual IP
|
||||
has_manual_ip = False
|
||||
# Rows of MANUAL_IP_TABLE; networks with the same settings share one. At most
|
||||
# MAX_WIFI_NETWORKS + 1 (the AP) rows, so an index never reaches MANUAL_IP_NONE (0xFF).
|
||||
manual_ip_entries: list[bytes] = []
|
||||
|
||||
def manual_ip_index(ip_config: ConfigType | None) -> int | None:
|
||||
if ip_config is None:
|
||||
return None
|
||||
entry = manual_ip_entry(ip_config)
|
||||
if entry not in manual_ip_entries:
|
||||
manual_ip_entries.append(entry)
|
||||
return manual_ip_entries.index(entry)
|
||||
|
||||
# Initialize FixedVector with the count of networks
|
||||
networks = config.get(CONF_NETWORKS, [])
|
||||
@@ -864,24 +867,20 @@ async def to_code(config):
|
||||
|
||||
def add_sta(ap: cg.MockObj, network: dict) -> None:
|
||||
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
|
||||
cg.add(var.add_sta(wifi_network(network, ap, ip_config)))
|
||||
cg.add(var.add_sta(wifi_network(network, ap, manual_ip_index(ip_config))))
|
||||
|
||||
for network in networks:
|
||||
if CONF_EAP in network:
|
||||
has_eap = True
|
||||
if network.get(CONF_MANUAL_IP) or config.get(CONF_MANUAL_IP):
|
||||
has_manual_ip = True
|
||||
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
|
||||
|
||||
if CONF_AP in config:
|
||||
conf = config[CONF_AP]
|
||||
ip_config = conf.get(CONF_MANUAL_IP)
|
||||
if ip_config:
|
||||
has_manual_ip = True
|
||||
ap_ip_index = manual_ip_index(conf.get(CONF_MANUAL_IP))
|
||||
cg.with_local_variable(
|
||||
conf[CONF_ID],
|
||||
WiFiAP(),
|
||||
lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))),
|
||||
lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ap_ip_index))),
|
||||
)
|
||||
# Skip the setter when the config matches the C++ initializer.
|
||||
if (ap_timeout := conf[CONF_AP_TIMEOUT]) != DEFAULT_AP_TIMEOUT_PERIOD:
|
||||
@@ -939,8 +938,13 @@ async def to_code(config):
|
||||
require_mbedtls_tls_extras()
|
||||
|
||||
# Only define USE_WIFI_MANUAL_IP if any AP uses manual IP
|
||||
if has_manual_ip:
|
||||
if manual_ip_entries:
|
||||
cg.add_define("USE_WIFI_MANUAL_IP")
|
||||
cg.extern_progmem_array(
|
||||
"esphome::wifi::MANUAL_IP_TABLE",
|
||||
cg.uint8,
|
||||
list(b"".join(manual_ip_entries)),
|
||||
)
|
||||
|
||||
# The C++ initializers are DEFAULT_REBOOT_TIMEOUT, power save NONE and minimum
|
||||
# auth WPA2; skip the setters when the config matches them.
|
||||
|
||||
@@ -1360,17 +1360,15 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
ESP_LOGV(TAG, " Channel not set");
|
||||
}
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
auto manual_ip = ap.get_manual_ip();
|
||||
if (manual_ip.has_value()) {
|
||||
ManualIP m = *manual_ip;
|
||||
if (const auto m = ap.get_manual_ip(); m.has_value()) {
|
||||
char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str_to(static_ip_buf),
|
||||
m.gateway.str_to(gateway_buf), m.subnet.str_to(subnet_buf), m.dns1.str_to(dns1_buf),
|
||||
m.dns2.str_to(dns2_buf));
|
||||
ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m->static_ip.str_to(static_ip_buf),
|
||||
m->gateway.str_to(gateway_buf), m->subnet.str_to(subnet_buf), m->dns1.str_to(dns1_buf),
|
||||
m->dns2.str_to(dns2_buf));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -1908,7 +1906,7 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
||||
#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
// On ESP8266, GOT_IP event may not fire for static IP configurations,
|
||||
// so notify IP state listeners here as a fallback.
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_manual_ip()) {
|
||||
this->notify_ip_state_listeners_();
|
||||
}
|
||||
#endif
|
||||
@@ -2881,6 +2879,17 @@ void WiFiComponent::process_roaming_scan_() {
|
||||
this->start_connecting(roam_params);
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
optional<ManualIP> WiFiAP::get_manual_ip() const {
|
||||
if (!this->has_manual_ip())
|
||||
return {};
|
||||
uint8_t b[MANUAL_IP_ENTRY_SIZE];
|
||||
progmem_memcpy(b, MANUAL_IP_TABLE + this->manual_ip_index_ * MANUAL_IP_ENTRY_SIZE, sizeof(b));
|
||||
auto ip = [&b](size_t i) { return network::IPAddress(b[i], b[i + 1], b[i + 2], b[i + 3]); };
|
||||
return ManualIP{ip(0), ip(4), ip(8), ip(12), ip(16)};
|
||||
}
|
||||
#endif
|
||||
|
||||
WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome::wifi
|
||||
|
||||
@@ -188,6 +188,14 @@ struct ManualIP {
|
||||
network::IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default.
|
||||
};
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
/// One MANUAL_IP_TABLE row: static IP, gateway, subnet, dns1 and dns2 as 4 octets each.
|
||||
static constexpr size_t MANUAL_IP_ENTRY_SIZE = 20;
|
||||
static constexpr uint8_t MANUAL_IP_NONE = 0xFF;
|
||||
/// Every configured manual IP, generated by codegen (components/wifi/__init__.py) and kept in flash.
|
||||
extern const uint8_t MANUAL_IP_TABLE[] PROGMEM;
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
struct EAPAuth {
|
||||
std::string identity; // required for all auth types
|
||||
@@ -300,7 +308,8 @@ class WiFiAP {
|
||||
void clear_channel() { this->channel_ = 0; }
|
||||
void set_priority(int8_t priority) { priority_ = priority; }
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
void set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
/// index is a row of MANUAL_IP_TABLE
|
||||
void set_manual_ip(uint8_t index) { this->manual_ip_index_ = index; }
|
||||
#endif
|
||||
void set_hidden(bool hidden) { this->hidden_ = hidden; }
|
||||
StringRef get_ssid() const { return this->ssid_.ref(); }
|
||||
@@ -314,7 +323,8 @@ class WiFiAP {
|
||||
bool has_channel() const { return this->channel_ != 0; }
|
||||
int8_t get_priority() const { return priority_; }
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
const optional<ManualIP> &get_manual_ip() const { return this->manual_ip_; }
|
||||
bool has_manual_ip() const { return this->manual_ip_index_ != MANUAL_IP_NONE; }
|
||||
optional<ManualIP> get_manual_ip() const;
|
||||
#endif
|
||||
bool get_hidden() const { return this->hidden_; }
|
||||
|
||||
@@ -324,14 +334,14 @@ class WiFiAP {
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
optional<EAPAuth> eap_;
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
optional<ManualIP> manual_ip_;
|
||||
#endif
|
||||
// Group small types together to minimize padding
|
||||
bssid_t bssid_{}; // 6 bytes, all zeros = any/not set
|
||||
uint8_t channel_{0}; // 1 byte, 0 = auto/not set
|
||||
int8_t priority_{0}; // 1 byte
|
||||
bool hidden_{false}; // 1 byte; WiFiAP is byte aligned unless manual IP or EAP adds a 4 byte aligned member
|
||||
bool hidden_{false}; // 1 byte; WiFiAP is byte aligned unless EAP adds a 4 byte aligned member
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
uint8_t manual_ip_index_{MANUAL_IP_NONE};
|
||||
#endif
|
||||
};
|
||||
|
||||
class WiFiScanResult {
|
||||
@@ -1050,7 +1060,7 @@ class WiFiComponent final : public Component {
|
||||
uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed
|
||||
#ifdef USE_WIFI_AP
|
||||
uint32_t ap_timeout_{90000}; // Keep in sync with DEFAULT_AP_TIMEOUT in __init__.py
|
||||
// WiFiAP is byte aligned unless manual IP or EAP is enabled; placed before the
|
||||
// WiFiAP is byte aligned unless EAP is enabled; placed before the
|
||||
// 1-byte members so they pack into its trailing bytes instead of padding after it
|
||||
WiFiAP ap_;
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
@@ -795,7 +795,7 @@ void WiFiComponent::save_lease_settings_() {
|
||||
return;
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value())
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_manual_ip())
|
||||
return;
|
||||
#endif
|
||||
|
||||
@@ -1093,7 +1093,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
bool static_ip = false;
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
const WiFiAP *config = this->get_selected_sta_();
|
||||
static_ip = config && config->get_manual_ip().has_value();
|
||||
static_ip = config && config->has_manual_ip();
|
||||
#endif
|
||||
#ifdef USE_WIFI_LEASE_CACHE
|
||||
static_ip = static_ip || this->applied_cached_lease_;
|
||||
|
||||
@@ -510,7 +510,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||
#endif
|
||||
// For static IP configurations, GOT_IP event may not fire, so set connected state here
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_manual_ip()) {
|
||||
this->sta_state_ = static_cast<uint8_t>(LTWiFiSTAState::CONNECTED);
|
||||
#ifdef USE_WIFI_IP_STATE_LISTENERS
|
||||
this->notify_ip_state_listeners_();
|
||||
|
||||
@@ -342,7 +342,7 @@ bool WiFiComponent::wifi_loop_() {
|
||||
#endif
|
||||
// For static IP configurations, notify IP listeners immediately as the IP is already configured
|
||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_manual_ip()) {
|
||||
s_sta_had_ip = true;
|
||||
this->notify_ip_state_listeners_();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
# Inherited by every network without its own manual_ip
|
||||
manual_ip:
|
||||
static_ip: 192.168.2.20
|
||||
gateway: 192.168.2.1
|
||||
subnet: 255.255.255.0
|
||||
dns1: 192.168.2.1
|
||||
networks:
|
||||
- ssid: own
|
||||
password: password1
|
||||
manual_ip:
|
||||
static_ip: 192.168.1.10
|
||||
gateway: 192.168.1.1
|
||||
subnet: 255.255.255.0
|
||||
dns1: 1.1.1.1
|
||||
dns2: 8.8.8.8
|
||||
- ssid: inherits1
|
||||
password: password2
|
||||
- ssid: inherits2
|
||||
password: password3
|
||||
ap:
|
||||
password: fallback_password
|
||||
manual_ip:
|
||||
static_ip: 192.168.4.1
|
||||
gateway: 192.168.4.1
|
||||
subnet: 255.255.255.0
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Tests for the manual IP table wifi codegen emits (MANUAL_IP_TABLE in wifi_component.h)."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def test_manual_ip_rows_are_shared(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Networks with the same settings share a row; the AP row packs its missing DNS as zeros."""
|
||||
main_cpp = generate_main(component_config_path("manual_ip.yaml"))
|
||||
|
||||
own = "192, 168, 1, 10, 192, 168, 1, 1, 255, 255, 255, 0, 1, 1, 1, 1, 8, 8, 8, 8"
|
||||
inherited = (
|
||||
"192, 168, 2, 20, 192, 168, 2, 1, 255, 255, 255, 0, 192, 168, 2, 1, 0, 0, 0, 0"
|
||||
)
|
||||
ap = "192, 168, 4, 1, 192, 168, 4, 1, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0"
|
||||
assert (
|
||||
f"esphome::wifi::MANUAL_IP_TABLE[] PROGMEM = {{{own}, {inherited}, {ap}}};"
|
||||
in main_cpp
|
||||
)
|
||||
# Three networks in config order, then the AP
|
||||
assert re.findall(r"set_manual_ip\((\d+)\)", main_cpp) == ["0", "1", "1", "2"]
|
||||
@@ -42,6 +42,11 @@ wifi:
|
||||
- ssid: MySSID2
|
||||
password: password2
|
||||
priority: 5
|
||||
manual_ip:
|
||||
static_ip: 192.168.2.100
|
||||
gateway: 192.168.2.1
|
||||
subnet: 255.255.255.0
|
||||
dns1: 192.168.2.1
|
||||
- ssid: MySSID3
|
||||
password: password3
|
||||
priority: 0
|
||||
|
||||
@@ -5,6 +5,10 @@ wifi:
|
||||
ap:
|
||||
ssid: Fallback AP
|
||||
password: fallback_password
|
||||
manual_ip:
|
||||
static_ip: 192.168.4.1
|
||||
gateway: 192.168.4.1
|
||||
subnet: 255.255.255.0
|
||||
|
||||
packages:
|
||||
- !include common.yaml
|
||||
Reference in new issue
Block a user