1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-26 15:12:21 +01:00

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

This commit is contained in:
J. Nick Koston
2025-09-23 15:05:30 -05:00
15 changed files with 400 additions and 51 deletions

View File

@@ -738,6 +738,16 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
return clean_mqtt(config, args) 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: def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import mqtt from esphome import mqtt
@@ -936,9 +946,10 @@ POST_CONFIG_ACTIONS = {
"upload": command_upload, "upload": command_upload,
"logs": command_logs, "logs": command_logs,
"run": command_run, "run": command_run,
"clean-mqtt": command_clean_mqtt,
"mqtt-fingerprint": command_mqtt_fingerprint,
"clean": command_clean, "clean": command_clean,
"clean-mqtt": command_clean_mqtt,
"clean-platform": command_clean_platform,
"mqtt-fingerprint": command_mqtt_fingerprint,
"idedata": command_idedata, "idedata": command_idedata,
"rename": command_rename, "rename": command_rename,
"discover": command_discover, "discover": command_discover,
@@ -947,6 +958,7 @@ POST_CONFIG_ACTIONS = {
SIMPLE_CONFIG_ACTIONS = [ SIMPLE_CONFIG_ACTIONS = [
"clean", "clean",
"clean-mqtt", "clean-mqtt",
"clean-platform",
"config", "config",
] ]
@@ -1162,6 +1174,13 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file(s).", nargs="+" "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( parser_dashboard = subparsers.add_parser(
"dashboard", help="Create a simple web server for a dashboard." "dashboard", help="Create a simple web server for a dashboard."
) )

View File

@@ -1571,7 +1571,7 @@ message BluetoothGATTWriteRequest {
uint32 handle = 2; uint32 handle = 2;
bool response = 3; bool response = 3;
bytes data = 4; bytes data = 4 [(pointer_to_buffer) = true];
} }
message BluetoothGATTReadDescriptorRequest { message BluetoothGATTReadDescriptorRequest {
@@ -1591,7 +1591,7 @@ message BluetoothGATTWriteDescriptorRequest {
uint64 address = 1; uint64 address = 1;
uint32 handle = 2; uint32 handle = 2;
bytes data = 3; bytes data = 3 [(pointer_to_buffer) = true];
} }
message BluetoothGATTNotifyRequest { message BluetoothGATTNotifyRequest {

View File

@@ -2037,9 +2037,12 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val
} }
bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 4: case 4: {
this->data = value.as_string(); // Use raw data directly to avoid allocation
this->data = value.data();
this->data_len = value.size();
break; break;
}
default: default:
return false; 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) { bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 3: case 3: {
this->data = value.as_string(); // Use raw data directly to avoid allocation
this->data = value.data();
this->data_len = value.size();
break; break;
}
default: default:
return false; return false;
} }

View File

@@ -1988,14 +1988,15 @@ class BluetoothGATTReadResponse final : public ProtoMessage {
class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 75; 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 #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_write_request"; } const char *message_name() const override { return "bluetooth_gatt_write_request"; }
#endif #endif
uint64_t address{0}; uint64_t address{0};
uint32_t handle{0}; uint32_t handle{0};
bool response{false}; bool response{false};
std::string data{}; const uint8_t *data{nullptr};
uint16_t data_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2023,13 +2024,14 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 77; 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 #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; }
#endif #endif
uint64_t address{0}; uint64_t address{0};
uint32_t handle{0}; uint32_t handle{0};
std::string data{}; const uint8_t *data{nullptr};
uint16_t data_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif

View File

@@ -1658,7 +1658,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
dump_field(out, "handle", this->handle); dump_field(out, "handle", this->handle);
dump_field(out, "response", this->response); dump_field(out, "response", this->response);
out.append(" data: "); 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"); out.append("\n");
} }
void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { 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, "address", this->address);
dump_field(out, "handle", this->handle); dump_field(out, "handle", this->handle);
out.append(" data: "); 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"); out.append("\n");
} }
void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {

View File

@@ -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); 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()) { if (!this->connected()) {
this->log_gatt_not_connected_("write", "characteristic"); this->log_gatt_not_connected_("write", "characteristic");
return ESP_GATT_NOT_CONNECTED; 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(), ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
handle); 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_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); 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); 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); 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()) { if (!this->connected()) {
this->log_gatt_not_connected_("write", "descriptor"); this->log_gatt_not_connected_("write", "descriptor");
return ESP_GATT_NOT_CONNECTED; 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(), ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
handle); 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( 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); 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); return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
} }

View File

@@ -18,9 +18,9 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
esp_err_t read_characteristic(uint16_t handle); 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 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); esp_err_t notify_characteristic(uint16_t handle, bool enable);

View File

@@ -305,7 +305,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
return; 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) { if (err != ESP_OK) {
this->send_gatt_error(msg.address, msg.handle, err); this->send_gatt_error(msg.address, msg.handle, err);
} }
@@ -331,7 +331,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
return; 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) { if (err != ESP_OK) {
this->send_gatt_error(msg.address, msg.handle, err); this->send_gatt_error(msg.address, msg.handle, err);
} }

View File

