mirror of
https://github.com/esphome/esphome.git
synced 2026-09-05 04:26:02 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7aad659691 | ||
|
|
d6758377d1 | ||
|
|
5dbc8ffe4c | ||
|
|
0f982f03b2 | ||
|
|
afb0022dd0 | ||
|
|
c54f05869f |
@@ -162,8 +162,9 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
|
||||
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status)
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
|
||||
void Alpha3::update() {
|
||||
|
||||
@@ -2391,8 +2391,9 @@ void APIConnection::process_batch_() {
|
||||
} else if (payload_size == 0) {
|
||||
// payload_size == 0 with remove set means encoding hit OOM and the
|
||||
// connection is being dropped; warn only for a genuinely oversized message
|
||||
if (!this->flags_.remove)
|
||||
if (!this->flags_.remove) {
|
||||
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
|
||||
}
|
||||
this->clear_batch_();
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -62,8 +62,9 @@ BdkActivityState bdk_scan_state(uint8_t activity_idx) {
|
||||
|
||||
uint8_t bdk_scan_acquire_activity() {
|
||||
uint8_t idx = app_ble_get_idle_actv_idx_handle(SCAN_ACTV);
|
||||
if (idx == INVALID_ACTIVITY_IDX)
|
||||
if (idx == INVALID_ACTIVITY_IDX) {
|
||||
ESP_LOGE(TAG, "Scan start failed: no idle activity handle");
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
|
||||
@@ -181,8 +181,9 @@ void BK72xxBLE::enable() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bdaddr_live)
|
||||
if (!bdaddr_live) {
|
||||
ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started");
|
||||
}
|
||||
#endif
|
||||
|
||||
this->state_ = BLEComponentState::ACTIVE;
|
||||
@@ -210,8 +211,9 @@ void BK72xxBLE::loop() {
|
||||
// Re-check a settled scan; scan_start() refills the bring-up budget.
|
||||
// WARN: the only report of a drop that recovers inside its budget.
|
||||
if (this->scan_start(this->requested_.interval, this->requested_.window, this->requested_.active) !=
|
||||
ScanOpResult::SETTLED)
|
||||
ScanOpResult::SETTLED) {
|
||||
ESP_LOGW(TAG, "Controller dropped the scan; restarting");
|
||||
}
|
||||
}
|
||||
|
||||
// Drain the lock-free ring filled by the BLE task; all per-report work runs
|
||||
@@ -230,8 +232,9 @@ void BK72xxBLE::loop() {
|
||||
// Log dropped reports — only reachable when reports were processed; drops can
|
||||
// only occur while the queue is full, and only this loop drains it.
|
||||
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
||||
if (dropped > 0)
|
||||
if (dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped);
|
||||
}
|
||||
}
|
||||
|
||||
void BK72xxBLE::get_mac_lsb_first(uint8_t out[MAC_ADDRESS_SIZE]) const {
|
||||
@@ -449,8 +452,9 @@ ScanOpResult BK72xxBLE::advance_stop_(BdkActivityState state, bool ready) {
|
||||
if (!ready) {
|
||||
// Acting mid-operation could delete an activity whose start lands
|
||||
// afterwards, leaking the slot with the radio on; wait.
|
||||
if (this->last_result_ == ScanOpResult::SETTLED)
|
||||
if (this->last_result_ == ScanOpResult::SETTLED) {
|
||||
ESP_LOGD(TAG, "Scan stop deferred (controller busy)");
|
||||
}
|
||||
return ScanOpResult::PENDING;
|
||||
}
|
||||
// Settled, so CREATED unambiguously means "never started".
|
||||
@@ -474,8 +478,9 @@ ScanOpResult BK72xxBLE::advance_start_(BdkActivityState state, bool ready) {
|
||||
return ScanOpResult::PENDING;
|
||||
}
|
||||
if (!ready) {
|
||||
if (this->last_result_ == ScanOpResult::SETTLED)
|
||||
if (this->last_result_ == ScanOpResult::SETTLED) {
|
||||
ESP_LOGD(TAG, "Scan start deferred (controller busy)");
|
||||
}
|
||||
return ScanOpResult::PENDING;
|
||||
}
|
||||
if (state == BdkActivityState::CREATED) {
|
||||
|
||||
@@ -69,8 +69,9 @@ void BK72xxBLETracker::on_ota_global_state(ota::OTAState state, float progress,
|
||||
this->stop_scan();
|
||||
// The transfer starves the loop; a deferred stop would leave the radio
|
||||
// scanning for the whole update, so drain it here, bounded.
|
||||
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS))
|
||||
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS)) {
|
||||
ESP_LOGE(TAG, "Scan still stopping at OTA start; the radio may contend with the update");
|
||||
}
|
||||
} else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
|
||||
// On success the device reboots, so restore only on a failed/aborted update;
|
||||
// loop() restarts the scan on its next iteration (continuous idle branch).
|
||||
|
||||
@@ -80,8 +80,9 @@ void BLEBinaryOutput::write_state(bool state) {
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
|
||||
sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_GATT_OK)
|
||||
if (err != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_str(char_buf), err);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
@@ -327,10 +327,12 @@ void BME680Component::read_data_() {
|
||||
|
||||
ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%% gas_resistance=%.1fΩ", temperature, pressure,
|
||||
humidity, gas_resistance);
|
||||
if (!gas_valid)
|
||||
if (!gas_valid) {
|
||||
ESP_LOGW(TAG, "Gas measurement unsuccessful, reading invalid!");
|
||||
if (!heat_stable)
|
||||
}
|
||||
if (!heat_stable) {
|
||||
ESP_LOGW(TAG, "Heater unstable, reading invalid! (Normal for a few readings after a power cycle)");
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
|
||||
@@ -749,33 +749,39 @@ void Climate::dump_traits_(const char *tag) {
|
||||
}
|
||||
if (!traits.get_supported_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported modes:");
|
||||
for (ClimateMode m : traits.get_supported_modes())
|
||||
for (ClimateMode m : traits.get_supported_modes()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_fan_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported fan modes:");
|
||||
for (ClimateFanMode m : traits.get_supported_fan_modes())
|
||||
for (ClimateFanMode m : traits.get_supported_fan_modes()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
||||
for (const char *s : traits.get_supported_custom_fan_modes())
|
||||
for (const char *s : traits.get_supported_custom_fan_modes()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", s);
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported presets:");
|
||||
for (ClimatePreset p : traits.get_supported_presets())
|
||||
for (ClimatePreset p : traits.get_supported_presets()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
||||
for (const char *s : traits.get_supported_custom_presets())
|
||||
for (const char *s : traits.get_supported_custom_presets()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", s);
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_swing_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
||||
for (ClimateSwingMode m : traits.get_supported_swing_modes())
|
||||
for (ClimateSwingMode m : traits.get_supported_swing_modes()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,8 +154,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
}
|
||||
}
|
||||
if (error_code != 0) {
|
||||
if (report_errors)
|
||||
if (report_errors) {
|
||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#include "epaper_spi_uc8179.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
static constexpr const char *const TAG = "epaper_spi.uc8179";
|
||||
|
||||
bool EPaperUC8179::initialise(bool partial) {
|
||||
EPaperBase::initialise(partial); // send the model init sequence
|
||||
this->partial_ = partial;
|
||||
ESP_LOGV(TAG, "Power on");
|
||||
// POWER ON must precede the waveform/mode registers and the data transfer
|
||||
// (the original driver powers on and busy-waits before writing them).
|
||||
// The state machine busy-waits before entering TRANSFER_DATA.
|
||||
this->command(0x04);
|
||||
// Give the busy line time to assert before the state machine polls it
|
||||
this->next_delay_ = 100;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set up the refresh mode. Must be called after power-on has completed.
|
||||
void EPaperUC8179::set_refresh_mode_() {
|
||||
if (!this->is_using_partial_update_()) {
|
||||
return; // plain full refresh uses the mode set by the init sequence
|
||||
}
|
||||
// Fast and partial refresh use flipped data polarity and a floating border
|
||||
this->cmd_data(0x50, {0xA9, 0x07});
|
||||
// Force the waveform via the temperature registers: 0x5A selects the fast
|
||||
// full-refresh waveform, 0x6E the partial-refresh waveform
|
||||
this->cmd_data(0xE0, {0x02});
|
||||
if (this->partial_) {
|
||||
this->cmd_data(0xE5, {0x6E});
|
||||
this->command(0x91); // enter partial mode
|
||||
// Set the partial window to the full screen
|
||||
const uint16_t x_end = this->width_ - 1;
|
||||
const uint16_t y_end = this->height_ - 1;
|
||||
this->cmd_data(0x90, {0x00, 0x00, static_cast<uint8_t>(x_end >> 8), static_cast<uint8_t>(x_end & 0xFF), 0x00, 0x00,
|
||||
static_cast<uint8_t>(y_end >> 8), static_cast<uint8_t>(y_end & 0xFF), 0x01});
|
||||
} else {
|
||||
this->cmd_data(0xE5, {0x5A});
|
||||
this->command(0x92); // exit partial mode
|
||||
}
|
||||
}
|
||||
|
||||
bool HOT EPaperUC8179::transfer_data() {
|
||||
const uint32_t start_time = millis();
|
||||
const size_t buffer_length = this->buffer_length_;
|
||||
if (this->current_data_index_ == 0) {
|
||||
this->set_refresh_mode_();
|
||||
}
|
||||
// Fast full refresh sends the previous-image plane as well, so that every pixel transitions
|
||||
const bool two_pass = this->is_using_partial_update_() && !this->partial_;
|
||||
// Plain full refresh sends inverted data (buffer is 1=white, the wire wants 0=white);
|
||||
// in fast/partial mode the data polarity is flipped via the VCOM/data-interval
|
||||
// register instead, so the new-image plane is sent unmodified
|
||||
const bool invert_new_data = !this->is_using_partial_update_();
|
||||
|
||||
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
|
||||
|
||||
// Phase 1 (fast full refresh only): previous image via 0x10 (DTM1), inverse of the new image
|
||||
if (two_pass && this->current_data_index_ < buffer_length) {
|
||||
if (this->current_data_index_ == 0) {
|
||||
this->command(0x10); // DATA START TRANSMISSION 1 (previous image)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < buffer_length) {
|
||||
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, buffer_length - this->current_data_index_);
|
||||
for (size_t i = 0; i < bytes_to_copy; i++) {
|
||||
bytes_to_send[i] = ~this->buffer_[this->current_data_index_ + i];
|
||||
}
|
||||
this->write_array(bytes_to_send, bytes_to_copy);
|
||||
this->current_data_index_ += bytes_to_copy;
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
// Phase 2: new image via 0x13 (DTM2)
|
||||
const size_t offset = two_pass ? buffer_length : 0;
|
||||
const size_t total = offset + buffer_length;
|
||||
if (this->current_data_index_ < total) {
|
||||
if (this->current_data_index_ == offset) {
|
||||
this->command(0x13); // DATA START TRANSMISSION 2 (new image)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < total) {
|
||||
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, total - this->current_data_index_);
|
||||
const size_t data_idx = this->current_data_index_ - offset;
|
||||
for (size_t i = 0; i < bytes_to_copy; i++) {
|
||||
const uint8_t byte = this->buffer_[data_idx + i];
|
||||
bytes_to_send[i] = invert_new_data ? ~byte : byte;
|
||||
}
|
||||
this->write_array(bytes_to_send, bytes_to_copy);
|
||||
this->current_data_index_ += bytes_to_copy;
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
this->current_data_index_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperUC8179::power_on() {
|
||||
// Power-on is sent at the end of initialise() instead, because the
|
||||
// waveform/mode registers and the data transfer must follow it
|
||||
}
|
||||
|
||||
void EPaperUC8179::refresh_screen(bool /*partial*/) {
|
||||
ESP_LOGV(TAG, "Refresh");
|
||||
this->command(0x12); // DISPLAY REFRESH
|
||||
// Delay the next busy poll: the busy line takes a short time to assert after
|
||||
// the refresh command, and polling too early would read it as already idle
|
||||
this->next_delay_ = 100;
|
||||
}
|
||||
|
||||
void EPaperUC8179::power_off() {
|
||||
ESP_LOGV(TAG, "Power off");
|
||||
this->command(0x02); // POWER OFF
|
||||
}
|
||||
|
||||
void EPaperUC8179::deep_sleep() {
|
||||
// Deep sleep loses the previous-image RAM that partial refresh compares against
|
||||
if (!this->is_using_partial_update_()) {
|
||||
ESP_LOGV(TAG, "Deep sleep");
|
||||
this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* Monochrome e-paper displays using the UC8179 controller.
|
||||
* Supports: 7.5" V2 (EPD_7in5_V2), 800x480 pixels, as used by the
|
||||
* Waveshare 7.5" V2 HAT and the Seeed reTerminal E1001.
|
||||
*
|
||||
* Buffer layout: 1 bit per pixel, 1=white, 0=black (the base class default).
|
||||
*
|
||||
* The INITIALISE state sends the panel configuration followed by power-on
|
||||
* (0x04); the state machine busy-waits for power-on to complete before
|
||||
* TRANSFER_DATA, which first writes the waveform/mode registers (these are
|
||||
* only accepted while powered) and then the image data. The state machine
|
||||
* busy-waits again before triggering REFRESH_SCREEN (0x12).
|
||||
*
|
||||
* Three refresh modes are used, following the Waveshare EPD_7in5_V2 examples:
|
||||
* - full_update_every == 1: plain full refresh. The new image is sent
|
||||
* inverted to DTM2 (0x13) and the controller uses its normal waveform.
|
||||
* - full_update_every > 1, full update: fast full refresh. The data polarity
|
||||
* is flipped via the VCOM/data-interval register, a fast waveform is forced
|
||||
* via the temperature registers, and the image is sent to both DTM1 (0x10,
|
||||
* inverted) and DTM2 (0x13) so that every pixel transitions.
|
||||
* - full_update_every > 1, partial update: partial refresh. A partial-update
|
||||
* waveform is forced, partial mode is entered with a full-screen window and
|
||||
* only DTM2 is sent; the controller compares against its previous-image RAM.
|
||||
*/
|
||||
class EPaperUC8179 final : public EPaperBase {
|
||||
public:
|
||||
EPaperUC8179(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
|
||||
this->buffer_length_ = this->row_width_ * height;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool initialise(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override;
|
||||
void power_off() override;
|
||||
void deep_sleep() override;
|
||||
void set_refresh_mode_();
|
||||
|
||||
// Set by initialise() so transfer_data() knows which planes to send
|
||||
bool partial_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Monochrome e-paper displays using the UC8179 controller.
|
||||
|
||||
Supported models:
|
||||
- waveshare-7.5in-v2: 7.5" mono display, 800x480 pixels (EPD_7in5_V2)
|
||||
- seeed-reterminal-e1001: Seeed reTerminal E1001, which uses the same
|
||||
7.5" 800x480 panel on an integrated ESP32-S3 board
|
||||
|
||||
Panel configuration and power-on (0x04) are both sent during the INITIALISE
|
||||
state; the state machine's built-in busy wait then covers the power-on delay
|
||||
before the waveform/mode registers and image data are transferred.
|
||||
|
||||
These displays support fast full and partial refresh: set ``full_update_every``
|
||||
greater than 1 to enable it. Every ``full_update_every``-th update is a fast
|
||||
full refresh, with partial refreshes in between.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from esphome.const import CONF_DATA_RATE
|
||||
|
||||
from . import EpaperModel
|
||||
|
||||
|
||||
class UC8179(EpaperModel):
|
||||
"""EpaperModel class for monochrome displays using the UC8179 controller."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
class_name: str = "EPaperUC8179",
|
||||
data_rate: str = "10MHz",
|
||||
**defaults: Any,
|
||||
) -> None:
|
||||
defaults.setdefault(CONF_DATA_RATE, data_rate)
|
||||
super().__init__(name, class_name, **defaults)
|
||||
|
||||
def get_init_sequence(self, config: dict) -> tuple:
|
||||
"""Generate the initialization sequence for UC8179 mono displays.
|
||||
|
||||
Panel configuration only — the driver appends power-on (0x04) at the
|
||||
end of the INITIALISE state, and the state machine busy-waits for it
|
||||
to complete before the data transfer starts.
|
||||
"""
|
||||
width, height = self.get_dimensions(config)
|
||||
return (
|
||||
# POWER SETTING
|
||||
(0x01, 0x07, 0x07, 0x3F, 0x3F),
|
||||
# BOOSTER SOFT START
|
||||
(0x06, 0x17, 0x17, 0x28, 0x17),
|
||||
# PANEL SETTING (black/white mode, LUT from OTP)
|
||||
(0x00, 0x1F),
|
||||
# RESOLUTION SETTING (width x height)
|
||||
(
|
||||
0x61,
|
||||
(width >> 8) & 0xFF,
|
||||
width & 0xFF,
|
||||
(height >> 8) & 0xFF,
|
||||
height & 0xFF,
|
||||
),
|
||||
# DUAL SPI MODE (disabled)
|
||||
(0x15, 0x00),
|
||||
# VCOM AND DATA INTERVAL SETTING
|
||||
(0x50, 0x10, 0x07),
|
||||
# TCON SETTING
|
||||
(0x60, 0x22),
|
||||
)
|
||||
|
||||
|
||||
uc8179 = UC8179("uc8179")
|
||||
|
||||
# Waveshare 7.5" V2 mono (EPD_7in5_V2) — 800x480, UC8179 controller
|
||||
waveshare_7_5_v2 = uc8179.extend(
|
||||
"waveshare-7.5in-v2",
|
||||
width=800,
|
||||
height=480,
|
||||
)
|
||||
|
||||
# Seeed reTerminal E1001 — 7.5" mono e-paper (800x480), same panel as the
|
||||
# Waveshare 7.5" V2, driven by an integrated ESP32-S3 board
|
||||
waveshare_7_5_v2.extend(
|
||||
"seeed-reterminal-e1001",
|
||||
cs_pin=10,
|
||||
dc_pin=11,
|
||||
reset_pin=12,
|
||||
busy_pin={
|
||||
"number": 13,
|
||||
"inverted": True,
|
||||
"mode": {
|
||||
"input": True,
|
||||
"pullup": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -210,8 +210,9 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
|
||||
if (!image) {
|
||||
// A shutdown is not a lost frame: wait_for_image_() returns empty as soon
|
||||
// as running_ clears, and the loop condition below ends the stream anyway.
|
||||
if (this->running_)
|
||||
if (this->running_) {
|
||||
ESP_LOGW(TAG, "STREAM: failed to acquire frame");
|
||||
}
|
||||
res = ESP_FAIL;
|
||||
}
|
||||
if (res == ESP_OK) {
|
||||
|
||||
@@ -334,8 +334,9 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
|
||||
}
|
||||
if (traits.supports_preset_modes()) {
|
||||
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
|
||||
for (const char *s : traits.supported_preset_modes())
|
||||
for (const char *s : traits.supported_preset_modes()) {
|
||||
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@ void HBridgeSwitch::dump_config() {
|
||||
LOG_PIN(" On Pin: ", this->on_pin_);
|
||||
LOG_PIN(" Off Pin: ", this->off_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Pulse length: %" PRId32 " ms", this->pulse_length_);
|
||||
if (this->wait_time_)
|
||||
if (this->wait_time_) {
|
||||
ESP_LOGCONFIG(TAG, " Wait time %" PRId32 " ms", this->wait_time_);
|
||||
}
|
||||
}
|
||||
|
||||
void HBridgeSwitch::write_state(bool state) {
|
||||
|
||||
@@ -44,8 +44,9 @@ void HE60rCover::dump_config() {
|
||||
" Close Duration: %.1fs",
|
||||
this->open_duration_ / 1e3f, this->close_duration_ / 1e3f);
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value())
|
||||
if (restore.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
|
||||
}
|
||||
}
|
||||
|
||||
void HE60rCover::endstop_reached_(CoverOperation operation) {
|
||||
@@ -77,8 +78,9 @@ void HE60rCover::process_rx_(uint8_t data) {
|
||||
ESP_LOGV(TAG, "Process RX data %X", data);
|
||||
if (!this->query_seen_) {
|
||||
this->query_seen_ = data == QUERY_BYTE;
|
||||
if (!this->query_seen_)
|
||||
if (!this->query_seen_) {
|
||||
ESP_LOGD(TAG, "RX Byte %02X", data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (data) {
|
||||
|
||||
@@ -257,8 +257,9 @@ void HoermannHcp::on_state_reg_(uint16_t value) {
|
||||
}
|
||||
}
|
||||
// The low byte can change on its own, so only report a state we cannot decode once.
|
||||
if (state != (previous >> 8))
|
||||
if (state != (previous >> 8)) {
|
||||
ESP_LOGW(TAG, "Unknown door state 0x%02X", state);
|
||||
}
|
||||
}
|
||||
|
||||
// Low byte of register 6: bit 0x10 is the lamp, bit 0x04 the relay. The reference implementation records
|
||||
|
||||
@@ -16,26 +16,33 @@ void KeyCollector::loop() {
|
||||
void KeyCollector::dump_config() {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_CONFIG
|
||||
ESP_LOGCONFIG(TAG, "Key Collector:");
|
||||
if (this->min_length_ > 0)
|
||||
if (this->min_length_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " min length: %d", this->min_length_);
|
||||
if (this->max_length_ > 0)
|
||||
}
|
||||
if (this->max_length_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " max length: %d", this->max_length_);
|
||||
if (!this->back_keys_.empty())
|
||||
}
|
||||
if (!this->back_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " erase keys '%s'", this->back_keys_.c_str());
|
||||
if (!this->clear_keys_.empty())
|
||||
}
|
||||
if (!this->clear_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " clear keys '%s'", this->clear_keys_.c_str());
|
||||
if (!this->start_keys_.empty())
|
||||
}
|
||||
if (!this->start_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " start keys '%s'", this->start_keys_.c_str());
|
||||
}
|
||||
if (!this->end_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" end keys '%s'\n"
|
||||
" end key is required: %s",
|
||||
this->end_keys_.c_str(), ONOFF(this->end_key_required_));
|
||||
}
|
||||
if (!this->allowed_keys_.empty())
|
||||
if (!this->allowed_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " allowed keys '%s'", this->allowed_keys_.c_str());
|
||||
if (this->timeout_ > 0)
|
||||
}
|
||||
if (this->timeout_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -333,8 +333,9 @@ void LN882HBLE::loop() {
|
||||
// the queue empty — from the very first report on. Checking here keeps that
|
||||
// failure visible instead of producing a scanner that is silently dead.
|
||||
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
||||
if (dropped > 0)
|
||||
if (dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u scan reports (queue full or out of memory for a report slot)", dropped);
|
||||
}
|
||||
// Drain the lock-free ring filled by the rw task; all per-report work runs
|
||||
// here on the main task, then the report returns to the pool.
|
||||
BLEScanReport *report = this->report_queue_.pop();
|
||||
|
||||
@@ -1059,8 +1059,9 @@ static void *lv_alloc_draw_buf(size_t size, bool internal) {
|
||||
void *buffer;
|
||||
size = LV_ROUND_UP(size, LV_DRAW_BUF_ALIGN);
|
||||
buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT
|
||||
if (buffer == nullptr)
|
||||
if (buffer == nullptr) {
|
||||
ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : "");
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
@@ -237,8 +237,9 @@ void MipiDsi::write_to_display_(int x_start, int y_start, int w, int h, const ui
|
||||
xSemaphoreTake(this->io_lock_, portMAX_DELAY);
|
||||
}
|
||||
}
|
||||
if (err != ESP_OK)
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
bool MipiDsi::check_buffer_() {
|
||||
|
||||
@@ -243,8 +243,9 @@ void MipiRgb::write_to_display_(int x_start, int y_start, int w, int h, const ui
|
||||
ptr += stride; // next line
|
||||
}
|
||||
}
|
||||
if (err != ESP_OK)
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
bool MipiRgb::check_buffer_() {
|
||||
|
||||
@@ -31,12 +31,15 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
|
||||
LOG_PIN(" CS Pin: ", cs);
|
||||
LOG_PIN(" Reset Pin: ", reset);
|
||||
LOG_PIN(" DC Pin: ", dc);
|
||||
if (offset_width != 0)
|
||||
if (offset_width != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Offset width: %d", offset_width);
|
||||
if (offset_height != 0)
|
||||
}
|
||||
if (offset_height != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Offset height: %d", offset_height);
|
||||
if (brightness.has_value())
|
||||
}
|
||||
if (brightness.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Brightness: %u", brightness.value());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::mipi_spi
|
||||
|
||||
@@ -1199,15 +1199,17 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
|
||||
this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() {
|
||||
ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1,
|
||||
this->deferred_payload_len_ - 1);
|
||||
if (!this->send_frame_(frame))
|
||||
if (!this->send_frame_(frame)) {
|
||||
ESP_LOGE(TAG, "Deferred server reply dropped: transmission still blocked");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ModbusFrame frame(payload[0], payload + 1, len - 1);
|
||||
if (!this->send_frame_(frame))
|
||||
if (!this->send_frame_(frame)) {
|
||||
ESP_LOGE(TAG, "Server reply dropped: a frame arrived during the send delay");
|
||||
}
|
||||
}
|
||||
|
||||
void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_to_clear) {
|
||||
|
||||
@@ -39,10 +39,12 @@ inline char *append_char(char *p, char c) {
|
||||
// Function implementation of LOG_MQTT_COMPONENT macro to reduce code size
|
||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (state_topic)
|
||||
if (state_topic) {
|
||||
ESP_LOGCONFIG(tag, " State Topic: '%s'", obj->get_state_topic_to_(buf).c_str());
|
||||
if (command_topic)
|
||||
}
|
||||
if (command_topic) {
|
||||
ESP_LOGCONFIG(tag, " Command Topic: '%s'", obj->get_command_topic_to_(buf).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
|
||||
|
||||
@@ -18,8 +18,9 @@ const std::vector<uint64_t> &OneWireBus::get_devices() { return this->devices_;
|
||||
|
||||
bool OneWireBus::reset_() {
|
||||
int res = this->reset_int();
|
||||
if (res == -1)
|
||||
if (res == -1) {
|
||||
ESP_LOGE(TAG, "1-wire bus is held low");
|
||||
}
|
||||
return res == 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -551,12 +551,14 @@ void PacketTransport::dump_config() {
|
||||
" Ping-pong: %s",
|
||||
this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_));
|
||||
#ifdef USE_SENSOR
|
||||
for (const auto &sensor : this->sensors_)
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
for (const auto &sensor : this->binary_sensors_)
|
||||
for (const auto &sensor : this->binary_sensors_) {
|
||||
ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id);
|
||||
}
|
||||
#endif
|
||||
for (const auto &host : this->providers_) {
|
||||
ESP_LOGCONFIG(TAG, " Remote host: %s", host.first.c_str());
|
||||
@@ -564,15 +566,17 @@ void PacketTransport::dump_config() {
|
||||
#ifdef USE_SENSOR
|
||||
auto rs = this->remote_sensors_.find(host.first.c_str());
|
||||
if (rs != this->remote_sensors_.end()) {
|
||||
for (const auto &key : rs->second | std::views::keys)
|
||||
for (const auto &key : rs->second | std::views::keys) {
|
||||
ESP_LOGCONFIG(TAG, " Sensor: %s", key.c_str());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
auto rbs = this->remote_binary_sensors_.find(host.first.c_str());
|
||||
if (rbs != this->remote_binary_sensors_.end()) {
|
||||
for (const auto &key : rbs->second | std::views::keys)
|
||||
for (const auto &key : rbs->second | std::views::keys) {
|
||||
ESP_LOGCONFIG(TAG, " Binary Sensor: %s", key.c_str());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -124,8 +124,9 @@ void QwiicPIRComponent::dump_config() {
|
||||
|
||||
void QwiicPIRComponent::clear_events_() {
|
||||
// Clear event status register
|
||||
if (!this->write_byte(QWIIC_PIR_EVENT_STATUS, 0x00))
|
||||
if (!this->write_byte(QWIIC_PIR_EVENT_STATUS, 0x00)) {
|
||||
ESP_LOGW(TAG, "Failed to clear events");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::qwiic_pir
|
||||
|
||||
@@ -75,8 +75,9 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (err != ESP_OK)
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
int RpiDpiRgb::get_width() {
|
||||
|
||||
@@ -629,8 +629,9 @@ stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const
|
||||
stm->pid = (buf[1] << 8) | buf[2];
|
||||
if (returned > 2) {
|
||||
ESP_LOGD(TAG, "This bootloader returns %d extra bytes in PID:", returned);
|
||||
for (auto i = 2; i <= returned; i++)
|
||||
for (auto i = 2; i <= returned; i++) {
|
||||
ESP_LOGD(TAG, " %02x", buf[i]);
|
||||
}
|
||||
}
|
||||
if (stm32_get_ack(stm) != STM32_ERR_OK) {
|
||||
return make_stm32_with_deletor(nullptr);
|
||||
|
||||
@@ -406,8 +406,9 @@ class SPIClient {
|
||||
this->release_device_, this->write_only_);
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
this->delegate_->set_psram_dma(this->psram_dma_);
|
||||
if (this->psram_dma_)
|
||||
if (this->psram_dma_) {
|
||||
esph_log_config("spi_device", "PSRAM DMA: enabled");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,9 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
if (this->release_device_)
|
||||
this->add_device_();
|
||||
if (this->is_ready()) {
|
||||
if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK)
|
||||
if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to acquire SPI bus");
|
||||
}
|
||||
SPIDelegate::begin_transaction();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "SPI device not ready, cannot begin transaction");
|
||||
@@ -63,8 +64,9 @@ class SPIDelegateHw : public SPIDelegate {
|
||||
|
||||
~SPIDelegateHw() override {
|
||||
esp_err_t const err = spi_bus_remove_device(this->handle_);
|
||||
if (err != ESP_OK)
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Remove device failed - err %X", err);
|
||||
}
|
||||
}
|
||||
|
||||
// do a transfer. either txbuf or rxbuf (but not both) may be null.
|
||||
@@ -284,8 +286,9 @@ class SPIBusHw : public SPIBus {
|
||||
}
|
||||
buscfg.max_transfer_sz = MAX_TRANSFER_SIZE;
|
||||
auto err = spi_bus_initialize(channel, &buscfg, SPI_DMA_CH_AUTO);
|
||||
if (err != ESP_OK)
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Bus init failed - err %X", err);
|
||||
}
|
||||
}
|
||||
|
||||
SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin,
|
||||
|
||||
@@ -78,8 +78,9 @@ void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (err != ESP_OK)
|
||||
if (err != ESP_OK) {
|
||||
esph_log_e(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
void ST7701S::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
@@ -2,7 +2,13 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CONDITION, CONF_ID, CONF_LAMBDA, CONF_STATE
|
||||
from esphome.const import (
|
||||
CONF_CONDITION,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_STATE,
|
||||
)
|
||||
from esphome.cpp_generator import LambdaExpression
|
||||
|
||||
from .. import template_ns
|
||||
@@ -12,7 +18,11 @@ TemplateBinarySensor = template_ns.class_(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
binary_sensor.binary_sensor_schema(TemplateBinarySensor)
|
||||
cv.with_visibility(
|
||||
binary_sensor.binary_sensor_schema(TemplateBinarySensor),
|
||||
cv.Visibility.UI,
|
||||
CONF_DEVICE_CLASS,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_CONDITION): cv.returning_lambda,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DEVICE_CLASS
|
||||
|
||||
from .. import template_ns
|
||||
|
||||
TemplateButton = template_ns.class_("TemplateButton", button.Button)
|
||||
|
||||
CONFIG_SCHEMA = button.button_schema(TemplateButton)
|
||||
CONFIG_SCHEMA = cv.with_visibility(
|
||||
button.button_schema(TemplateButton), cv.Visibility.UI, CONF_DEVICE_CLASS
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.const import (
|
||||
CONF_ASSUMED_STATE,
|
||||
CONF_CLOSE_ACTION,
|
||||
CONF_CURRENT_OPERATION,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_OPEN_ACTION,
|
||||
@@ -38,7 +39,11 @@ CONF_HAS_POSITION = "has_position"
|
||||
CONF_TOGGLE_ACTION = "toggle_action"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cover.cover_schema(TemplateCover)
|
||||
cv.with_visibility(
|
||||
cover.cover_schema(TemplateCover),
|
||||
cv.Visibility.UI,
|
||||
CONF_DEVICE_CLASS,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import event
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_EVENT_TYPES
|
||||
from esphome.const import CONF_DEVICE_CLASS, CONF_EVENT_TYPES
|
||||
|
||||
from .. import template_ns
|
||||
|
||||
@@ -9,7 +9,9 @@ CODEOWNERS = ["@nohat"]
|
||||
|
||||
TemplateEvent = template_ns.class_("TemplateEvent", event.Event, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = event.event_schema(TemplateEvent).extend(
|
||||
CONFIG_SCHEMA = cv.with_visibility(
|
||||
event.event_schema(TemplateEvent), cv.Visibility.UI, CONF_DEVICE_CLASS
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_EVENT_TYPES): cv.ensure_list(cv.string_strict),
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ID,
|
||||
CONF_INITIAL_VALUE,
|
||||
CONF_LAMBDA,
|
||||
@@ -12,6 +13,7 @@ from esphome.const import (
|
||||
CONF_RESTORE_VALUE,
|
||||
CONF_SET_ACTION,
|
||||
CONF_STEP,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
|
||||
from .. import template_ns
|
||||
@@ -46,7 +48,12 @@ def validate(config):
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
number.number_schema(TemplateNumber)
|
||||
cv.with_visibility(
|
||||
number.number_schema(TemplateNumber),
|
||||
cv.Visibility.UI,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_MAX_VALUE): cv.float_,
|
||||
|
||||
@@ -2,7 +2,16 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE
|
||||
from esphome.const import (
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_FORCE_UPDATE,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_STATE,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
|
||||
from .. import template_ns
|
||||
|
||||
@@ -11,9 +20,14 @@ TemplateSensor = template_ns.class_(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
TemplateSensor,
|
||||
accuracy_decimals=1,
|
||||
cv.with_visibility(
|
||||
sensor.sensor_schema(TemplateSensor, accuracy_decimals=1),
|
||||
cv.Visibility.UI,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_FORCE_UPDATE,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ASSUMED_STATE,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_OPTIMISTIC,
|
||||
@@ -31,7 +32,11 @@ def validate(config):
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
switch.switch_schema(TemplateSwitch)
|
||||
cv.with_visibility(
|
||||
switch.switch_schema(TemplateSwitch),
|
||||
cv.Visibility.UI,
|
||||
CONF_DEVICE_CLASS,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
|
||||
|
||||
@@ -3,7 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.components.text_sensor import TextSensorPublishAction
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE
|
||||
from esphome.const import CONF_DEVICE_CLASS, CONF_ID, CONF_LAMBDA, CONF_STATE
|
||||
|
||||
from .. import template_ns
|
||||
|
||||
@@ -12,7 +12,11 @@ TemplateTextSensor = template_ns.class_(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
text_sensor.text_sensor_schema()
|
||||
cv.with_visibility(
|
||||
text_sensor.text_sensor_schema(),
|
||||
cv.Visibility.UI,
|
||||
CONF_DEVICE_CLASS,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TemplateTextSensor),
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.const import (
|
||||
CONF_ASSUMED_STATE,
|
||||
CONF_CLOSE_ACTION,
|
||||
CONF_CURRENT_OPERATION,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_OPEN_ACTION,
|
||||
@@ -36,7 +37,11 @@ CONF_HAS_POSITION = "has_position"
|
||||
CONF_TOGGLE_ACTION = "toggle_action"
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
valve.valve_schema(TemplateValve)
|
||||
cv.with_visibility(
|
||||
valve.valve_schema(TemplateValve),
|
||||
cv.Visibility.UI,
|
||||
CONF_DEVICE_CLASS,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
|
||||
|
||||
@@ -177,14 +177,18 @@ water_heater::WaterHeaterMode TuyaWaterHeater::default_on_mode_() const {
|
||||
|
||||
void TuyaWaterHeater::dump_config() {
|
||||
LOG_WATER_HEATER("", "Tuya Water Heater", this);
|
||||
if (this->switch_id_.has_value())
|
||||
if (this->switch_id_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_);
|
||||
if (this->mode_id_.has_value())
|
||||
}
|
||||
if (this->mode_id_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Mode has datapoint ID %u", *this->mode_id_);
|
||||
if (this->target_temperature_id_.has_value())
|
||||
}
|
||||
if (this->target_temperature_id_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_);
|
||||
if (this->current_temperature_id_.has_value())
|
||||
}
|
||||
if (this->current_temperature_id_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::tuya
|
||||
|
||||
@@ -129,8 +129,9 @@ void UDPComponent::dump_config() {
|
||||
" Listen Port: %u\n"
|
||||
" Broadcast Port: %u",
|
||||
this->listen_port_, this->broadcast_port_);
|
||||
for (const char *address : this->addresses_)
|
||||
for (const char *address : this->addresses_) {
|
||||
ESP_LOGCONFIG(TAG, " Address: %s", address);
|
||||
}
|
||||
if (this->listen_address_.has_value()) {
|
||||
char addr_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf));
|
||||
@@ -145,8 +146,9 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
for (const auto &saddr : this->sockaddrs_) {
|
||||
auto result = this->broadcast_socket_->sendto(data, size, 0, &saddr, sizeof(saddr));
|
||||
if (result < 0)
|
||||
if (result < 0) {
|
||||
ESP_LOGW(TAG, "sendto() error %d", errno);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SOCKET_IMPL_LWIP_TCP
|
||||
@@ -155,8 +157,9 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) {
|
||||
if (this->udp_client_.beginPacketMulticast(saddr, this->broadcast_port_, iface, 128) != 0) {
|
||||
this->udp_client_.write(data, size);
|
||||
auto result = this->udp_client_.endPacket();
|
||||
if (result == 0)
|
||||
if (result == 0) {
|
||||
ESP_LOGW(TAG, "udp.write() error");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -110,8 +110,9 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) {
|
||||
// Handle packet
|
||||
size_t data_len = (packet_len - 6) / 3;
|
||||
if (data_len == 0) {
|
||||
if (packet[4] == UPONOR_ID_REQUEST)
|
||||
if (packet[4] == UPONOR_ID_REQUEST) {
|
||||
ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08" PRIX32 "", device_address);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -194,8 +194,9 @@ std::vector<CdcEps> USBUartTypePL2303::parse_descriptors(usb_device_handle_t dev
|
||||
}
|
||||
}
|
||||
|
||||
if (cdc_devs.empty())
|
||||
if (cdc_devs.empty()) {
|
||||
ESP_LOGE(TAG, "PL2303: failed to find bulk IN+OUT endpoints");
|
||||
}
|
||||
|
||||
return cdc_devs;
|
||||
}
|
||||
|
||||
@@ -40,8 +40,9 @@ void WakeOnLanButton::press_action() {
|
||||
memcpy(buffer + i * sizeof(this->macaddr_) + sizeof(PREFIX), this->macaddr_, sizeof(this->macaddr_));
|
||||
}
|
||||
if (this->broadcast_socket_->sendto(buffer, sizeof(buffer), 0, reinterpret_cast<const sockaddr *>(&saddr),
|
||||
addr_len) <= 0)
|
||||
addr_len) <= 0) {
|
||||
ESP_LOGW(TAG, "sendto() error %d", errno);
|
||||
}
|
||||
#else
|
||||
IPAddress broadcast = IPAddress(255, 255, 255, 255);
|
||||
for (auto ip : esphome::network::get_ip_addresses()) {
|
||||
|
||||
@@ -348,14 +348,18 @@ size_t WeikaiChannel::rx_in_fifo_() {
|
||||
uint8_t const fsr = this->reg(WKREG_FSR);
|
||||
if (fsr & (FSR_RFOE | FSR_RFLB | FSR_RFFE | FSR_RFPE)) {
|
||||
char bin_buf[9];
|
||||
if (fsr & FSR_RFOE)
|
||||
if (fsr & FSR_RFOE) {
|
||||
ESP_LOGE(TAG, "Receive data overflow FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
if (fsr & FSR_RFLB)
|
||||
}
|
||||
if (fsr & FSR_RFLB) {
|
||||
ESP_LOGE(TAG, "Receive line break FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
if (fsr & FSR_RFFE)
|
||||
}
|
||||
if (fsr & FSR_RFFE) {
|
||||
ESP_LOGE(TAG, "Receive frame error FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
if (fsr & FSR_RFPE)
|
||||
}
|
||||
if (fsr & FSR_RFPE) {
|
||||
ESP_LOGE(TAG, "Receive parity error FSR=%s", format_bin_to(bin_buf, fsr));
|
||||
}
|
||||
}
|
||||
if ((available == 0) && (fsr & FSR_RFDAT)) {
|
||||
// here we should be very careful because we can have something like this:
|
||||
@@ -495,8 +499,9 @@ void print_buffer(std::vector<uint8_t> buffer) {
|
||||
hex_buffer[(3 * 32) + 1] = 0;
|
||||
for (size_t i = 0; i < buffer.size(); i++) {
|
||||
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", buffer[i]);
|
||||
if (i % 32 == 31)
|
||||
if (i % 32 == 31) {
|
||||
ESP_LOGI(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
if (buffer.size() % 32) {
|
||||
// null terminate if incomplete line
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import contextmanager, suppress
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from ipaddress import (
|
||||
AddressValueError,
|
||||
@@ -418,6 +419,37 @@ class Required(vol.Required):
|
||||
self.visibility: Visibility | None = visibility
|
||||
|
||||
|
||||
def with_visibility(schema: Schema, visibility: Visibility, *keys: str) -> Schema:
|
||||
"""Return a copy of ``schema`` with the given ``keys`` re-marked at ``visibility``.
|
||||
|
||||
Lets a platform override the editor :class:`Visibility` of fields it
|
||||
inherits from a shared schema builder — without that builder needing a
|
||||
visibility parameter of its own. The canonical use is a ``template``
|
||||
platform promoting the value metadata its user is expected to define
|
||||
(``device_class``, ``unit_of_measurement``, …) onto the main form:
|
||||
|
||||
CONFIG_SCHEMA = cv.with_visibility(
|
||||
sensor.sensor_schema(TemplateSensor),
|
||||
cv.Visibility.UI,
|
||||
CONF_DEVICE_CLASS, CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
|
||||
The original marker's key, default and validator are preserved; only the
|
||||
visibility changes, and the input ``schema`` is left untouched. Raises if
|
||||
a requested key is not present so typos fail at schema-build time.
|
||||
"""
|
||||
wanted = {str(k) for k in keys}
|
||||
overrides = {}
|
||||
for marker, validator in schema.schema.items():
|
||||
if str(marker) in wanted:
|
||||
marker = copy.copy(marker)
|
||||
marker.visibility = visibility
|
||||
overrides[marker] = validator
|
||||
if missing := wanted - {str(m) for m in overrides}:
|
||||
raise ValueError(f"with_visibility: keys not in schema: {sorted(missing)}")
|
||||
return schema.extend(overrides)
|
||||
|
||||
|
||||
class FinalExternalInvalid(Invalid):
|
||||
"""Represents an invalid value in the final validation phase where the path should not be prepended."""
|
||||
|
||||
|
||||
@@ -457,6 +457,15 @@ def _clone_complete_marker_path(repo_dir: Path) -> Path:
|
||||
return repo_dir / ".git" / _CLONE_COMPLETE_MARKER
|
||||
|
||||
|
||||
def has_complete_clone(
|
||||
url: str, ref: str | None, domain: str, subpath: Path | None = None
|
||||
) -> bool:
|
||||
"""Lock-free probe for a complete clone; can go stale immediately, so
|
||||
best-effort decisions only, never a substitute for ``clone_or_update``."""
|
||||
repo_dir = _repo_entry_dir(_cache_key(url, ref), domain, subpath)
|
||||
return _clone_complete_marker_path(repo_dir).is_file()
|
||||
|
||||
|
||||
def _clear_clone_complete_marker(repo_dir: Path) -> None:
|
||||
"""Best-effort removal of the completion marker.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ regardless of which toolchain consumes the result.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
from collections.abc import Callable, Iterable
|
||||
from collections.abc import Callable, Hashable, Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from functools import partial
|
||||
import glob
|
||||
@@ -99,6 +99,17 @@ class Source:
|
||||
) -> Path:
|
||||
raise NotImplementedError
|
||||
|
||||
def prefetch_key(self, dir_suffix: str) -> Hashable | None:
|
||||
"""Prefetch dedup identity; None = not prefetchable. Sources that
|
||||
could write one cache dir must return equal keys (workers must never
|
||||
share a dir); a coarser key only skips a prefetch."""
|
||||
return None
|
||||
|
||||
def is_cached(self, dir_suffix: str, salt: str = "", namespace: str = "") -> bool:
|
||||
"""Whether a completed fetch exists; only consulted when
|
||||
``prefetch_key()`` is not None, True is the safe default."""
|
||||
return True
|
||||
|
||||
def source_root(self, build_path: Path) -> Path:
|
||||
"""Directory holding the library's own files (manifest + sources).
|
||||
|
||||
@@ -127,6 +138,9 @@ class URLSource(Source):
|
||||
h.update(salt.encode())
|
||||
return base_dir / h.hexdigest()[:8] / dir_suffix
|
||||
|
||||
def prefetch_key(self, dir_suffix: str) -> Hashable | None:
|
||||
return self.url if self.size else None
|
||||
|
||||
def is_cached(self, dir_suffix: str, salt: str = "", namespace: str = "") -> bool:
|
||||
"""Whether a completed extraction already exists for this source."""
|
||||
return (
|
||||
@@ -177,14 +191,29 @@ class GitSource(Source):
|
||||
self.url = url
|
||||
self.ref = ref
|
||||
|
||||
def download(
|
||||
self, dir_suffix: str, force: bool = False, salt: str = "", namespace: str = ""
|
||||
) -> Path:
|
||||
@staticmethod
|
||||
def _domain(salt: str, namespace: str) -> str:
|
||||
domain = DOMAIN
|
||||
if namespace:
|
||||
domain = f"{domain}/{namespace}"
|
||||
if salt:
|
||||
domain = f"{domain}/{salt}"
|
||||
return domain
|
||||
|
||||
def prefetch_key(self, dir_suffix: str) -> Hashable | None:
|
||||
# The clone target dir is hash(url@ref)/<dir_suffix>
|
||||
return (self.url, self.ref, dir_suffix)
|
||||
|
||||
def is_cached(self, dir_suffix: str, salt: str = "", namespace: str = "") -> bool:
|
||||
"""Whether a completed clone already exists for this source."""
|
||||
return git.has_complete_clone(
|
||||
self.url, self.ref, self._domain(salt, namespace), Path(dir_suffix)
|
||||
)
|
||||
|
||||
def download(
|
||||
self, dir_suffix: str, force: bool = False, salt: str = "", namespace: str = ""
|
||||
) -> Path:
|
||||
domain = self._domain(salt, namespace)
|
||||
path, _ = git.clone_or_update(
|
||||
url=self.url,
|
||||
ref=self.ref,
|
||||
@@ -988,56 +1017,78 @@ def _fetch_source(
|
||||
)
|
||||
|
||||
|
||||
def _clone_source(
|
||||
component: ConvertedLibrary,
|
||||
salt: str,
|
||||
namespace: str,
|
||||
tracker: Callable[[int], None],
|
||||
) -> None:
|
||||
# No byte progress from git; one tick so a cancelled batch stops here
|
||||
tracker(0)
|
||||
component.source.download(
|
||||
component.get_sanitized_name(), salt=salt, namespace=namespace
|
||||
)
|
||||
|
||||
|
||||
def _prefetch_wave(
|
||||
wave: list[tuple[str, ConvertedLibrary]], salt: str, namespace: str
|
||||
) -> None:
|
||||
"""Best-effort parallel download of a wave's registry archives.
|
||||
"""Best-effort parallel fetch of a wave's registry archives and git clones.
|
||||
|
||||
The walk's own ``download()`` stays authoritative; duplicate URLs
|
||||
The walk's own ``download()`` stays authoritative; duplicate sources
|
||||
prefetch once so two threads never share a cache directory. Archives
|
||||
whose size the registry did not report are left to the sequential
|
||||
loop, whose per-file bars don't interleave. A node a sibling in the
|
||||
same wave supersedes has its archive fetched in vain (knowing better
|
||||
same wave supersedes has its source fetched in vain (knowing better
|
||||
would need the manifests being downloaded).
|
||||
"""
|
||||
try:
|
||||
components: list[ConvertedLibrary] = []
|
||||
seen: set[str] = set()
|
||||
archives: list[ConvertedLibrary] = []
|
||||
clones: list[ConvertedLibrary] = []
|
||||
seen: set[Hashable] = set()
|
||||
for _key, component in wave:
|
||||
source = component.source
|
||||
if not isinstance(source, URLSource) or not source.size:
|
||||
name = component.get_sanitized_name()
|
||||
dedup_key = source.prefetch_key(name)
|
||||
if dedup_key is None or dedup_key in seen:
|
||||
continue
|
||||
if source.url in seen:
|
||||
continue
|
||||
seen.add(source.url)
|
||||
seen.add(dedup_key)
|
||||
try:
|
||||
cached = source.is_cached(
|
||||
component.get_sanitized_name(), salt=salt, namespace=namespace
|
||||
)
|
||||
cached = source.is_cached(name, salt=salt, namespace=namespace)
|
||||
except OSError as err:
|
||||
# Best-effort, but visibly: a systematic probe failure makes
|
||||
# every warm build re-download every archive
|
||||
# every warm build re-fetch every source
|
||||
_LOGGER.warning("Cache probe for %s failed: %s", component.name, err)
|
||||
cached = False
|
||||
if cached:
|
||||
# A warm build must stay silent
|
||||
continue
|
||||
components.append(component)
|
||||
if not components:
|
||||
(archives if isinstance(source, URLSource) else clones).append(component)
|
||||
if not archives and not clones:
|
||||
return
|
||||
# Single-item waves (a dependency chain discovers one archive per
|
||||
# wave) go through the same runner: one download method, one bar
|
||||
_LOGGER.info(
|
||||
"Downloading %d library archive(s): %s",
|
||||
len(components),
|
||||
", ".join(c.name for c in components),
|
||||
)
|
||||
if archives:
|
||||
_LOGGER.info(
|
||||
"Downloading %d library archive(s): %s",
|
||||
len(archives),
|
||||
", ".join(c.name for c in archives),
|
||||
)
|
||||
if clones:
|
||||
_LOGGER.info(
|
||||
"Cloning %d library repo(s): %s",
|
||||
len(clones),
|
||||
", ".join(c.name for c in clones),
|
||||
)
|
||||
failures = run_batch_downloads(
|
||||
"Downloading libraries",
|
||||
[
|
||||
(c.name, c.source.size, partial(_fetch_source, c, salt, namespace))
|
||||
for c in components
|
||||
],
|
||||
for c in archives
|
||||
]
|
||||
# Size 0: clones share the worker pool without skewing the
|
||||
# byte bar, whose total stays the archive sum
|
||||
+ [(c.name, 0, partial(_clone_source, c, salt, namespace)) for c in clones],
|
||||
)
|
||||
# The sequential call below retries and raises the real error
|
||||
warn_prefetch_failures(
|
||||
|
||||
@@ -832,16 +832,16 @@ def _prefetch(build_dir: Path, env: str) -> None:
|
||||
for name, opts in p.packages.items()
|
||||
if not opts.get("optional")
|
||||
]
|
||||
# PIO's build engine installs outside the platform package list;
|
||||
# skipped when the platform lists it itself
|
||||
if not any(s.name == "tool-scons" for s in specs):
|
||||
specs.append(
|
||||
PackageSpec(
|
||||
owner="platformio",
|
||||
name="tool-scons",
|
||||
requirements=get_core_dependencies()["tool-scons"],
|
||||
)
|
||||
# PIO's build engine installs tool-scons by its own registry spec at build
|
||||
# start; a platform URL copy has no owner to match it, so prefetch that spec
|
||||
specs = [s for s in specs if s.name != "tool-scons"]
|
||||
specs.append(
|
||||
PackageSpec(
|
||||
owner="platformio",
|
||||
name="tool-scons",
|
||||
requirements=get_core_dependencies()["tool-scons"],
|
||||
)
|
||||
)
|
||||
lib_deps = config.get(f"env:{env}", "lib_deps", [])
|
||||
# pio run's storage dir for this env, with its compatibility
|
||||
# qualifiers: an unqualified library install could land a different
|
||||
|
||||
@@ -319,6 +319,154 @@ def lint_no_long_delays(fname, match):
|
||||
)
|
||||
|
||||
|
||||
# An if/else/for/while whose only body is an unbraced ESP_LOG*() call. When the build's compile-time
|
||||
# log level drops that macro, the body expands to nothing and the compiler warns (-Wempty-body).
|
||||
# clang-tidy's brace check does not catch these (ShortStatementLines allows short unbraced bodies), so
|
||||
# this fills that gap. Matched against comment/string-masked content, so commented-out or quoted code
|
||||
# is ignored. Both spellings are covered: core/log.h defines the uppercase ESP_LOG*() macros and
|
||||
# the lowercase esph_log_*() ones, and both expand to nothing below their log level.
|
||||
# 'for' allows ';' inside its parentheses (the classic C-style header); 'if'/'while' do not, so their
|
||||
# condition cannot run past the statement it guards. The 'for' header permits one level of nested
|
||||
# parens so it stays bounded to its own statement: without that, it can run past the loop body and
|
||||
# latch onto a later ')', mis-reporting the line and skipping the '#' preprocessor check below.
|
||||
ESP_LOG_NEEDS_BRACES_RE = re.compile(
|
||||
r"(?:\bif\s*\([^{};]*\)|\bwhile\s*\([^{};]*\)|\bfor\s*\((?:[^{}()]|\([^{}()]*\))*\)|\belse\b)"
|
||||
r"[ \t]*\n?[ \t]*(?:ESP_LOG[A-Z]*|esph_log_[a-z]+)\s*\(",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def _mask_cpp_comments_strings(s):
|
||||
"""Return s with // and /* */ comments and string/char/raw-string literals blanked to spaces
|
||||
(length and newlines preserved) so a regex only matches real code. Parentheses in real code are
|
||||
kept, so callers can still balance them on the masked text."""
|
||||
out = list(s)
|
||||
i = 0
|
||||
n = len(s)
|
||||
while i < n:
|
||||
c = s[i]
|
||||
# Raw string literal: an optional encoding prefix, then R"delim( ... )delim". The body may
|
||||
# contain quotes, //, /* and unbalanced parens, so it must be consumed as one unit.
|
||||
if c == "R" and i + 1 < n and s[i + 1] == '"':
|
||||
j = i + 2
|
||||
delim = ""
|
||||
while j < n and s[j] not in "( \t\r\n\\" and len(delim) < 16:
|
||||
delim += s[j]
|
||||
j += 1
|
||||
if j < n and s[j] == "(":
|
||||
closing = ")" + delim + '"'
|
||||
end = s.find(closing, j + 1)
|
||||
end = n if end == -1 else end + len(closing)
|
||||
for k in range(i, end):
|
||||
if s[k] != "\n":
|
||||
out[k] = " "
|
||||
i = end
|
||||
continue
|
||||
i += 1
|
||||
elif c == "/" and i + 1 < n and s[i + 1] == "/":
|
||||
while i < n and s[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
elif c == "/" and i + 1 < n and s[i + 1] == "*":
|
||||
out[i] = out[i + 1] = " "
|
||||
i += 2
|
||||
while i < n and not (s[i] == "*" and i + 1 < n and s[i + 1] == "/"):
|
||||
if s[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
if i < n:
|
||||
out[i] = " "
|
||||
if i + 1 < n:
|
||||
out[i + 1] = " "
|
||||
i += 2
|
||||
# A "'" after an alphanumeric or '_' is a C++ digit separator (1'000), not a literal opener.
|
||||
elif c == '"' or (
|
||||
c == "'" and not (i and (s[i - 1].isalnum() or s[i - 1] == "_"))
|
||||
):
|
||||
quote = c
|
||||
out[i] = " "
|
||||
i += 1
|
||||
while i < n:
|
||||
if s[i] == "\\":
|
||||
out[i] = " "
|
||||
if i + 1 < n:
|
||||
out[i + 1] = " "
|
||||
i += 2
|
||||
continue
|
||||
if s[i] == quote:
|
||||
out[i] = " "
|
||||
i += 1
|
||||
break
|
||||
if s[i] != "\n":
|
||||
out[i] = " "
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _log_statement_end(masked, open_paren):
|
||||
"""Index of the ';' ending the ESP_LOG call whose '(' is at open_paren, or None. Balanced on the
|
||||
masked text so quotes/comments inside the arguments do not confuse the paren count."""
|
||||
depth = 0
|
||||
i = open_paren
|
||||
n = len(masked)
|
||||
while i < n:
|
||||
ch = masked[i]
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
j = i + 1
|
||||
while j < n and masked[j] != ";":
|
||||
if not masked[j].isspace():
|
||||
return None
|
||||
j += 1
|
||||
return j if j < n else None
|
||||
i += 1
|
||||
return None
|
||||
|
||||
|
||||
@lint_content_check(include=cpp_include)
|
||||
def lint_esp_log_needs_braces(fname, content):
|
||||
# Cheap bailout: no log call means nothing to flag, and skips masking the file entirely.
|
||||
if "ESP_LOG" not in content and "esph_log_" not in content:
|
||||
return []
|
||||
masked = _mask_cpp_comments_strings(content)
|
||||
errors = []
|
||||
for match in ESP_LOG_NEEDS_BRACES_RE.finditer(masked):
|
||||
pos = match.start()
|
||||
line_start = content.rfind("\n", 0, pos) + 1
|
||||
# Skip preprocessor conditionals (#if/#else/#elif): not C++ control statements.
|
||||
if content[line_start:pos].lstrip().startswith("#"):
|
||||
continue
|
||||
# A '// NOLINT' may sit at the end of the log line (where the message says to put it) or on the
|
||||
# control-statement line, so scan the whole statement rather than only up to the ESP_LOG token.
|
||||
stmt_end = _log_statement_end(masked, match.end() - 1)
|
||||
nolint_end = (
|
||||
content.find("\n", stmt_end) if stmt_end is not None else match.end()
|
||||
)
|
||||
if nolint_end == -1:
|
||||
nolint_end = len(content)
|
||||
if "NOLINT" in content[pos:nolint_end]:
|
||||
continue
|
||||
snippet = content[pos : match.end()].replace("\n", " ").strip()
|
||||
errors.append(
|
||||
(
|
||||
content.count("\n", 0, pos) + 1,
|
||||
pos - line_start + 1,
|
||||
(
|
||||
f"{highlight(snippet)} - an if/else/for/while body that is a single log "
|
||||
"call must be wrapped in braces. When the log level compiles the macro out, the "
|
||||
"body becomes empty and the compiler warns (-Wempty-body). Add { } around the "
|
||||
"log call (or a '// NOLINT' comment if this is genuinely intended)."
|
||||
),
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
@lint_content_check(
|
||||
include=[
|
||||
"esphome/const.py",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
variant: esp32s3
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO7
|
||||
mosi_pin: GPIO9
|
||||
|
||||
display:
|
||||
- platform: epaper_spi
|
||||
id: epaper_display
|
||||
model: seeed-reterminal-e1001
|
||||
@@ -439,6 +439,23 @@ def test_enable_pin_multiple(
|
||||
assert all(pin["mode"]["output"] is True for pin in enable_pins)
|
||||
|
||||
|
||||
def test_uc8179_e1001_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test that the reTerminal E1001 model generates the UC8179 driver and init sequence."""
|
||||
main_cpp = generate_main(component_config_path("uc8179_e1001_test.yaml"))
|
||||
|
||||
# The model must instantiate the UC8179 driver class with the panel dimensions
|
||||
assert "epaper_spi::EPaperUC8179" in main_cpp
|
||||
assert re.search(r'"SEEED-RETERMINAL-E1001",\s*800,\s*480', main_cpp)
|
||||
|
||||
# The generated init sequence must contain the UC8179 resolution setting
|
||||
# for 800x480: command 0x61, 4 data bytes 0x03 0x20 0x01 0xE0
|
||||
# (rendered as decimal in the generated array)
|
||||
assert "97, 4, 3, 32, 1, 224" in main_cpp
|
||||
|
||||
|
||||
def test_enable_pin_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""The template platforms surface value-describing metadata on the main form.
|
||||
|
||||
Hardware platforms get sensible defaults for unit/device_class/etc., so those
|
||||
fields fall through to the editor's advanced disclosure. A ``template`` entity
|
||||
has no such defaults -- the user is expected to define them -- so the template
|
||||
platforms pass ``visibility=cv.Visibility.UI`` to promote them onto the form.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
def _markers(schema: cv.Schema) -> dict[str, object]:
|
||||
s = schema
|
||||
if hasattr(s, "validators"):
|
||||
# cv.All -> the schema is the first validator.
|
||||
s = s.validators[0]
|
||||
return {str(k): k for k in s.schema}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("platform", "fields"),
|
||||
[
|
||||
(
|
||||
"sensor",
|
||||
[
|
||||
"unit_of_measurement",
|
||||
"accuracy_decimals",
|
||||
"device_class",
|
||||
"state_class",
|
||||
"force_update",
|
||||
],
|
||||
),
|
||||
("binary_sensor", ["device_class"]),
|
||||
("switch", ["device_class"]),
|
||||
("cover", ["device_class"]),
|
||||
("button", ["device_class"]),
|
||||
("valve", ["device_class"]),
|
||||
("event", ["device_class"]),
|
||||
("text_sensor", ["device_class"]),
|
||||
("number", ["device_class", "unit_of_measurement"]),
|
||||
],
|
||||
)
|
||||
def test_template_metadata_is_ui(platform: str, fields: list[str]) -> None:
|
||||
mod = importlib.import_module(f"esphome.components.template.{platform}")
|
||||
markers = _markers(mod.CONFIG_SCHEMA)
|
||||
for field in fields:
|
||||
assert markers[field].visibility is cv.Visibility.UI, f"{platform}.{field}"
|
||||
|
||||
|
||||
def test_template_sensor_promotion_preserves_defaults() -> None:
|
||||
"""Promoting to UI must not drop the fields' defaults."""
|
||||
from esphome.components.template.sensor import CONFIG_SCHEMA
|
||||
|
||||
markers = _markers(CONFIG_SCHEMA)
|
||||
assert markers["accuracy_decimals"].default() == 1
|
||||
assert markers["force_update"].default() is False
|
||||
|
||||
|
||||
def test_hardware_platform_metadata_not_promoted() -> None:
|
||||
"""Without ``visibility=`` the builders leave metadata unset.
|
||||
|
||||
Unset markers fall through to the consumer's ``Optional`` default of
|
||||
advanced, so hardware platforms are unaffected by the template promotion.
|
||||
"""
|
||||
from esphome.components import binary_sensor, sensor
|
||||
|
||||
hw_sensor = _markers(sensor.sensor_schema(device_class="temperature"))
|
||||
assert hw_sensor["device_class"].visibility is None
|
||||
hw_bs = _markers(binary_sensor.binary_sensor_schema(device_class="motion"))
|
||||
assert hw_bs["device_class"].visibility is None
|
||||
@@ -255,3 +255,45 @@ display:
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 60, Color(255, 0, 0));
|
||||
|
||||
# Waveshare 7.5" V2 mono (800x480, UC8179 controller, EPD_7in5_V2)
|
||||
# full_update_every > 1 exercises the fast/partial refresh paths
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: waveshare-7.5in-v2
|
||||
full_update_every: 4
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
lambda: |-
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK);
|
||||
|
||||
# Seeed reTerminal E1001 - 7.5" mono e-paper (800x480, UC8179)
|
||||
# Pins overridden to avoid conflicts with the E1002 defaults above
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: seeed-reterminal-e1001
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Unit tests for the ESP_LOG-needs-braces lint rule in script/ci-custom.py.
|
||||
|
||||
The rule flags an if/else/for/while whose only body is an unbraced ESP_LOG*() call (which becomes an
|
||||
empty statement -- and a -Wempty-body warning -- once the log level compiles the macro out). These
|
||||
tests pin the comment/string/raw-string masker, the accepted control-statement shapes, and the
|
||||
NOLINT escape hatch at both placements a contributor would try.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
SCRIPT_DIR = (Path(__file__).parent / ".." / ".." / "script").resolve()
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
_spec = importlib.util.spec_from_file_location("ci_custom", SCRIPT_DIR / "ci-custom.py")
|
||||
ci_custom = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(ci_custom)
|
||||
|
||||
mask = ci_custom._mask_cpp_comments_strings
|
||||
|
||||
|
||||
def _lint(content: str) -> list:
|
||||
return ci_custom.lint_esp_log_needs_braces("test.cpp", content)
|
||||
|
||||
|
||||
# --- masker ---
|
||||
|
||||
|
||||
def test_mask_preserves_length_newlines_and_real_parens() -> None:
|
||||
src = 'foo("bar") + baz();\nqux();\n'
|
||||
masked = mask(src)
|
||||
assert len(masked) == len(src)
|
||||
assert masked.count("\n") == src.count("\n")
|
||||
assert masked.count("(") == src.count("(") # real parens survive for balancing
|
||||
|
||||
|
||||
def test_mask_blanks_line_and_block_comments() -> None:
|
||||
assert "ESP_LOGD" not in mask("a; // if (x) ESP_LOGD(t);\n")
|
||||
assert "ESP_LOGD" not in mask("a; /* if (x) ESP_LOGD(t); */ b;\n")
|
||||
|
||||
|
||||
def test_mask_blanks_string_literals() -> None:
|
||||
assert "if" not in mask('x = "if (y) ESP_LOGD";\n')
|
||||
|
||||
|
||||
def test_mask_handles_raw_string_without_desync() -> None:
|
||||
# A raw string full of quotes/parens must be consumed as one unit; code after it stays intact.
|
||||
src = 's.print(R"(<a href="x">)");\nreturn;\n'
|
||||
masked = mask(src)
|
||||
assert "href" not in masked
|
||||
assert "return;" in masked # not swallowed by a desynced string scan
|
||||
|
||||
|
||||
# --- rule: flags real violations ---
|
||||
|
||||
|
||||
def test_flags_unbraced_if_next_line() -> None:
|
||||
assert _lint("if (x)\n ESP_LOGD(t);\n")
|
||||
|
||||
|
||||
def test_flags_unbraced_same_line() -> None:
|
||||
assert _lint("if (x) ESP_LOGW(t);\n")
|
||||
|
||||
|
||||
def test_flags_c_style_for() -> None:
|
||||
assert _lint("for (int i = 0; i < n; i++)\n ESP_LOGD(t, i);\n")
|
||||
|
||||
|
||||
def test_flags_range_for_and_else() -> None:
|
||||
assert _lint("for (auto &x : v)\n ESP_LOGCONFIG(t);\n")
|
||||
assert _lint("else\n ESP_LOGE(t);\n")
|
||||
|
||||
|
||||
def test_flags_for_header_with_nested_call() -> None:
|
||||
assert _lint("for (auto it = v.begin(); it != v.end(); ++it)\n ESP_LOGD(t);\n")
|
||||
|
||||
|
||||
def test_for_header_does_not_reach_into_a_later_statement() -> None:
|
||||
# The 'for' header is bounded to its own statement, so it cannot swallow the loop body and latch
|
||||
# onto a later ')'. Without that, the '#if' line below is reported as an unbraced body even though
|
||||
# the '#' preprocessor check should skip it.
|
||||
assert not _lint(
|
||||
"for (int i = 0; i < n; i++)\n arr[i] = 0;\n#if defined(USE_X)\n ESP_LOGD(t);\n#endif\n"
|
||||
)
|
||||
|
||||
|
||||
def test_violation_after_a_for_loop_is_reported_at_its_own_line() -> None:
|
||||
errors = _lint(
|
||||
"for (int i = 0; i < n; i++)\n sum += a[i];\nif (verbose)\n ESP_LOGD(t, sum);\n"
|
||||
)
|
||||
lines = [line for line, _col, _msg in errors]
|
||||
assert lines == [3] # the 'if', not the 'for' on line 1
|
||||
|
||||
|
||||
def test_flags_lowercase_esph_log_family() -> None:
|
||||
# core/log.h defines esph_log_*() alongside ESP_LOG*(); both expand to nothing below their level.
|
||||
assert _lint('if (x)\n esph_log_config(t, "m");\n')
|
||||
assert _lint('if (err != ESP_OK)\n esph_log_e(t, "m");\n')
|
||||
|
||||
|
||||
def test_digit_separator_does_not_disable_the_rest_of_the_file() -> None:
|
||||
# A "'" digit separator must not be read as a char-literal opener, which blanked everything after.
|
||||
assert _lint("uint32_t x = 1'000;\nif (y)\n ESP_LOGD(t);\n")
|
||||
|
||||
|
||||
def test_mask_still_blanks_real_char_literals() -> None:
|
||||
assert "ESP_LOGD" not in mask("char c = '\"'; // if (x) ESP_LOGD(t);\n")
|
||||
assert not _lint("char sep = ';';\nif (x) {\n ESP_LOGD(t);\n}\n")
|
||||
|
||||
|
||||
def test_flags_multiline_log_body() -> None:
|
||||
assert _lint('if (x)\n ESP_LOGD(t, "%d %d",\n a, b);\n')
|
||||
|
||||
|
||||
def test_raw_string_before_violation_still_caught() -> None:
|
||||
# Regression for the masker desyncing on a raw string and disabling the check for the rest.
|
||||
assert _lint('s.print(R"(<a href="x">)");\nif (y)\n ESP_LOGD(t);\n')
|
||||
|
||||
|
||||
# --- rule: ignores non-violations ---
|
||||
|
||||
|
||||
def test_ignores_braced_body() -> None:
|
||||
assert not _lint("if (x) {\n ESP_LOGD(t);\n}\n")
|
||||
|
||||
|
||||
def test_ignores_commented_out_code() -> None:
|
||||
assert not _lint("// if (x) ESP_LOGD(t);\n")
|
||||
|
||||
|
||||
def test_ignores_preprocessor_else() -> None:
|
||||
assert not _lint("#else\n ESP_LOGCONFIG(t);\n#endif\n")
|
||||
|
||||
|
||||
def test_ignores_non_log_body() -> None:
|
||||
assert not _lint("if (x)\n return false;\n")
|
||||
|
||||
|
||||
# --- NOLINT escape hatch, both placements ---
|
||||
|
||||
|
||||
def test_nolint_at_end_of_log_line_suppresses() -> None:
|
||||
assert not _lint("if (x)\n ESP_LOGD(t); // NOLINT\n")
|
||||
|
||||
|
||||
def test_nolint_on_control_line_suppresses() -> None:
|
||||
assert not _lint("if (x) // NOLINT\n ESP_LOGD(t);\n")
|
||||
@@ -1394,6 +1394,35 @@ def test_entity_metadata_visibility_hints() -> None:
|
||||
assert web["web_server"].visibility is advanced
|
||||
|
||||
|
||||
def test_with_visibility_remarks_keys() -> None:
|
||||
"""``with_visibility`` re-marks the named keys, preserving each field's
|
||||
default and validator, without touching the other keys or the input schema.
|
||||
"""
|
||||
base = cv.Schema(
|
||||
{
|
||||
cv.Optional("a", default=7): cv.int_,
|
||||
cv.Optional("b", visibility=cv.Visibility.ADVANCED): cv.string,
|
||||
}
|
||||
)
|
||||
promoted = cv.with_visibility(base, cv.Visibility.UI, "a")
|
||||
|
||||
pm = {str(k): k for k in promoted.schema}
|
||||
assert pm["a"].visibility is cv.Visibility.UI # re-marked
|
||||
assert pm["a"].default() == 7 # default preserved
|
||||
assert pm["b"].visibility is cv.Visibility.ADVANCED # sibling untouched
|
||||
assert promoted({}) == {"a": 7} # validator/default still applied
|
||||
|
||||
# The input schema is left untouched (no shared-marker mutation).
|
||||
assert {str(k): k for k in base.schema}["a"].visibility is None
|
||||
|
||||
|
||||
def test_with_visibility_unknown_key_raises() -> None:
|
||||
"""A key not present in the schema is a typo — fail at build time."""
|
||||
base = cv.Schema({cv.Optional("a"): cv.int_})
|
||||
with pytest.raises(ValueError, match="not in schema"):
|
||||
cv.with_visibility(base, cv.Visibility.UI, "nope")
|
||||
|
||||
|
||||
def _wrap_str(value: str) -> ESPHomeDataBase:
|
||||
"""Wrap a raw string as an ESPHomeDataBase, mimicking a YAML-loaded value."""
|
||||
return make_data_base(value)
|
||||
|
||||
@@ -714,6 +714,25 @@ def test_run_git_command_without_git_dir_raises_error(
|
||||
git.run_git_command(["git", "clone", "https://invalid.url/repo.git"])
|
||||
|
||||
|
||||
def test_has_complete_clone(tmp_path: Path) -> None:
|
||||
"""The lock-free probe tracks the completion marker, subpath included."""
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
url = "https://github.com/test/repo"
|
||||
subpath = Path("lib")
|
||||
assert not git.has_complete_clone(url, "v1", "test_domain", subpath)
|
||||
|
||||
repo_dir = _compute_repo_dir(url, "v1", "test_domain") / subpath
|
||||
(repo_dir / ".git").mkdir(parents=True)
|
||||
# A directory without the marker is an incomplete clone
|
||||
assert not git.has_complete_clone(url, "v1", "test_domain", subpath)
|
||||
|
||||
_mark_clone_complete(repo_dir)
|
||||
assert git.has_complete_clone(url, "v1", "test_domain", subpath)
|
||||
# The ref is part of the cache key
|
||||
assert not git.has_complete_clone(url, "v2", "test_domain", subpath)
|
||||
|
||||
|
||||
def test_clone_or_update_with_never_refresh(
|
||||
tmp_path: Path, mock_run_git_command: Mock
|
||||
) -> None:
|
||||
|
||||
@@ -638,7 +638,7 @@ def test_prefetch_wave_downloads_registry_archives_in_parallel(
|
||||
setup_core, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Registry archives in one wave download concurrently, deduped by URL;
|
||||
git/local sources and failures are left to the sequential call."""
|
||||
local sources and failures are left to the sequential call."""
|
||||
calls: list[str] = []
|
||||
|
||||
def fake_download(
|
||||
@@ -658,7 +658,7 @@ def test_prefetch_wave_downloads_registry_archives_in_parallel(
|
||||
# into the same cache directory)
|
||||
("b2", ConvertedLibrary("b2", "1.0", URLSource("https://x/b.tar.gz", 1))),
|
||||
("c", ConvertedLibrary("c", "1.0", URLSource("https://x/boom.tar.gz", 1))),
|
||||
("g", ConvertedLibrary("g", "*", lib.GitSource("https://x/g.git", None))),
|
||||
("l", ConvertedLibrary("l", "*", LocalSource("/some/lib"))),
|
||||
]
|
||||
lib._prefetch_wave(wave, "", "idf")
|
||||
assert sorted(calls) == [
|
||||
@@ -670,6 +670,83 @@ def test_prefetch_wave_downloads_registry_archives_in_parallel(
|
||||
assert "Prefetch of c failed (retrying sequentially)" in caplog.text
|
||||
|
||||
|
||||
def test_prefetch_wave_clones_git_sources_in_parallel(
|
||||
setup_core, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Git sources join the same prefetch batch as the archives, deduped by
|
||||
clone target; a clone failure warns and is left to the sequential call."""
|
||||
caplog.set_level("INFO")
|
||||
calls: list[str] = []
|
||||
|
||||
def fake_clone(self, dir_suffix, force=False, salt="", namespace=""):
|
||||
calls.append(f"{self}/{dir_suffix}")
|
||||
if "boom" in self.url:
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(GitSource, "download", fake_clone)
|
||||
wave = [
|
||||
("a", ConvertedLibrary("a", "1.0", URLSource("https://x/a.tar.gz", 1))),
|
||||
("g", ConvertedLibrary("g", "*", GitSource("https://x/g.git", "v1"))),
|
||||
# Same url@ref and target dir must clone once
|
||||
("g2", ConvertedLibrary("g", "*", GitSource("https://x/g.git", "v1"))),
|
||||
("h", ConvertedLibrary("h", "*", GitSource("https://x/boom.git", None))),
|
||||
]
|
||||
monkeypatch.setattr(
|
||||
URLSource, "download", lambda self, dir_suffix, progress=None, **kw: None
|
||||
)
|
||||
lib._prefetch_wave(wave, "", "idf")
|
||||
assert sorted(calls) == ["https://x/boom.git/h", "https://x/g.git#v1/g"]
|
||||
assert "Cloning 2 library repo(s): g, h" in caplog.text
|
||||
assert "Prefetch of h failed (retrying sequentially)" in caplog.text
|
||||
|
||||
|
||||
def test_source_base_prefetch_defaults() -> None:
|
||||
"""The base Source is not prefetchable and reports cached (nothing to do)."""
|
||||
source = Source()
|
||||
assert source.prefetch_key("x") is None
|
||||
assert source.is_cached("x") is True
|
||||
|
||||
|
||||
def test_prefetch_wave_single_clone_uses_the_batch(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A wave with only git sources still clones through the batch runner."""
|
||||
caplog.set_level("INFO")
|
||||
calls: list[str] = []
|
||||
monkeypatch.setattr(GitSource, "is_cached", lambda self, *a, **kw: False)
|
||||
monkeypatch.setattr(
|
||||
GitSource,
|
||||
"download",
|
||||
lambda self, dir_suffix, force=False, salt="", namespace="": calls.append(
|
||||
self.url
|
||||
),
|
||||
)
|
||||
lib._prefetch_wave(
|
||||
[("g", ConvertedLibrary("g", "*", GitSource("https://x/g.git", None)))],
|
||||
"",
|
||||
"idf",
|
||||
)
|
||||
assert calls == ["https://x/g.git"]
|
||||
assert "Cloning 1 library repo(s): g" in caplog.text
|
||||
assert "Downloading" not in caplog.text
|
||||
|
||||
|
||||
def test_prefetch_wave_warm_git_cache_is_silent(
|
||||
setup_core, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""An already-complete clone is neither re-fetched nor announced."""
|
||||
caplog.set_level("INFO")
|
||||
monkeypatch.setattr(
|
||||
GitSource,
|
||||
"download",
|
||||
lambda self, dir_suffix, **kw: (_ for _ in ()).throw(AssertionError("cloned")),
|
||||
)
|
||||
monkeypatch.setattr(GitSource, "is_cached", lambda self, *a, **kw: True)
|
||||
wave = [("g", ConvertedLibrary("g", "*", GitSource("https://x/g.git", None)))]
|
||||
lib._prefetch_wave(wave, "", "idf")
|
||||
assert "Cloning" not in caplog.text
|
||||
|
||||
|
||||
def test_prefetch_wave_unknown_size_left_to_sequential(
|
||||
setup_core, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
@@ -13,6 +13,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from filelock import Timeout
|
||||
from platformio.dependencies import get_core_dependencies
|
||||
from platformio.package.manager._install import PackageManagerInstallMixin
|
||||
from platformio.package.manager.base import BasePackageManager
|
||||
from platformio.package.manager.library import LibraryPackageManager
|
||||
@@ -1728,30 +1729,31 @@ def test_preinstall_unlocks_even_when_pool_fails(tmp_path: Path) -> None:
|
||||
m.unlock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_prefetch_skips_duplicate_tool_scons(tmp_path: Path) -> None:
|
||||
"""A platform that lists tool-scons itself does not get it appended."""
|
||||
def test_prefetch_replaces_platform_tool_scons_with_core_spec(tmp_path: Path) -> None:
|
||||
"""A platform's own tool-scons spec gives way to the core's registry spec."""
|
||||
_write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n")
|
||||
fake_platform = MagicMock()
|
||||
fake_platform.packages = {"tool-scons": {"optional": False}}
|
||||
fake_platform.get_package_spec.side_effect = lambda name: _FakeSpec(
|
||||
uri=None, name=name
|
||||
uri="https://x/scons.zip", name=name, owner=None
|
||||
)
|
||||
config = _fake_config(tmp_path, {"platform": "fake/p@1"})
|
||||
modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config)
|
||||
batches: list[list[str]] = []
|
||||
batches: list[list] = []
|
||||
with (
|
||||
patch.dict("sys.modules", modules),
|
||||
patch.object(
|
||||
pf,
|
||||
"_registry_jobs",
|
||||
side_effect=lambda mgr, specs, seen: (
|
||||
batches.append([s.name for s in specs]) or ([], 0, [])
|
||||
batches.append(list(specs)) or ([], 0, [])
|
||||
),
|
||||
),
|
||||
patch.object(pf, "_uri_jobs", return_value=([], 0, [])),
|
||||
):
|
||||
pf._prefetch(tmp_path, "testenv")
|
||||
assert batches[0] == ["tool-scons"]
|
||||
(spec,) = batches[0]
|
||||
assert (spec.name, spec.owner, spec.uri) == ("tool-scons", "platformio", None)
|
||||
|
||||
|
||||
def test_platformio_private_api_contract() -> None:
|
||||
@@ -1784,6 +1786,8 @@ def test_platformio_private_api_contract() -> None:
|
||||
assert callable(getattr(BasePackageManager, name))
|
||||
# The dependency wave mirrors install_dependency's builtin skip
|
||||
assert callable(LibraryPackageManager.is_builtin_lib)
|
||||
# The prefetch keys tool-scons on this core dependency
|
||||
assert "tool-scons" in get_core_dependencies()
|
||||
# The pre-install passes these positionally / by keyword
|
||||
assert "compatibility" in inspect.signature(BasePackageManager.__init__).parameters
|
||||
lib_params = inspect.signature(LibraryPackageManager.__init__).parameters
|
||||
|
||||
Reference in New Issue
Block a user