mirror of
https://github.com/esphome/esphome.git
synced 2025-09-26 07:02:21 +01:00
Merge branch 'dev' into core_api_no_allocate
This commit is contained in:
@@ -731,6 +731,16 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return clean_mqtt(config, args)
|
||||
|
||||
|
||||
def command_clean_platform(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
try:
|
||||
writer.clean_platform()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error deleting platform files: %s", err)
|
||||
return 1
|
||||
_LOGGER.info("Done!")
|
||||
return 0
|
||||
|
||||
|
||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import mqtt
|
||||
|
||||
@@ -929,9 +939,10 @@ POST_CONFIG_ACTIONS = {
|
||||
"upload": command_upload,
|
||||
"logs": command_logs,
|
||||
"run": command_run,
|
||||
"clean-mqtt": command_clean_mqtt,
|
||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||
"clean": command_clean,
|
||||
"clean-mqtt": command_clean_mqtt,
|
||||
"clean-platform": command_clean_platform,
|
||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
@@ -940,6 +951,7 @@ POST_CONFIG_ACTIONS = {
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
"clean",
|
||||
"clean-mqtt",
|
||||
"clean-platform",
|
||||
"config",
|
||||
]
|
||||
|
||||
@@ -1144,6 +1156,13 @@ def parse_args(argv):
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
|
||||
parser_clean = subparsers.add_parser(
|
||||
"clean-platform", help="Delete all platform files."
|
||||
)
|
||||
parser_clean.add_argument(
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
|
||||
parser_dashboard = subparsers.add_parser(
|
||||
"dashboard", help="Create a simple web server for a dashboard."
|
||||
)
|
||||
|
@@ -1571,7 +1571,7 @@ message BluetoothGATTWriteRequest {
|
||||
uint32 handle = 2;
|
||||
bool response = 3;
|
||||
|
||||
bytes data = 4;
|
||||
bytes data = 4 [(pointer_to_buffer) = true];
|
||||
}
|
||||
|
||||
message BluetoothGATTReadDescriptorRequest {
|
||||
@@ -1591,7 +1591,7 @@ message BluetoothGATTWriteDescriptorRequest {
|
||||
uint64 address = 1;
|
||||
uint32 handle = 2;
|
||||
|
||||
bytes data = 3;
|
||||
bytes data = 3 [(pointer_to_buffer) = true];
|
||||
}
|
||||
|
||||
message BluetoothGATTNotifyRequest {
|
||||
|
@@ -2037,9 +2037,12 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val
|
||||
}
|
||||
bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 4:
|
||||
this->data = value.as_string();
|
||||
case 4: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -2073,9 +2076,12 @@ bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, Proto
|
||||
}
|
||||
bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 3:
|
||||
this->data = value.as_string();
|
||||
case 3: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@@ -1988,14 +1988,15 @@ class BluetoothGATTReadResponse final : public ProtoMessage {
|
||||
class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 75;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 29;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_gatt_write_request"; }
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
uint32_t handle{0};
|
||||
bool response{false};
|
||||
std::string data{};
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2023,13 +2024,14 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
|
||||
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 77;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; }
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
uint32_t handle{0};
|
||||
std::string data{};
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
@@ -1658,7 +1658,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "handle", this->handle);
|
||||
dump_field(out, "response", this->response);
|
||||
out.append(" data: ");
|
||||
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()));
|
||||
out.append(format_hex_pretty(this->data, this->data_len));
|
||||
out.append("\n");
|
||||
}
|
||||
void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
|
||||
@@ -1671,7 +1671,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "address", this->address);
|
||||
dump_field(out, "handle", this->handle);
|
||||
out.append(" data: ");
|
||||
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()));
|
||||
out.append(format_hex_pretty(this->data, this->data_len));
|
||||
out.append("\n");
|
||||
}
|
||||
void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
|
||||
|
@@ -514,7 +514,8 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
|
||||
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8_t *data, size_t length,
|
||||
bool response) {
|
||||
if (!this->connected()) {
|
||||
this->log_gatt_not_connected_("write", "characteristic");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
@@ -522,8 +523,11 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
|
||||
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||
// const_cast is safe here and was previously hidden by a C-style cast
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
|
||||
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_write_char", err);
|
||||
}
|
||||
@@ -540,7 +544,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) {
|
||||
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response) {
|
||||
if (!this->connected()) {
|
||||
this->log_gatt_not_connected_("write", "descriptor");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
@@ -548,8 +552,11 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
|
||||
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||
// const_cast is safe here and was previously hidden by a C-style cast
|
||||
esp_err_t err = esp_ble_gattc_write_char_descr(
|
||||
this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
|
||||
this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
|
||||
}
|
||||
|
@@ -18,9 +18,9 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
esp_err_t read_characteristic(uint16_t handle);
|
||||
esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response);
|
||||
esp_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response);
|
||||
esp_err_t read_descriptor(uint16_t handle);
|
||||
esp_err_t write_descriptor(uint16_t handle, const std::string &data, bool response);
|
||||
esp_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response);
|
||||
|
||||
esp_err_t notify_characteristic(uint16_t handle, bool enable);
|
||||
|
||||
|
@@ -305,7 +305,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
|
||||
return;
|
||||
}
|
||||
|
||||
auto err = connection->write_characteristic(msg.handle, msg.data, msg.response);
|
||||
auto err = connection->write_characteristic(msg.handle, msg.data, msg.data_len, msg.response);
|
||||
if (err != ESP_OK) {
|
||||
this->send_gatt_error(msg.address, msg.handle, err);
|
||||
}
|
||||
@@ -331,7 +331,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
|
||||
return;
|
||||
}
|
||||
|
||||
auto err = connection->write_descriptor(msg.handle, msg.data, true);
|
||||
auto err = connection->write_descriptor(msg.handle, msg.data, msg.data_len, true);
|
||||
if (err != ESP_OK) {
|
||||
this->send_gatt_error(msg.address, msg.handle, err);
|
||||
}
|
||||
|
@@ -125,8 +125,8 @@ EAP_AUTH_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_USERNAME): cv.string_strict,
|
||||
cv.Optional(CONF_PASSWORD): cv.string_strict,
|
||||
cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate,
|
||||
cv.SplitDefault(CONF_TTLS_PHASE_2, esp32_idf="mschapv2"): cv.All(
|
||||
cv.enum(TTLS_PHASE_2), cv.only_with_esp_idf
|
||||
cv.SplitDefault(CONF_TTLS_PHASE_2, esp32="mschapv2"): cv.All(
|
||||
cv.enum(TTLS_PHASE_2), cv.only_on_esp32
|
||||
),
|
||||
cv.Inclusive(
|
||||
CONF_CERTIFICATE, "certificate_and_key"
|
||||
@@ -280,11 +280,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
|
||||
cv.decibel, cv.float_range(min=8.5, max=20.5)
|
||||
),
|
||||
cv.SplitDefault(CONF_ENABLE_BTM, esp32_idf=False): cv.All(
|
||||
cv.boolean, cv.only_with_esp_idf
|
||||
cv.SplitDefault(CONF_ENABLE_BTM, esp32=False): cv.All(
|
||||
cv.boolean, cv.only_on_esp32
|
||||
),
|
||||
cv.SplitDefault(CONF_ENABLE_RRM, esp32_idf=False): cv.All(
|
||||
cv.boolean, cv.only_with_esp_idf
|
||||
cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All(
|
||||
cv.boolean, cv.only_on_esp32
|
||||
),
|
||||
cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean,
|
||||
cv.Optional("enable_mdns"): cv.invalid(
|
||||
@@ -416,10 +416,10 @@ async def to_code(config):
|
||||
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266WiFi", None)
|
||||
elif (CORE.is_esp32 and CORE.using_arduino) or CORE.is_rp2040:
|
||||
elif CORE.is_rp2040:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
if CORE.is_esp32 and CORE.using_esp_idf:
|
||||
if CORE.is_esp32:
|
||||
if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]:
|
||||
add_idf_sdkconfig_option("CONFIG_WPA_11KV_SUPPORT", True)
|
||||
cg.add_define("USE_WIFI_11KV_SUPPORT")
|
||||
@@ -506,8 +506,10 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args):
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
{
|
||||
"wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
|
||||
"wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
|
||||
"wifi_component_esp_idf.cpp": {
|
||||
PlatformFramework.ESP32_IDF,
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
},
|
||||
"wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
|
||||
"wifi_component_libretiny.cpp": {
|
||||
PlatformFramework.BK72XX_ARDUINO,
|
||||
|
@@ -3,7 +3,7 @@
|
||||
#include <cinttypes>
|
||||
#include <map>
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
@@ -11,7 +11,7 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP_IDF)
|
||||
#if defined(USE_ESP32)
|
||||
#include <esp_wifi.h>
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
@@ -344,7 +344,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
|
||||
ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
|
||||
ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
|
||||
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
std::map<esp_eap_ttls_phase2_types, std::string> phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"},
|
||||
{ESP_EAP_TTLS_PHASE2_CHAP, "chap"},
|
||||
|
@@ -20,7 +20,7 @@
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP_IDF) && defined(USE_WIFI_WPA2_EAP)
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP)
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
@@ -113,7 +113,7 @@ struct EAPAuth {
|
||||
const char *client_cert;
|
||||
const char *client_key;
|
||||
// used for EAP-TTLS
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
esp_eap_ttls_phase2_types ttls_phase_2;
|
||||
#endif
|
||||
};
|
||||
@@ -199,7 +199,7 @@ enum WiFiPowerSaveMode : uint8_t {
|
||||
WIFI_POWER_SAVE_HIGH,
|
||||
};
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
struct IDFWiFiEvent;
|
||||
#endif
|
||||
|
||||
@@ -368,7 +368,7 @@ class WiFiComponent : public Component {
|
||||
void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info);
|
||||
void wifi_scan_done_callback_();
|
||||
#endif
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
void wifi_process_event_(IDFWiFiEvent *data);
|
||||
#endif
|
||||
|
||||
|
@@ -1,860 +0,0 @@
|
||||
#include "wifi_component.h"
|
||||
|
||||
#ifdef USE_WIFI
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
|
||||
#include <esp_netif.h>
|
||||
#include <esp_wifi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
#include <esp_eap_client.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
#include "dhcpserver/dhcpserver.h"
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
#include "lwip/apps/sntp.h"
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/err.h"
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace wifi {
|
||||
|
||||
static const char *const TAG = "wifi_esp32";
|
||||
|
||||
static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
#ifdef USE_WIFI_AP
|
||||
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void WiFiComponent::wifi_pre_setup_() {
|
||||
uint8_t mac[6];
|
||||
if (has_custom_mac_address()) {
|
||||
get_mac_address_raw(mac);
|
||||
set_mac_address(mac);
|
||||
}
|
||||
auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
|
||||
WiFi.onEvent(f);
|
||||
WiFi.persistent(false);
|
||||
// Make sure WiFi is in clean state before anything starts
|
||||
this->wifi_mode_(false, false);
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
|
||||
wifi_mode_t current_mode = WiFiClass::getMode();
|
||||
bool current_sta = current_mode == WIFI_MODE_STA || current_mode == WIFI_MODE_APSTA;
|
||||
bool current_ap = current_mode == WIFI_MODE_AP || current_mode == WIFI_MODE_APSTA;
|
||||
|
||||
bool set_sta = sta.value_or(current_sta);
|
||||
bool set_ap = ap.value_or(current_ap);
|
||||
|
||||
wifi_mode_t set_mode;
|
||||
if (set_sta && set_ap) {
|
||||
set_mode = WIFI_MODE_APSTA;
|
||||
} else if (set_sta && !set_ap) {
|
||||
set_mode = WIFI_MODE_STA;
|
||||
} else if (!set_sta && set_ap) {
|
||||
set_mode = WIFI_MODE_AP;
|
||||
} else {
|
||||
set_mode = WIFI_MODE_NULL;
|
||||
}
|
||||
|
||||
if (current_mode == set_mode)
|
||||
return true;
|
||||
|
||||
if (set_sta && !current_sta) {
|
||||
ESP_LOGV(TAG, "Enabling STA");
|
||||
} else if (!set_sta && current_sta) {
|
||||
ESP_LOGV(TAG, "Disabling STA");
|
||||
}
|
||||
if (set_ap && !current_ap) {
|
||||
ESP_LOGV(TAG, "Enabling AP");
|
||||
} else if (!set_ap && current_ap) {
|
||||
ESP_LOGV(TAG, "Disabling AP");
|
||||
}
|
||||
|
||||
bool ret = WiFiClass::mode(set_mode);
|
||||
|
||||
if (!ret) {
|
||||
ESP_LOGW(TAG, "Setting mode failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// WiFiClass::mode above calls esp_netif_create_default_wifi_sta() and
|
||||
// esp_netif_create_default_wifi_ap(), which creates the interfaces.
|
||||
// s_sta_netif handle is set during ESPHOME_EVENT_ID_WIFI_STA_START event
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
if (set_ap)
|
||||
s_ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
|
||||
#endif
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_sta_pre_setup_() {
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
|
||||
WiFi.setAutoReconnect(false);
|
||||
delay(10);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_apply_output_power_(float output_power) {
|
||||
int8_t val = static_cast<int8_t>(output_power * 4);
|
||||
return esp_wifi_set_max_tx_power(val) == ESP_OK;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_apply_power_save_() {
|
||||
wifi_ps_type_t power_save;
|
||||
switch (this->power_save_) {
|
||||
case WIFI_POWER_SAVE_LIGHT:
|
||||
power_save = WIFI_PS_MIN_MODEM;
|
||||
break;
|
||||
case WIFI_POWER_SAVE_HIGH:
|
||||
power_save = WIFI_PS_MAX_MODEM;
|
||||
break;
|
||||
case WIFI_POWER_SAVE_NONE:
|
||||
default:
|
||||
power_save = WIFI_PS_NONE;
|
||||
break;
|
||||
}
|
||||
return esp_wifi_set_ps(power_save) == ESP_OK;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
// enable STA
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
|
||||
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
|
||||
wifi_config_t conf;
|
||||
memset(&conf, 0, sizeof(conf));
|
||||
if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
|
||||
ESP_LOGE(TAG, "SSID too long");
|
||||
return false;
|
||||
}
|
||||
if (ap.get_password().size() > sizeof(conf.sta.password)) {
|
||||
ESP_LOGE(TAG, "Password too long");
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
||||
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
|
||||
|
||||
// The weakest authmode to accept in the fast scan mode
|
||||
if (ap.get_password().empty()) {
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||
} else {
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
if (ap.get_eap().has_value()) {
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ap.get_bssid().has_value()) {
|
||||
conf.sta.bssid_set = true;
|
||||
memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6);
|
||||
} else {
|
||||
conf.sta.bssid_set = false;
|
||||
}
|
||||
if (ap.get_channel().has_value()) {
|
||||
conf.sta.channel = *ap.get_channel();
|
||||
conf.sta.scan_method = WIFI_FAST_SCAN;
|
||||
} else {
|
||||
conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
||||
}
|
||||
// Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set.
|
||||
// Units: AP beacon intervals. Defaults to 3 if set to 0.
|
||||
conf.sta.listen_interval = 0;
|
||||
|
||||
// Protected Management Frame
|
||||
// Device will prefer to connect in PMF mode if other device also advertises PMF capability.
|
||||
conf.sta.pmf_cfg.capable = true;
|
||||
conf.sta.pmf_cfg.required = false;
|
||||
|
||||
// note, we do our own filtering
|
||||
// The minimum rssi to accept in the fast scan mode
|
||||
conf.sta.threshold.rssi = -127;
|
||||
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||
|
||||
wifi_config_t current_conf;
|
||||
esp_err_t err;
|
||||
err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_get_config failed: %s", esp_err_to_name(err));
|
||||
// can continue
|
||||
}
|
||||
|
||||
if (memcmp(¤t_conf, &conf, sizeof(wifi_config_t)) != 0) { // NOLINT
|
||||
err = esp_wifi_disconnect();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_wifi_disconnect failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
err = esp_wifi_set_config(WIFI_IF_STA, &conf);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_wifi_set_config failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// setup enterprise authentication if required
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
if (ap.get_eap().has_value()) {
|
||||
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
|
||||
EAPAuth eap = ap.get_eap().value();
|
||||
err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_eap_client_set_identity failed: %d", err);
|
||||
}
|
||||
int ca_cert_len = strlen(eap.ca_cert);
|
||||
int client_cert_len = strlen(eap.client_cert);
|
||||
int client_key_len = strlen(eap.client_key);
|
||||
if (ca_cert_len) {
|
||||
err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed: %d", err);
|
||||
}
|
||||
}
|
||||
// workout what type of EAP this is
|
||||
// validation is not required as the config tool has already validated it
|
||||
if (client_cert_len && client_key_len) {
|
||||
// if we have certs, this must be EAP-TLS
|
||||
err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
|
||||
(uint8_t *) eap.client_key, client_key_len + 1,
|
||||
(uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed: %d", err);
|
||||
}
|
||||
} else {
|
||||
// in the absence of certs, assume this is username/password based
|
||||
err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_eap_client_set_username failed: %d", err);
|
||||
}
|
||||
err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_eap_client_set_password failed: %d", err);
|
||||
}
|
||||
}
|
||||
err = esp_wifi_sta_enterprise_enable();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed: %d", err);
|
||||
}
|
||||
}
|
||||
#endif // USE_WIFI_WPA2_EAP
|
||||
|
||||
this->wifi_apply_hostname_();
|
||||
|
||||
s_sta_connecting = true;
|
||||
|
||||
err = esp_wifi_connect();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
||||
// enable STA
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
|
||||
// Check if the STA interface is initialized before using it
|
||||
if (s_sta_netif == nullptr) {
|
||||
ESP_LOGW(TAG, "STA interface not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_netif_dhcp_status_t dhcp_status;
|
||||
esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_netif_dhcpc_get_status failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!manual_ip.has_value()) {
|
||||
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
|
||||
// https://github.com/esphome/issues/issues/6591
|
||||
// https://github.com/espressif/arduino-esp32/issues/10526
|
||||
{
|
||||
LwIPLock lock;
|
||||
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
|
||||
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
|
||||
// https://github.com/esphome/issues/issues/2299
|
||||
sntp_servermode_dhcp(false);
|
||||
}
|
||||
|
||||
// No manual IP is set; use DHCP client
|
||||
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
|
||||
err = esp_netif_dhcpc_start(s_sta_netif);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "Starting DHCP client failed: %d", err);
|
||||
}
|
||||
return err == ESP_OK;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw
|
||||
info.ip = manual_ip->static_ip;
|
||||
info.gw = manual_ip->gateway;
|
||||
info.netmask = manual_ip->subnet;
|
||||
err = esp_netif_dhcpc_stop(s_sta_netif);
|
||||
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
|
||||
ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
err = esp_netif_set_ip_info(s_sta_netif, &info);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
esp_netif_dns_info_t dns;
|
||||
if (manual_ip->dns1.is_set()) {
|
||||
dns.ip = manual_ip->dns1;
|
||||
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns);
|
||||
}
|
||||
if (manual_ip->dns2.is_set()) {
|
||||
dns.ip = manual_ip->dns2;
|
||||
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
|
||||
if (!this->has_sta())
|
||||
return {};
|
||||
network::IPAddresses addresses;
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
|
||||
// TODO: do something smarter
|
||||
// return false;
|
||||
} else {
|
||||
addresses[0] = network::IPAddress(&ip.ip);
|
||||
}
|
||||
#if USE_NETWORK_IPV6
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
uint8_t count = 0;
|
||||
count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s);
|
||||
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
|
||||
for (int i = 0; i < count; i++) {
|
||||
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
return addresses;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_apply_hostname_() {
|
||||
// setting is done in SYSTEM_EVENT_STA_START callback
|
||||
return true;
|
||||
}
|
||||
const char *get_auth_mode_str(uint8_t mode) {
|
||||
switch (mode) {
|
||||
case WIFI_AUTH_OPEN:
|
||||
return "OPEN";
|
||||
case WIFI_AUTH_WEP:
|
||||
return "WEP";
|
||||
case WIFI_AUTH_WPA_PSK:
|
||||
return "WPA PSK";
|
||||
case WIFI_AUTH_WPA2_PSK:
|
||||
return "WPA2 PSK";
|
||||
case WIFI_AUTH_WPA_WPA2_PSK:
|
||||
return "WPA/WPA2 PSK";
|
||||
case WIFI_AUTH_WPA2_ENTERPRISE:
|
||||
return "WPA2 Enterprise";
|
||||
case WIFI_AUTH_WPA3_PSK:
|
||||
return "WPA3 PSK";
|
||||
case WIFI_AUTH_WPA2_WPA3_PSK:
|
||||
return "WPA2/WPA3 PSK";
|
||||
case WIFI_AUTH_WAPI_PSK:
|
||||
return "WAPI PSK";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
using esphome_ip4_addr_t = esp_ip4_addr_t;
|
||||
|
||||
std::string format_ip4_addr(const esphome_ip4_addr_t &ip) {
|
||||
char buf[20];
|
||||
sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16),
|
||||
uint8_t(ip.addr >> 24));
|
||||
return buf;
|
||||
}
|
||||
const char *get_op_mode_str(uint8_t mode) {
|
||||
switch (mode) {
|
||||
case WIFI_OFF:
|
||||
return "OFF";
|
||||
case WIFI_STA:
|
||||
return "STA";
|
||||
case WIFI_AP:
|
||||
return "AP";
|
||||
case WIFI_AP_STA:
|
||||
return "AP+STA";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
const char *get_disconnect_reason_str(uint8_t reason) {
|
||||
switch (reason) {
|
||||
case WIFI_REASON_AUTH_EXPIRE:
|
||||
return "Auth Expired";
|
||||
case WIFI_REASON_AUTH_LEAVE:
|
||||
return "Auth Leave";
|
||||
case WIFI_REASON_ASSOC_EXPIRE:
|
||||
return "Association Expired";
|
||||
case WIFI_REASON_ASSOC_TOOMANY:
|
||||
return "Too Many Associations";
|
||||
case WIFI_REASON_NOT_AUTHED:
|
||||
return "Not Authenticated";
|
||||
case WIFI_REASON_NOT_ASSOCED:
|
||||
return "Not Associated";
|
||||
case WIFI_REASON_ASSOC_LEAVE:
|
||||
return "Association Leave";
|
||||
case WIFI_REASON_ASSOC_NOT_AUTHED:
|
||||
return "Association not Authenticated";
|
||||
case WIFI_REASON_DISASSOC_PWRCAP_BAD:
|
||||
return "Disassociate Power Cap Bad";
|
||||
case WIFI_REASON_DISASSOC_SUPCHAN_BAD:
|
||||
return "Disassociate Supported Channel Bad";
|
||||
case WIFI_REASON_IE_INVALID:
|
||||
return "IE Invalid";
|
||||
case WIFI_REASON_MIC_FAILURE:
|
||||
return "Mic Failure";
|
||||
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
|
||||
return "4-Way Handshake Timeout";
|
||||
case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT:
|
||||
return "Group Key Update Timeout";
|
||||
case WIFI_REASON_IE_IN_4WAY_DIFFERS:
|
||||
return "IE In 4-Way Handshake Differs";
|
||||
case WIFI_REASON_GROUP_CIPHER_INVALID:
|
||||
return "Group Cipher Invalid";
|
||||
case WIFI_REASON_PAIRWISE_CIPHER_INVALID:
|
||||
return "Pairwise Cipher Invalid";
|
||||
case WIFI_REASON_AKMP_INVALID:
|
||||
return "AKMP Invalid";
|
||||
case WIFI_REASON_UNSUPP_RSN_IE_VERSION:
|
||||
return "Unsupported RSN IE version";
|
||||
case WIFI_REASON_INVALID_RSN_IE_CAP:
|
||||
return "Invalid RSN IE Cap";
|
||||
case WIFI_REASON_802_1X_AUTH_FAILED:
|
||||
return "802.1x Authentication Failed";
|
||||
case WIFI_REASON_CIPHER_SUITE_REJECTED:
|
||||
return "Cipher Suite Rejected";
|
||||
case WIFI_REASON_BEACON_TIMEOUT:
|
||||
return "Beacon Timeout";
|
||||
case WIFI_REASON_NO_AP_FOUND:
|
||||
return "AP Not Found";
|
||||
case WIFI_REASON_AUTH_FAIL:
|
||||
return "Authentication Failed";
|
||||
case WIFI_REASON_ASSOC_FAIL:
|
||||
return "Association Failed";
|
||||
case WIFI_REASON_HANDSHAKE_TIMEOUT:
|
||||
return "Handshake Failed";
|
||||
case WIFI_REASON_CONNECTION_FAIL:
|
||||
return "Connection Failed";
|
||||
case WIFI_REASON_AP_TSF_RESET:
|
||||
return "AP TSF reset";
|
||||
case WIFI_REASON_ROAMING:
|
||||
return "Station Roaming";
|
||||
case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG:
|
||||
return "Association comeback time too long";
|
||||
case WIFI_REASON_SA_QUERY_TIMEOUT:
|
||||
return "SA query timeout";
|
||||
case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY:
|
||||
return "No AP found with compatible security";
|
||||
case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD:
|
||||
return "No AP found in auth mode threshold";
|
||||
case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD:
|
||||
return "No AP found in RSSI threshold";
|
||||
case WIFI_REASON_UNSPECIFIED:
|
||||
default:
|
||||
return "Unspecified";
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiComponent::wifi_loop_() {}
|
||||
|
||||
#define ESPHOME_EVENT_ID_WIFI_READY ARDUINO_EVENT_WIFI_READY
|
||||
#define ESPHOME_EVENT_ID_WIFI_SCAN_DONE ARDUINO_EVENT_WIFI_SCAN_DONE
|
||||
#define ESPHOME_EVENT_ID_WIFI_STA_START ARDUINO_EVENT_WIFI_STA_START
|
||||
#define ESPHOME_EVENT_ID_WIFI_STA_STOP ARDUINO_EVENT_WIFI_STA_STOP
|
||||
#define ESPHOME_EVENT_ID_WIFI_STA_CONNECTED ARDUINO_EVENT_WIFI_STA_CONNECTED
|
||||
#define ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED ARDUINO_EVENT_WIFI_STA_DISCONNECTED
|
||||
#define ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE
|
||||
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP ARDUINO_EVENT_WIFI_STA_GOT_IP
|
||||
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6 ARDUINO_EVENT_WIFI_STA_GOT_IP6
|
||||
#define ESPHOME_EVENT_ID_WIFI_STA_LOST_IP ARDUINO_EVENT_WIFI_STA_LOST_IP
|
||||
#define ESPHOME_EVENT_ID_WIFI_AP_START ARDUINO_EVENT_WIFI_AP_START
|
||||
#define ESPHOME_EVENT_ID_WIFI_AP_STOP ARDUINO_EVENT_WIFI_AP_STOP
|
||||
#define ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED ARDUINO_EVENT_WIFI_AP_STACONNECTED
|
||||
#define ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED ARDUINO_EVENT_WIFI_AP_STADISCONNECTED
|
||||
#define ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED
|
||||
#define ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED
|
||||
#define ESPHOME_EVENT_ID_WIFI_AP_GOT_IP6 ARDUINO_EVENT_WIFI_AP_GOT_IP6
|
||||
using esphome_wifi_event_id_t = arduino_event_id_t;
|
||||
using esphome_wifi_event_info_t = arduino_event_info_t;
|
||||
|
||||
void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) {
|
||||
switch (event) {
|
||||
case ESPHOME_EVENT_ID_WIFI_READY: {
|
||||
ESP_LOGV(TAG, "Ready");
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: {
|
||||
auto it = info.wifi_scan_done;
|
||||
ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
|
||||
|
||||
this->wifi_scan_done_callback_();
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_START: {
|
||||
ESP_LOGV(TAG, "STA start");
|
||||
// apply hostname
|
||||
s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||
esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str());
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
|
||||
ESP_LOGV(TAG, "STA stop");
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
|
||||
auto it = info.wifi_sta_connected;
|
||||
char buf[33];
|
||||
memcpy(buf, it.ssid, it.ssid_len);
|
||||
buf[it.ssid_len] = '\0';
|
||||
ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
|
||||
format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
|
||||
#if USE_NETWORK_IPV6
|
||||
this->set_timeout(100, [] { WiFi.enableIPv6(); });
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: {
|
||||
auto it = info.wifi_sta_disconnected;
|
||||
char buf[33];
|
||||
memcpy(buf, it.ssid, it.ssid_len);
|
||||
buf[it.ssid_len] = '\0';
|
||||
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
|
||||
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
|
||||
}
|
||||
|
||||
uint8_t reason = it.reason;
|
||||
if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT ||
|
||||
reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
|
||||
reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
|
||||
err_t err = esp_wifi_disconnect();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
this->error_from_callback_ = true;
|
||||
}
|
||||
|
||||
s_sta_connecting = false;
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
|
||||
auto it = info.wifi_sta_authmode_change;
|
||||
ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode));
|
||||
// Mitigate CVE-2020-12638
|
||||
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
|
||||
if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
|
||||
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
|
||||
// we can't call retry_connect() from this context, so disconnect immediately
|
||||
// and notify main thread with error_from_callback_
|
||||
err_t err = esp_wifi_disconnect();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Disconnect failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
this->error_from_callback_ = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: {
|
||||
auto it = info.got_ip.ip_info;
|
||||
ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str());
|
||||
this->got_ipv4_address_ = true;
|
||||
#if USE_NETWORK_IPV6
|
||||
s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT;
|
||||
#else
|
||||
s_sta_connecting = false;
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
break;
|
||||
}
|
||||
#if USE_NETWORK_IPV6
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
|
||||
auto it = info.got_ip6.ip6_info;
|
||||
ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip));
|
||||
this->num_ipv6_addresses_++;
|
||||
s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT));
|
||||
break;
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
|
||||
ESP_LOGV(TAG, "Lost IP");
|
||||
this->got_ipv4_address_ = false;
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_AP_START: {
|
||||
ESP_LOGV(TAG, "AP start");
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_AP_STOP: {
|
||||
ESP_LOGV(TAG, "AP stop");
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
|
||||
auto it = info.wifi_sta_connected;
|
||||
auto &mac = it.bssid;
|
||||
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str());
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
|
||||
auto it = info.wifi_sta_disconnected;
|
||||
auto &mac = it.bssid;
|
||||
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str());
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
|
||||
ESP_LOGV(TAG, "AP client assigned IP");
|
||||
break;
|
||||
}
|
||||
case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
|
||||
auto it = info.wifi_ap_probereqrecved;
|
||||
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
|
||||
const auto status = WiFi.status();
|
||||
if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
|
||||
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
|
||||
}
|
||||
if (status == WL_NO_SSID_AVAIL) {
|
||||
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
|
||||
}
|
||||
if (s_sta_connecting) {
|
||||
return WiFiSTAConnectStatus::CONNECTING;
|
||||
}
|
||||
if (status == WL_CONNECTED) {
|
||||
return WiFiSTAConnectStatus::CONNECTED;
|
||||
}
|
||||
return WiFiSTAConnectStatus::IDLE;
|
||||
}
|
||||
bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
// enable STA
|
||||
if (!this->wifi_mode_(true, {}))
|
||||
return false;
|
||||
|
||||
// need to use WiFi because of WiFiScanClass allocations :(
|
||||
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
|
||||
if (err != WIFI_SCAN_RUNNING) {
|
||||
ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
void WiFiComponent::wifi_scan_done_callback_() {
|
||||
this->scan_result_.clear();
|
||||
|
||||
int16_t num = WiFi.scanComplete();
|
||||
if (num < 0)
|
||||
return;
|
||||
|
||||
this->scan_result_.reserve(static_cast<unsigned int>(num));
|
||||
for (int i = 0; i < num; i++) {
|
||||
String ssid = WiFi.SSID(i);
|
||||
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
|
||||
int32_t rssi = WiFi.RSSI(i);
|
||||
uint8_t *bssid = WiFi.BSSID(i);
|
||||
int32_t channel = WiFi.channel(i);
|
||||
|
||||
WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()),
|
||||
channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0);
|
||||
this->scan_result_.push_back(scan);
|
||||
}
|
||||
WiFi.scanDelete();
|
||||
this->scan_done_ = true;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
||||
esp_err_t err;
|
||||
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
|
||||
// Check if the AP interface is initialized before using it
|
||||
if (s_ap_netif == nullptr) {
|
||||
ESP_LOGW(TAG, "AP interface not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t info;
|
||||
if (manual_ip.has_value()) {
|
||||
info.ip = manual_ip->static_ip;
|
||||
info.gw = manual_ip->gateway;
|
||||
info.netmask = manual_ip->subnet;
|
||||
} else {
|
||||
info.ip = network::IPAddress(192, 168, 4, 1);
|
||||
info.gw = network::IPAddress(192, 168, 4, 1);
|
||||
info.netmask = network::IPAddress(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
err = esp_netif_dhcps_stop(s_ap_netif);
|
||||
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
|
||||
ESP_LOGE(TAG, "esp_netif_dhcps_stop failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
err = esp_netif_set_ip_info(s_ap_netif, &info);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
dhcps_lease_t lease;
|
||||
lease.enable = true;
|
||||
network::IPAddress start_address = network::IPAddress(&info.ip);
|
||||
start_address += 99;
|
||||
lease.start_ip = start_address;
|
||||
ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str().c_str());
|
||||
start_address += 10;
|
||||
lease.end_ip = start_address;
|
||||
ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str());
|
||||
err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease));
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
err = esp_netif_dhcps_start(s_ap_netif);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
|
||||
wifi_config_t conf;
|
||||
memset(&conf, 0, sizeof(conf));
|
||||
if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
|
||||
ESP_LOGE(TAG, "AP SSID too long");
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
|
||||
conf.ap.channel = ap.get_channel().value_or(1);
|
||||
conf.ap.ssid_hidden = ap.get_ssid().size();
|
||||
conf.ap.max_connection = 5;
|
||||
conf.ap.beacon_interval = 100;
|
||||
|
||||
if (ap.get_password().empty()) {
|
||||
conf.ap.authmode = WIFI_AUTH_OPEN;
|
||||
*conf.ap.password = 0;
|
||||
} else {
|
||||
conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
if (ap.get_password().size() > sizeof(conf.ap.password)) {
|
||||
ESP_LOGE(TAG, "AP password too long");
|
||||
return false;
|
||||
}
|
||||
memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
|
||||
}
|
||||
|
||||
// pairwise cipher of SoftAP, group cipher will be derived using this.
|
||||
conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP;
|
||||
|
||||
esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGV(TAG, "esp_wifi_set_config failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
yield();
|
||||
|
||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
|
||||
esp_netif_ip_info_t ip;
|
||||
esp_netif_get_ip_info(s_ap_netif, &ip);
|
||||
return network::IPAddress(&ip.ip);
|
||||
}
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
|
||||
|
||||
bssid_t WiFiComponent::wifi_bssid() {
|
||||
bssid_t bssid{};
|
||||
uint8_t *raw_bssid = WiFi.BSSID();
|
||||
if (raw_bssid != nullptr) {
|
||||
for (size_t i = 0; i < bssid.size(); i++)
|
||||
bssid[i] = raw_bssid[i];
|
||||
}
|
||||
return bssid;
|
||||
}
|
||||
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
|
||||
int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
|
||||
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
|
||||
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return network::IPAddress(WiFi.subnetMask()); }
|
||||
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(WiFi.gatewayIP()); }
|
||||
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(WiFi.dnsIP(num)); }
|
||||
|
||||
} // namespace wifi
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#endif
|
@@ -1,7 +1,7 @@
|
||||
#include "wifi_component.h"
|
||||
|
||||
#ifdef USE_WIFI
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_event.h>
|
||||
#include <esp_netif.h>
|
||||
@@ -1050,5 +1050,5 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
|
||||
} // namespace wifi
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
#endif // USE_ESP32
|
||||
#endif
|
||||
|
@@ -1,4 +1,5 @@
|
||||
#include "zwave_proxy.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
@@ -12,6 +13,7 @@ static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
|
||||
// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
|
||||
static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
|
||||
static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
|
||||
static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup
|
||||
|
||||
static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
|
||||
// Calculate Z-Wave frame checksum
|
||||
@@ -26,7 +28,44 @@ static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
|
||||
|
||||
ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; }
|
||||
|
||||
void ZWaveProxy::setup() { this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); }
|
||||
void ZWaveProxy::setup() {
|
||||
this->setup_time_ = App.get_loop_component_start_time();
|
||||
this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
|
||||
}
|
||||
|
||||
float ZWaveProxy::get_setup_priority() const {
|
||||
// Set up before API so home ID is ready when API starts
|
||||
return setup_priority::BEFORE_CONNECTION;
|
||||
}
|
||||
|
||||
bool ZWaveProxy::can_proceed() {
|
||||
// If we already have the home ID, we can proceed
|
||||
if (this->home_id_ready_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle any pending responses
|
||||
if (this->response_handler_()) {
|
||||
ESP_LOGV(TAG, "Handled response during setup");
|
||||
}
|
||||
|
||||
// Process UART data to check for home ID
|
||||
this->process_uart_();
|
||||
|
||||
// Check if we got the home ID after processing
|
||||
if (this->home_id_ready_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wait up to HOME_ID_TIMEOUT_MS for home ID response
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) {
|
||||
ESP_LOGW(TAG, "Timeout reading Home ID during setup");
|
||||
return true; // Proceed anyway after timeout
|
||||
}
|
||||
|
||||
return false; // Keep waiting
|
||||
}
|
||||
|
||||
void ZWaveProxy::loop() {
|
||||
if (this->response_handler_()) {
|
||||
@@ -37,6 +76,11 @@ void ZWaveProxy::loop() {
|
||||
this->api_connection_ = nullptr; // Unsubscribe if disconnected
|
||||
}
|
||||
|
||||
this->process_uart_();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void ZWaveProxy::process_uart_() {
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
if (!this->read_byte(&byte)) {
|
||||
@@ -56,6 +100,7 @@ void ZWaveProxy::loop() {
|
||||
// Extract the 4-byte Home ID starting at offset 4
|
||||
// The frame parser has already validated the checksum and ensured all bytes are present
|
||||
std::memcpy(this->home_id_.data(), this->buffer_.data() + 4, this->home_id_.size());
|
||||
this->home_id_ready_ = true;
|
||||
ESP_LOGI(TAG, "Home ID: %s",
|
||||
format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
|
||||
}
|
||||
@@ -73,7 +118,6 @@ void ZWaveProxy::loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void ZWaveProxy::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy"); }
|
||||
|
@@ -44,6 +44,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
bool can_proceed() override;
|
||||
|
||||
void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type);
|
||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||
@@ -60,19 +62,24 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
|
||||
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
|
||||
void parse_start_(uint8_t byte);
|
||||
bool response_handler_();
|
||||
|
||||
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
||||
|
||||
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
|
||||
std::array<uint8_t, sizeof(api::ZWaveProxyFrame::data)> buffer_; // Fixed buffer for incoming data
|
||||
uint8_t buffer_index_{0}; // Index for populating the data buffer
|
||||
uint8_t end_frame_after_{0}; // Payload reception ends after this index
|
||||
uint8_t last_response_{0}; // Last response type sent
|
||||
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
|
||||
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
|
||||
void process_uart_(); // Process all available UART data
|
||||
|
||||
// Pre-allocated message - always ready to send
|
||||
api::ZWaveProxyFrame outgoing_proto_msg_;
|
||||
std::array<uint8_t, sizeof(api::ZWaveProxyFrame::data)> buffer_; // Fixed buffer for incoming data
|
||||
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
|
||||
|
||||
// Pointers and 32-bit values (aligned together)
|
||||
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
||||
uint32_t setup_time_{0}; // Time when setup() was called
|
||||
|
||||
// 8-bit values (grouped together to minimize padding)
|
||||
uint8_t buffer_index_{0}; // Index for populating the data buffer
|
||||
uint8_t end_frame_after_{0}; // Payload reception ends after this index
|
||||
uint8_t last_response_{0}; // Last response type sent
|
||||
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
|
||||
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
|
||||
bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module
|
||||
};
|
||||
|
||||
extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
@@ -479,6 +479,12 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
|
||||
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
|
||||
|
||||
|
||||
class EsphomeCleanPlatformHandler(EsphomeCommandWebSocket):
|
||||
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
||||
config_file = settings.rel_path(json_message["configuration"])
|
||||
return [*DASHBOARD_COMMAND, "clean-platform", config_file]
|
||||
|
||||
|
||||
class EsphomeCleanHandler(EsphomeCommandWebSocket):
|
||||
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
||||
config_file = settings.rel_path(json_message["configuration"])
|
||||
@@ -1313,6 +1319,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
|
||||
(f"{rel}compile", EsphomeCompileHandler),
|
||||
(f"{rel}validate", EsphomeValidateHandler),
|
||||
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
|
||||
(f"{rel}clean-platform", EsphomeCleanPlatformHandler),
|
||||
(f"{rel}clean", EsphomeCleanHandler),
|
||||
(f"{rel}vscode", EsphomeVscodeHandler),
|
||||
(f"{rel}ace", EsphomeAceEditorHandler),
|
||||
|
@@ -323,17 +323,39 @@ def clean_build():
|
||||
# Clean PlatformIO cache to resolve CMake compiler detection issues
|
||||
# This helps when toolchain paths change or get corrupted
|
||||
try:
|
||||
from platformio.project.helpers import get_project_cache_dir
|
||||
from platformio.project.config import ProjectConfig
|
||||
except ImportError:
|
||||
# PlatformIO is not available, skip cache cleaning
|
||||
pass
|
||||
else:
|
||||
cache_dir = get_project_cache_dir()
|
||||
if cache_dir and cache_dir.strip():
|
||||
cache_path = Path(cache_dir)
|
||||
if cache_path.is_dir():
|
||||
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
|
||||
shutil.rmtree(cache_dir)
|
||||
config = ProjectConfig.get_instance()
|
||||
cache_dir = Path(config.get("platformio", "cache_dir"))
|
||||
if cache_dir.is_dir():
|
||||
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
|
||||
shutil.rmtree(cache_dir)
|
||||
|
||||
|
||||
def clean_platform():
|
||||
import shutil
|
||||
|
||||
# Clean entire build dir
|
||||
if CORE.build_path.is_dir():
|
||||
_LOGGER.info("Deleting %s", CORE.build_path)
|
||||
shutil.rmtree(CORE.build_path)
|
||||
|
||||
# Clean PlatformIO project files
|
||||
try:
|
||||
from platformio.project.config import ProjectConfig
|
||||
except ImportError:
|
||||
# PlatformIO is not available, skip cleaning
|
||||
pass
|
||||
else:
|
||||
config = ProjectConfig.get_instance()
|
||||
for pio_dir in ["cache_dir", "packages_dir", "platforms_dir", "core_dir"]:
|
||||
path = Path(config.get("platformio", pio_dir))
|
||||
if path.is_dir():
|
||||
_LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path)
|
||||
shutil.rmtree(path)
|
||||
|
||||
|
||||
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
|
||||
|
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20250904.0
|
||||
aioesphomeapi==41.7.0
|
||||
aioesphomeapi==41.8.0
|
||||
zeroconf==0.147.2
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.15 # dashboard_import
|
||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
@@ -16,6 +17,7 @@ from esphome import platformio_api
|
||||
from esphome.__main__ import (
|
||||
Purpose,
|
||||
choose_upload_log_host,
|
||||
command_clean_platform,
|
||||
command_rename,
|
||||
command_update_all,
|
||||
command_wizard,
|
||||
@@ -1853,3 +1855,101 @@ esp32:
|
||||
# Should not have any Python error messages
|
||||
assert "TypeError" not in clean_output
|
||||
assert "can only concatenate str" not in clean_output
|
||||
|
||||
|
||||
def test_command_clean_platform_success(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test command_clean_platform when writer.clean_platform() succeeds."""
|
||||
args = MockArgs()
|
||||
config = {}
|
||||
|
||||
# Set logger level to capture INFO messages
|
||||
with (
|
||||
caplog.at_level(logging.INFO),
|
||||
patch("esphome.writer.clean_platform") as mock_clean_platform,
|
||||
):
|
||||
result = command_clean_platform(args, config)
|
||||
|
||||
assert result == 0
|
||||
mock_clean_platform.assert_called_once()
|
||||
|
||||
# Check that success message was logged
|
||||
assert "Done!" in caplog.text
|
||||
|
||||
|
||||
def test_command_clean_platform_oserror(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test command_clean_platform when writer.clean_platform() raises OSError."""
|
||||
args = MockArgs()
|
||||
config = {}
|
||||
|
||||
# Create a mock OSError with a specific message
|
||||
mock_error = OSError("Permission denied: cannot delete directory")
|
||||
|
||||
# Set logger level to capture ERROR and INFO messages
|
||||
with (
|
||||
caplog.at_level(logging.INFO),
|
||||
patch(
|
||||
"esphome.writer.clean_platform", side_effect=mock_error
|
||||
) as mock_clean_platform,
|
||||
):
|
||||
result = command_clean_platform(args, config)
|
||||
|
||||
assert result == 1
|
||||
mock_clean_platform.assert_called_once()
|
||||
|
||||
# Check that error message was logged
|
||||
assert (
|
||||
"Error deleting platform files: Permission denied: cannot delete directory"
|
||||
in caplog.text
|
||||
)
|
||||
# Should not have success message
|
||||
assert "Done!" not in caplog.text
|
||||
|
||||
|
||||
def test_command_clean_platform_oserror_no_message(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test command_clean_platform when writer.clean_platform() raises OSError without message."""
|
||||
args = MockArgs()
|
||||
config = {}
|
||||
|
||||
# Create a mock OSError without a message
|
||||
mock_error = OSError()
|
||||
|
||||
# Set logger level to capture ERROR and INFO messages
|
||||
with (
|
||||
caplog.at_level(logging.INFO),
|
||||
patch(
|
||||
"esphome.writer.clean_platform", side_effect=mock_error
|
||||
) as mock_clean_platform,
|
||||
):
|
||||
result = command_clean_platform(args, config)
|
||||
|
||||
assert result == 1
|
||||
mock_clean_platform.assert_called_once()
|
||||
|
||||
# Check that error message was logged (should show empty string for OSError without message)
|
||||
assert "Error deleting platform files:" in caplog.text
|
||||
# Should not have success message
|
||||
assert "Done!" not in caplog.text
|
||||
|
||||
|
||||
def test_command_clean_platform_args_and_config_ignored() -> None:
|
||||
"""Test that command_clean_platform ignores args and config parameters."""
|
||||
# Test with various args and config to ensure they don't affect the function
|
||||
args1 = MockArgs(name="test1", file="test.bin")
|
||||
config1 = {"wifi": {"ssid": "test"}}
|
||||
|
||||
args2 = MockArgs(name="test2", dashboard=True)
|
||||
config2 = {"api": {}, "ota": {}}
|
||||
|
||||
with patch("esphome.writer.clean_platform") as mock_clean_platform:
|
||||
result1 = command_clean_platform(args1, config1)
|
||||
result2 = command_clean_platform(args2, config2)
|
||||
|
||||
assert result1 == 0
|
||||
assert result2 == 0
|
||||
assert mock_clean_platform.call_count == 2
|
||||
|
@@ -362,11 +362,17 @@ def test_clean_build(
|
||||
assert dependencies_lock.exists()
|
||||
assert platformio_cache_dir.exists()
|
||||
|
||||
# Mock PlatformIO's get_project_cache_dir
|
||||
# Mock PlatformIO's ProjectConfig cache_dir
|
||||
with patch(
|
||||
"platformio.project.helpers.get_project_cache_dir"
|
||||
) as mock_get_cache_dir:
|
||||
mock_get_cache_dir.return_value = str(platformio_cache_dir)
|
||||
"platformio.project.config.ProjectConfig.get_instance"
|
||||
) as mock_get_instance:
|
||||
mock_config = MagicMock()
|
||||
mock_get_instance.return_value = mock_config
|
||||
mock_config.get.side_effect = (
|
||||
lambda section, option: str(platformio_cache_dir)
|
||||
if (section, option) == ("platformio", "cache_dir")
|
||||
else ""
|
||||
)
|
||||
|
||||
# Call the function
|
||||
with caplog.at_level("INFO"):
|
||||
@@ -486,7 +492,7 @@ def test_clean_build_platformio_not_available(
|
||||
|
||||
# Mock import error for platformio
|
||||
with (
|
||||
patch.dict("sys.modules", {"platformio.project.helpers": None}),
|
||||
patch.dict("sys.modules", {"platformio.project.config": None}),
|
||||
caplog.at_level("INFO"),
|
||||
):
|
||||
# Call the function
|
||||
@@ -520,11 +526,17 @@ def test_clean_build_empty_cache_dir(
|
||||
# Verify pioenvs exists before
|
||||
assert pioenvs_dir.exists()
|
||||
|
||||
# Mock PlatformIO's get_project_cache_dir to return whitespace
|
||||
# Mock PlatformIO's ProjectConfig cache_dir to return whitespace
|
||||
with patch(
|
||||
"platformio.project.helpers.get_project_cache_dir"
|
||||
) as mock_get_cache_dir:
|
||||
mock_get_cache_dir.return_value = " " # Whitespace only
|
||||
"platformio.project.config.ProjectConfig.get_instance"
|
||||
) as mock_get_instance:
|
||||
mock_config = MagicMock()
|
||||
mock_get_instance.return_value = mock_config
|
||||
mock_config.get.side_effect = (
|
||||
lambda section, option: " " # Whitespace only
|
||||
if (section, option) == ("platformio", "cache_dir")
|
||||
else ""
|
||||
)
|
||||
|
||||
# Call the function
|
||||
with caplog.at_level("INFO"):
|
||||
@@ -723,3 +735,126 @@ def test_write_cpp_with_duplicate_markers(
|
||||
# Call should raise an error
|
||||
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
|
||||
write_cpp("// New code")
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
def test_clean_platform(
|
||||
mock_core: MagicMock,
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test clean_platform removes build and PlatformIO dirs."""
|
||||
# Create build directory
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
(build_dir / "dummy.txt").write_text("x")
|
||||
|
||||
# Create PlatformIO directories
|
||||
pio_cache = tmp_path / "pio_cache"
|
||||
pio_packages = tmp_path / "pio_packages"
|
||||
pio_platforms = tmp_path / "pio_platforms"
|
||||
pio_core = tmp_path / "pio_core"
|
||||
for d in (pio_cache, pio_packages, pio_platforms, pio_core):
|
||||
d.mkdir()
|
||||
(d / "keep").write_text("x")
|
||||
|
||||
# Setup CORE
|
||||
mock_core.build_path = build_dir
|
||||
|
||||
# Mock ProjectConfig
|
||||
with patch(
|
||||
"platformio.project.config.ProjectConfig.get_instance"
|
||||
) as mock_get_instance:
|
||||
mock_config = MagicMock()
|
||||
mock_get_instance.return_value = mock_config
|
||||
|
||||
def cfg_get(section: str, option: str) -> str:
|
||||
mapping = {
|
||||
("platformio", "cache_dir"): str(pio_cache),
|
||||
("platformio", "packages_dir"): str(pio_packages),
|
||||
("platformio", "platforms_dir"): str(pio_platforms),
|
||||
("platformio", "core_dir"): str(pio_core),
|
||||
}
|
||||
return mapping.get((section, option), "")
|
||||
|
||||
mock_config.get.side_effect = cfg_get
|
||||
|
||||
# Call
|
||||
from esphome.writer import clean_platform
|
||||
|
||||
with caplog.at_level("INFO"):
|
||||
clean_platform()
|
||||
|
||||
# Verify deletions
|
||||
assert not build_dir.exists()
|
||||
assert not pio_cache.exists()
|
||||
assert not pio_packages.exists()
|
||||
assert not pio_platforms.exists()
|
||||
assert not pio_core.exists()
|
||||
|
||||
# Verify logging mentions each
|
||||
assert "Deleting" in caplog.text
|
||||
assert str(build_dir) in caplog.text
|
||||
assert "PlatformIO cache" in caplog.text
|
||||
assert "PlatformIO packages" in caplog.text
|
||||
assert "PlatformIO platforms" in caplog.text
|
||||
assert "PlatformIO core" in caplog.text
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
def test_clean_platform_platformio_not_available(
|
||||
mock_core: MagicMock,
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test clean_platform when PlatformIO is not available."""
|
||||
# Build dir
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
mock_core.build_path = build_dir
|
||||
|
||||
# PlatformIO dirs that should remain untouched
|
||||
pio_cache = tmp_path / "pio_cache"
|
||||
pio_cache.mkdir()
|
||||
|
||||
from esphome.writer import clean_platform
|
||||
|
||||
with (
|
||||
patch.dict("sys.modules", {"platformio.project.config": None}),
|
||||
caplog.at_level("INFO"),
|
||||
):
|
||||
clean_platform()
|
||||
|
||||
# Build dir removed, PlatformIO dirs remain
|
||||
assert not build_dir.exists()
|
||||
assert pio_cache.exists()
|
||||
|
||||
# No PlatformIO-specific logs
|
||||
assert "PlatformIO" not in caplog.text
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
def test_clean_platform_partial_exists(
|
||||
mock_core: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test clean_platform when only build dir exists."""
|
||||
build_dir = tmp_path / "build"
|
||||
build_dir.mkdir()
|
||||
mock_core.build_path = build_dir
|
||||
|
||||
with patch(
|
||||
"platformio.project.config.ProjectConfig.get_instance"
|
||||
) as mock_get_instance:
|
||||
mock_config = MagicMock()
|
||||
mock_get_instance.return_value = mock_config
|
||||
# Return non-existent dirs
|
||||
mock_config.get.side_effect = lambda *_args, **_kw: str(
|
||||
tmp_path / "does_not_exist"
|
||||
)
|
||||
|
||||
from esphome.writer import clean_platform
|
||||
|
||||
clean_platform()
|
||||
|
||||
assert not build_dir.exists()
|
||||
|
Reference in New Issue
Block a user