@@ -1,4 +1,5 @@
#include "zwave_proxy.h" #include "zwave_proxy.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.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][...] // 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_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 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) { static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
// Calculate Z-Wave frame checksum // 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; } 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() { void ZWaveProxy::loop() {
if (this->response_handler_()) { if (this->response_handler_()) {
@@ -37,6 +76,11 @@ void ZWaveProxy::loop() {
this->api_connection_ = nullptr; // Unsubscribe if disconnected this->api_connection_ = nullptr; // Unsubscribe if disconnected
} }
this->process_uart_();
this->status_clear_warning();
}
void ZWaveProxy::process_uart_() {
while (this->available()) { while (this->available()) {
uint8_t byte; uint8_t byte;
if (!this->read_byte(&byte)) { if (!this->read_byte(&byte)) {
@@ -56,6 +100,7 @@ void ZWaveProxy::loop() {
// Extract the 4-byte Home ID starting at offset 4 // Extract the 4-byte Home ID starting at offset 4
// The frame parser has already validated the checksum and ensured all bytes are present // 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()); 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", ESP_LOGI(TAG, "Home ID: %s",
format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); 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"); } void ZWaveProxy::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy"); }

View File

@@ -44,6 +44,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
void setup() override; void setup() override;
void loop() override; void loop() override;
void dump_config() 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); void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type);
api::APIConnection *get_api_connection() { return this->api_connection_; } 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) 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); void parse_start_(uint8_t byte);
bool response_handler_(); bool response_handler_();
void process_uart_(); // Process all available UART data
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
// Pre-allocated message - always ready to send // Pre-allocated message - always ready to send
api::ZWaveProxyFrame outgoing_proto_msg_; 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) extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -479,6 +479,12 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] 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): class EsphomeCleanHandler(EsphomeCommandWebSocket):
async def build_command(self, json_message: dict[str, Any]) -> list[str]: async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) 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}compile", EsphomeCompileHandler),
(f"{rel}validate", EsphomeValidateHandler), (f"{rel}validate", EsphomeValidateHandler),
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler), (f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
(f"{rel}clean-platform", EsphomeCleanPlatformHandler),
(f"{rel}clean", EsphomeCleanHandler), (f"{rel}clean", EsphomeCleanHandler),
(f"{rel}vscode", EsphomeVscodeHandler), (f"{rel}vscode", EsphomeVscodeHandler),
(f"{rel}ace", EsphomeAceEditorHandler), (f"{rel}ace", EsphomeAceEditorHandler),

View File

@@ -323,17 +323,39 @@ def clean_build():
# Clean PlatformIO cache to resolve CMake compiler detection issues # Clean PlatformIO cache to resolve CMake compiler detection issues
# This helps when toolchain paths change or get corrupted # This helps when toolchain paths change or get corrupted
try: try:
from platformio.project.helpers import get_project_cache_dir from platformio.project.config import ProjectConfig
except ImportError: except ImportError:
# PlatformIO is not available, skip cache cleaning # PlatformIO is not available, skip cache cleaning
pass pass
else: else:
cache_dir = get_project_cache_dir() config = ProjectConfig.get_instance()
if cache_dir and cache_dir.strip(): cache_dir = Path(config.get("platformio", "cache_dir"))
cache_path = Path(cache_dir) if cache_dir.is_dir():
if cache_path.is_dir(): _LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir) shutil.rmtree(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 GITIGNORE_CONTENT = """# Gitignore settings for ESPHome

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0 esptool==5.1.0
click==8.1.7 click==8.1.7
esphome-dashboard==20250904.0 esphome-dashboard==20250904.0
aioesphomeapi==41.7.0 aioesphomeapi==41.8.0
zeroconf==0.147.2 zeroconf==0.147.2
puremagic==1.30 puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import ruamel.yaml==0.18.15 # dashboard_import

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Generator from collections.abc import Generator
from dataclasses import dataclass from dataclasses import dataclass
import logging
from pathlib import Path from pathlib import Path
import re import re
from typing import Any from typing import Any
@@ -16,6 +17,7 @@ from esphome import platformio_api
from esphome.__main__ import ( from esphome.__main__ import (
Purpose, Purpose,
choose_upload_log_host, choose_upload_log_host,
command_clean_platform,
command_rename, command_rename,
command_update_all, command_update_all,
command_wizard, command_wizard,
@@ -1853,3 +1855,101 @@ esp32:
# Should not have any Python error messages # Should not have any Python error messages
assert "TypeError" not in clean_output assert "TypeError" not in clean_output
assert "can only concatenate str" 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

View File

@@ -363,11 +363,17 @@ def test_clean_build(
assert dependencies_lock.exists() assert dependencies_lock.exists()
assert platformio_cache_dir.exists() assert platformio_cache_dir.exists()
# Mock PlatformIO's get_project_cache_dir # Mock PlatformIO's ProjectConfig cache_dir
with patch( with patch(
"platformio.project.helpers.get_project_cache_dir" "platformio.project.config.ProjectConfig.get_instance"
) as mock_get_cache_dir: ) as mock_get_instance:
mock_get_cache_dir.return_value = str(platformio_cache_dir) 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 # Call the function
with caplog.at_level("INFO"): with caplog.at_level("INFO"):
@@ -487,7 +493,7 @@ def test_clean_build_platformio_not_available(
# Mock import error for platformio # Mock import error for platformio
with ( with (
patch.dict("sys.modules", {"platformio.project.helpers": None}), patch.dict("sys.modules", {"platformio.project.config": None}),
caplog.at_level("INFO"), caplog.at_level("INFO"),
): ):
# Call the function # Call the function
@@ -521,11 +527,17 @@ def test_clean_build_empty_cache_dir(
# Verify pioenvs exists before # Verify pioenvs exists before
assert pioenvs_dir.exists() 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( with patch(
"platformio.project.helpers.get_project_cache_dir" "platformio.project.config.ProjectConfig.get_instance"
) as mock_get_cache_dir: ) as mock_get_instance:
mock_get_cache_dir.return_value = " " # Whitespace only 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 # Call the function
with caplog.at_level("INFO"): with caplog.at_level("INFO"):
@@ -724,3 +736,126 @@ def test_write_cpp_with_duplicate_markers(
# Call should raise an error # Call should raise an error
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"): with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
write_cpp("// New code") 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()