1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 16:51:52 +00:00

Compare commits

...

24 Commits

Author SHA1 Message Date
J. Nick Koston
e529545118 cover 2025-12-01 11:33:10 -06:00
J. Nick Koston
8a0b412e1f tweaks 2025-12-01 11:24:49 -06:00
Jonathan Swoboda
a157b4431e Add tests for read-only file handling
Add integration tests to verify clean_build and clean_all handle
read-only files correctly (e.g., git pack files on Windows).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 07:51:10 -05:00
Jonathan Swoboda
588cea939f Cleanup 2025-11-30 23:10:09 -05:00
Jonathan Swoboda
66c6ce607a Fix clean all on windows 2025-11-30 23:02:29 -05:00
Jonathan Swoboda
a7a5a0b9a2 [esp32] Improve IDF component support (#12127) 2025-11-26 22:46:17 -05:00
Jonathan Swoboda
9c85ec9182 [esp32] Fix hosted update when there is no wifi (#12123) 2025-11-26 20:01:35 -05:00
Jesse Hills
23e58c1c7b [inkplate] Ignore strapping pin warnings on default pins (#12110) 2025-11-26 17:08:40 -06:00
Clyde Stubbs
b3955cd151 [epaper_spi] Add SSD1677 and Waveshare 4.26 (#11887)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 17:07:51 -06:00
Clyde Stubbs
927d3715c1 [lvgl] Allow setting text directly on a button (#11964)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 17:06:40 -06:00
Clyde Stubbs
a2d9941c62 [lvgl] Add option to sync updates with display (#11896)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 17:06:32 -06:00
Clyde Stubbs
caaa08d678 [core] Fix for missing arguments to shared_lambda (#12115) 2025-11-26 17:05:45 -06:00
Jon Oberheide
eb970cf44e make thermostat humidification_action public (#12132) 2025-11-26 16:56:22 -06:00
Pawelo
083886c4b0 [prometheus] Avoid generating unused light color metrics to reduce memory usage on ESP8266 (#9530)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 18:06:51 +00:00
Javier Peletier
12a51ff047 [packages] Fix package schema validation (#12116)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 11:00:44 -06:00
J. Nick Koston
b328758634 Revert "[core] Deduplicate identical stateless lambdas to reduce flash usage" (#12117) 2025-11-26 10:53:44 -06:00
Clyde Stubbs
1207b9e995 [lvgl] Automatically pad rows and columns (#11879)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 01:53:51 +00:00
Clyde Stubbs
e071380532 [lvgl] Add missing obj scroll properties (#11901)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 01:49:47 +00:00
Clyde Stubbs
f071b6232a [lvgl] Fix position of errors in widget config (#12111)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 01:47:27 +00:00
J. Nick Koston
d443dbbf34 [lvgl] Fix lambda return types for coord and font validators (#12113) 2025-11-25 19:42:09 -06:00
J. Nick Koston
03a8ef71ff [esp32_ble_client] Replace std::string with char[18] for BLE address storage (#12070) 2025-11-25 18:37:49 -06:00
J. Nick Koston
bda17180df [core] Deduplicate identical stateless lambdas to reduce flash usage (#11918) 2025-11-26 12:48:08 +13:00
J. Nick Koston
ffae3501ab [core] Replace seq<>/gens<> with std::index_sequence for code clarity (#11921) 2025-11-26 12:44:50 +13:00
Jesse Hills
50bdcdee0c Add developer-breaking-change labelling (#12095) 2025-11-26 12:39:41 +13:00
58 changed files with 1279 additions and 448 deletions

View File

@@ -7,6 +7,7 @@
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Developer breaking change (an API change that could break external components)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other

View File

@@ -68,6 +68,7 @@ jobs:
'bugfix',
'new-feature',
'breaking-change',
'developer-breaking-change',
'code-quality'
];
@@ -367,6 +368,7 @@ jobs:
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
];

View File

@@ -56,13 +56,13 @@ bool Alpha3::is_current_response_type_(const uint8_t *response_type) {
void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
if (this->response_offset_ >= this->response_length_) {
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str());
if (length < GENI_RESPONSE_HEADER_LENGTH) {
ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str());
return;
}
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str(),
response[0], response[1], response[2], response[3], response[4]);
return;
}
@@ -77,11 +77,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
};
if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str());
extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F);
extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
} else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str());
extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F);
@@ -100,7 +100,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
if (param->open.status == ESP_GATT_OK) {
this->response_offset_ = 0;
this->response_length_ = 0;
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str());
}
break;
}
@@ -132,7 +132,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
if (chr == nullptr) {
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str());
break;
}
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
@@ -164,12 +164,12 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
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)
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
void Alpha3::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str());
return;
}

View File

@@ -44,11 +44,9 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
if (chr == nullptr) {
if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) {
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.",
this->parent_->address_str().c_str());
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->parent_->address_str());
} else {
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?",
this->parent_->address_str().c_str());
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str());
}
break;
}
@@ -82,8 +80,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
this->char_handle_, packet->length, packet->data,
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
this->current_sensor_ = 0;
@@ -97,7 +94,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
void Am43::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str());
return;
}
if (this->current_sensor_ == 0) {
@@ -107,7 +104,7 @@ void Am43::update() {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
this->current_sensor_++;

View File

@@ -42,7 +42,7 @@ void Anova::control(const ClimateCall &call) {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
if (call.get_target_temperature().has_value()) {
@@ -51,7 +51,7 @@ void Anova::control(const ClimateCall &call) {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
}
@@ -124,8 +124,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
}
@@ -150,7 +149,7 @@ void Anova::update() {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
this->current_request_++;
}

View File

@@ -51,13 +51,14 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
return true;
}
protected:
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
template<typename ArgsContainer, size_t... S>
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}
@@ -95,13 +96,14 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
return true;
}
protected:
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
template<typename ArgsContainer, size_t... S>
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}

View File

@@ -198,7 +198,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
}
this->node_state = espbt::ClientState::ESTABLISHED;
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
ble_client_->address_str().c_str());
ble_client_->address_str());
break;
}
default:

View File

@@ -39,7 +39,7 @@ void BLEClient::set_enabled(bool enabled) {
return;
this->enabled = enabled;
if (!enabled) {
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str());
this->disconnect();
}
}

View File

@@ -14,7 +14,7 @@ void BLEBinaryOutput::dump_config() {
" MAC address : %s\n"
" Service UUID : %s\n"
" Characteristic UUID: %s",
this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(),
this->parent_->address_str(), this->service_uuid_.to_string().c_str(),
this->char_uuid_.to_string().c_str());
LOG_BINARY_OUTPUT(this);
}
@@ -44,7 +44,7 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
}
this->node_state = espbt::ClientState::ESTABLISHED;
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
this->parent()->address_str().c_str());
this->parent()->address_str());
this->node_state = espbt::ClientState::ESTABLISHED;
break;
}

View File

@@ -19,7 +19,7 @@ void BLEClientRSSISensor::loop() {
void BLEClientRSSISensor::dump_config() {
LOG_SENSOR("", "BLE Client RSSI Sensor", this);
ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str());
ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str());
LOG_UPDATE_INTERVAL(this);
}
@@ -69,10 +69,10 @@ void BLEClientRSSISensor::update() {
this->get_rssi_();
}
void BLEClientRSSISensor::get_rssi_() {
ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str());
ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str());
auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda());
if (status != ESP_OK) {
ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status);
ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str(), status);
this->status_set_warning();
this->publish_state(NAN);
}

View File

@@ -25,7 +25,7 @@ void BLESensor::dump_config() {
" Characteristic UUID: %s\n"
" Descriptor UUID : %s\n"
" Notifications : %s",
this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(),
this->parent()->address_str(), this->service_uuid_.to_string().c_str(),
this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_));
LOG_UPDATE_INTERVAL(this);
}

View File

@@ -28,7 +28,7 @@ void BLETextSensor::dump_config() {
" Characteristic UUID: %s\n"
" Descriptor UUID : %s\n"
" Notifications : %s",
this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(),
this->parent()->address_str(), this->service_uuid_.to_string().c_str(),
this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_));
LOG_UPDATE_INTERVAL(this);
}

View File

@@ -196,8 +196,8 @@ void BluetoothConnection::send_service_for_discovery_() {
if (service_status != ESP_GATT_OK || service_count == 0) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
this->connection_index_, this->address_str().c_str(),
service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_);
this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing",
service_status, service_count, this->send_service_);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
@@ -312,13 +312,13 @@ void BluetoothConnection::send_service_for_discovery_() {
if (resp.services.size() > 1) {
resp.services.pop_back();
ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch",
this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size,
this->connection_index_, this->address_str(), this->send_service_, current_size, service_size,
MAX_PACKET_SIZE);
// Don't increment send_service_ - we'll retry this service in next batch
} else {
// This single service is too large, but we have to send it anyway
ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_,
this->address_str().c_str(), this->send_service_, service_size);
this->address_str(), this->send_service_, service_size);
// Increment so we don't get stuck
this->send_service_++;
}
@@ -337,21 +337,20 @@ void BluetoothConnection::send_service_for_discovery_() {
}
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation,
status);
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status);
}
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err);
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err);
}
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(),
action, type);
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action,
type);
}
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(),
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(),
operation, handle, status);
}
@@ -372,14 +371,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
case ESP_GATTC_DISCONNECT_EVT: {
// Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
// This prevents race condition where we mark slot as free before controller cleanup is complete
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(),
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_,
param->disconnect.reason);
// Send disconnection notification but don't free the slot yet
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
break;
}
case ESP_GATTC_CLOSE_EVT: {
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(),
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_,
param->close.reason);
// Now the GATT connection is fully closed and controller resources are freed
// Safe to mark the connection slot as available
@@ -463,7 +462,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
break;
}
case ESP_GATTC_NOTIFY_EVT: {
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(),
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_,
param->notify.handle);
api::BluetoothGATTNotifyDataResponse resp;
resp.address = this->address_;
@@ -502,8 +501,7 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
@@ -515,8 +513,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8
this->log_gatt_not_connected_("write", "characteristic");
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
@@ -532,8 +529,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
this->log_gatt_not_connected_("read", "descriptor");
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
@@ -544,8 +540,7 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *
this->log_gatt_not_connected_("write", "descriptor");
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
@@ -564,13 +559,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
if (enable) {
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle);
this->address_str_, handle);
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
}
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle);
this->address_str_, handle);
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
}

View File

@@ -47,12 +47,11 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) {
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(),
connection->address_str().c_str(), espbt::client_state_to_string(state));
connection->address_str(), espbt::client_state_to_string(state));
}
void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) {
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(),
message);
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message);
}
void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
@@ -186,7 +185,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
}
if (!msg.has_address_type) {
ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(),
connection->address_str().c_str());
connection->address_str());
this->send_device_connection(msg.address, false);
return;
}
@@ -199,7 +198,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
} else if (connection->state() == espbt::ClientState::CONNECTING) {
if (connection->disconnect_pending()) {
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
connection->get_connection_index(), connection->address_str().c_str());
connection->get_connection_index(), connection->address_str());
connection->cancel_pending_disconnect();
return;
}
@@ -339,7 +338,7 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
return;
}
if (!connection->service_count_) {
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str());
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str());
this->send_gatt_services_done(msg.address);
return;
}

View File

@@ -7,7 +7,6 @@
namespace esphome {
namespace display {
static const char *const TAG = "display";
const Color COLOR_OFF(0, 0, 0, 0);
@@ -16,6 +15,7 @@ const Color COLOR_ON(255, 255, 255, 255);
void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); }
void Display::clear() { this->fill(COLOR_OFF); }
void Display::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; }
void HOT Display::line(int x1, int y1, int x2, int y2, Color color) {
const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
@@ -91,23 +91,27 @@ void HOT Display::horizontal_line(int x, int y, int width, Color color) {
for (int i = x; i < x + width; i++)
this->draw_pixel_at(i, y, color);
}
void HOT Display::vertical_line(int x, int y, int height, Color color) {
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
for (int i = y; i < y + height; i++)
this->draw_pixel_at(x, i, color);
}
void Display::rectangle(int x1, int y1, int width, int height, Color color) {
this->horizontal_line(x1, y1, width, color);
this->horizontal_line(x1, y1 + height - 1, width, color);
this->vertical_line(x1, y1, height, color);
this->vertical_line(x1 + width - 1, y1, height, color);
}
void Display::filled_rectangle(int x1, int y1, int width, int height, Color color) {
// Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses.
for (int i = y1; i < y1 + height; i++) {
this->horizontal_line(x1, i, width, color);
}
}
void HOT Display::circle(int center_x, int center_xy, int radius, Color color) {
int dx = -radius;
int dy = 0;
@@ -131,6 +135,7 @@ void HOT Display::circle(int center_x, int center_xy, int radius, Color color) {
}
} while (dx <= 0);
}
void Display::filled_circle(int center_x, int center_y, int radius, Color color) {
int dx = -int32_t(radius);
int dy = 0;
@@ -157,6 +162,7 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color)
}
} while (dx <= 0);
}
void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, Color color) {
int rmax = radius1 > radius2 ? radius1 : radius2;
int rmin = radius1 < radius2 ? radius1 : radius2;
@@ -213,6 +219,7 @@ void Display::filled_ring(int center_x, int center_y, int radius1, int radius2,
}
} while (dxmax <= 0);
}
void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, int progress, Color color) {
int rmax = radius1 > radius2 ? radius1 : radius2;
int rmin = radius1 < radius2 ? radius1 : radius2;
@@ -228,7 +235,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
// outer dots
this->draw_pixel_at(center_x + dxmax, center_y - dymax, color);
this->draw_pixel_at(center_x - dxmax, center_y - dymax, color);
if (dymin < rmin) { // side parts
if (dymin < rmin) {
// side parts
int lhline_width = -(dxmax - dxmin) + 1;
if (progress >= 50) {
if (float(dymax) < float(-dxmax) * tan_a) {
@@ -239,7 +247,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); // left
if (!dymax)
this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border
if (upd_dxmax > -dxmin) { // right
if (upd_dxmax > -dxmin) {
// right
int rhline_width = (upd_dxmax + dxmin) + 1;
this->horizontal_line(center_x - dxmin, center_y - dymax,
rhline_width > lhline_width ? lhline_width : rhline_width, color);
@@ -256,7 +265,8 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
if (lhline_width > 0)
this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color);
}
} else { // top part
} else {
// top part
int hline_width = 2 * (-dxmax) + 1;
if (progress >= 50) {
if (dymax < float(-dxmax) * tan_a) {
@@ -300,11 +310,13 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2,
}
} while (dxmax <= 0);
}
void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
this->line(x1, y1, x2, y2, color);
this->line(x1, y1, x3, y3, color);
this->line(x2, y2, x3, y3, color);
}
void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) {
if (*y1 > *y2) {
int x_temp = *x1, y_temp = *y1;
@@ -322,6 +334,7 @@ void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int
*x3 = x_temp, *y3 = y_temp;
}
}
void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
// y2 must be equal to y3 (same horizontal line)
@@ -333,7 +346,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3,
int s1_dy = abs(y2 - y1);
int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1;
int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1;
if (s1_dy > s1_dx) { // swap values
if (s1_dy > s1_dx) {
// swap values
int tmp = s1_dx;
s1_dx = s1_dy;
s1_dy = tmp;
@@ -349,7 +363,8 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3,
int s2_dy = abs(y3 - y1);
int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1;
int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1;
if (s2_dy > s2_dx) { // swap values
if (s2_dy > s2_dx) {
// swap values
int tmp = s2_dx;
s2_dx = s2_dy;
s2_dy = tmp;
@@ -402,20 +417,25 @@ void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3,
}
}
}
void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
// Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point
this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3);
if (y2 == y3) { // Check for special case of a bottom-flat triangle
if (y2 == y3) {
// Check for special case of a bottom-flat triangle
this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color);
} else if (y1 == y2) { // Check for special case of a top-flat triangle
} else if (y1 == y2) {
// Check for special case of a top-flat triangle
this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color);
} else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle
} else {
// General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle
int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2;
this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color);
this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color);
}
}
void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y,
int radius, int edges, RegularPolygonVariation variation,
float rotation_degrees) {
@@ -447,7 +467,8 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo
int current_vertex_x, current_vertex_y;
get_regular_polygon_vertex(current_vertex_id, &current_vertex_x, &current_vertex_y, x, y, radius, edges,
variation, rotation_degrees);
if (current_vertex_id > 0) { // Start drawing after the 2nd vertex coordinates has been calculated
if (current_vertex_id > 0) {
// Start drawing after the 2nd vertex coordinates has been calculated
if (drawing == DRAWING_FILLED) {
this->filled_triangle(x, y, previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color);
} else if (drawing == DRAWING_OUTLINE) {
@@ -459,21 +480,26 @@ void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPo
}
}
}
void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color,
RegularPolygonDrawing drawing) {
regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, drawing);
}
void HOT Display::regular_polygon(int x, int y, int radius, int edges, Color color, RegularPolygonDrawing drawing) {
regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, drawing);
}
void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation,
float rotation_degrees, Color color) {
regular_polygon(x, y, radius, edges, variation, rotation_degrees, color, DRAWING_FILLED);
}
void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation,
Color color) {
regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, DRAWING_FILLED);
}
void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color color) {
regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED);
}
@@ -584,15 +610,19 @@ void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, Te
break;
}
}
void Display::print(int x, int y, BaseFont *font, Color color, const char *text, Color background) {
this->print(x, y, font, color, TextAlign::TOP_LEFT, text, background);
}
void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) {
this->print(x, y, font, COLOR_ON, align, text);
}
void Display::print(int x, int y, BaseFont *font, const char *text) {
this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text);
}
void Display::printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
...) {
va_list arg;
@@ -600,31 +630,37 @@ void Display::printf(int x, int y, BaseFont *font, Color color, Color background
this->vprintf_(x, y, font, color, background, align, format, arg);
va_end(arg);
}
void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, color, COLOR_OFF, align, format, arg);
va_end(arg);
}
void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, arg);
va_end(arg);
}
void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, align, format, arg);
va_end(arg);
}
void Display::printf(int x, int y, BaseFont *font, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, arg);
va_end(arg);
}
void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; }
void Display::set_pages(std::vector<DisplayPage *> pages) {
for (auto *page : pages)
page->set_parent(this);
@@ -637,6 +673,7 @@ void Display::set_pages(std::vector<DisplayPage *> pages) {
pages[pages.size() - 1]->set_next(pages[0]);
this->show_page(pages[0]);
}
void Display::show_page(DisplayPage *page) {
this->previous_page_ = this->page_;
this->page_ = page;
@@ -645,8 +682,10 @@ void Display::show_page(DisplayPage *page) {
t->process(this->previous_page_, this->page_);
}
}
void Display::show_next_page() { this->page_->show_next(); }
void Display::show_prev_page() { this->page_->show_prev(); }
void Display::do_update_() {
if (this->auto_clear_enabled_) {
this->clear();
@@ -660,10 +699,12 @@ void Display::do_update_() {
}
this->clear_clipping_();
}
void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
this->trigger(from, to);
}
void Display::strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
ESPTime time) {
char buffer[64];
@@ -671,15 +712,19 @@ void Display::strftime(int x, int y, BaseFont *font, Color color, Color backgrou
if (ret > 0)
this->print(x, y, font, color, align, buffer, background);
}
void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) {
this->strftime(x, y, font, color, COLOR_OFF, align, format, time);
}
void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
}
void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, COLOR_OFF, align, format, time);
}
void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
}
@@ -691,6 +736,7 @@ void Display::start_clipping(Rect rect) {
}
this->clipping_rectangle_.push_back(rect);
}
void Display::end_clipping() {
if (this->clipping_rectangle_.empty()) {
ESP_LOGE(TAG, "clear: Clipping is not set.");
@@ -698,6 +744,7 @@ void Display::end_clipping() {
this->clipping_rectangle_.pop_back();
}
}
void Display::extend_clipping(Rect add_rect) {
if (this->clipping_rectangle_.empty()) {
ESP_LOGE(TAG, "add: Clipping is not set.");
@@ -705,6 +752,7 @@ void Display::extend_clipping(Rect add_rect) {
this->clipping_rectangle_.back().extend(add_rect);
}
}
void Display::shrink_clipping(Rect add_rect) {
if (this->clipping_rectangle_.empty()) {
ESP_LOGE(TAG, "add: Clipping is not set.");
@@ -712,6 +760,7 @@ void Display::shrink_clipping(Rect add_rect) {
this->clipping_rectangle_.back().shrink(add_rect);
}
}
Rect Display::get_clipping() const {
if (this->clipping_rectangle_.empty()) {
return Rect();
@@ -719,7 +768,9 @@ Rect Display::get_clipping() const {
return this->clipping_rectangle_.back();
}
}
void Display::clear_clipping_() { this->clipping_rectangle_.clear(); }
bool Display::clip(int x, int y) {
if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height())
return false;
@@ -727,6 +778,7 @@ bool Display::clip(int x, int y) {
return false;
return true;
}
bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) {
min_x = std::max(x, 0);
max_x = std::min(x + w, this->get_width());
@@ -742,6 +794,7 @@ bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) {
return min_x < max_x;
}
bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) {
min_y = std::max(y, 0);
max_y = std::min(y + h, this->get_height());
@@ -766,15 +819,15 @@ void Display::test_card() {
int w = get_width(), h = get_height(), image_w, image_h;
this->clear();
this->show_test_card_ = false;
image_w = std::min(w - 20, 310);
image_h = std::min(h - 20, 255);
int shift_x = (w - image_w) / 2;
int shift_y = (h - image_h) / 2;
int line_w = (image_w - 6) / 6;
int image_c = image_w / 2;
if (this->get_display_type() == DISPLAY_TYPE_COLOR) {
Color r(255, 0, 0), g(0, 255, 0), b(0, 0, 255);
image_w = std::min(w - 20, 310);
image_h = std::min(h - 20, 255);
int shift_x = (w - image_w) / 2;
int shift_y = (h - image_h) / 2;
int line_w = (image_w - 6) / 6;
int image_c = image_w / 2;
for (auto i = 0; i != image_h; i++) {
int c = esp_scale(i, image_h);
this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c));
@@ -786,26 +839,26 @@ void Display::test_card() {
this->horizontal_line(shift_x + image_w - (line_w * 2), shift_y + i, line_w, b.fade_to_white(c));
this->horizontal_line(shift_x + image_w - line_w, shift_y + i, line_w, b.fade_to_black(c));
}
this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0));
}
this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0));
uint16_t shift_r = shift_x + line_w - (8 * 3);
uint16_t shift_g = shift_x + image_c - (8 * 3);
uint16_t shift_b = shift_x + image_w - line_w - (8 * 3);
shift_y = h / 2 - (8 * 3);
for (auto i = 0; i < 8; i++) {
uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]);
uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]);
uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]);
for (auto k = 0; k < 8; k++) {
if ((ftr & (1 << k)) != 0) {
this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF);
}
if ((ftg & (1 << k)) != 0) {
this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF);
}
if ((ftb & (1 << k)) != 0) {
this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF);
}
uint16_t shift_r = shift_x + line_w - (8 * 3);
uint16_t shift_g = shift_x + image_c - (8 * 3);
uint16_t shift_b = shift_x + image_w - line_w - (8 * 3);
shift_y = h / 2 - (8 * 3);
for (auto i = 0; i < 8; i++) {
uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]);
uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]);
uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]);
for (auto k = 0; k < 8; k++) {
if ((ftr & (1 << k)) != 0) {
this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF);
}
if ((ftg & (1 << k)) != 0) {
this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF);
}
if ((ftb & (1 << k)) != 0) {
this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF);
}
}
}
@@ -818,7 +871,9 @@ void Display::test_card() {
}
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
void DisplayPage::show() { this->parent_->show_page(this); }
void DisplayPage::show_next() {
if (this->next_ == nullptr) {
ESP_LOGE(TAG, "no next page");
@@ -826,6 +881,7 @@ void DisplayPage::show_next() {
}
this->next_->show();
}
void DisplayPage::show_prev() {
if (this->prev_ == nullptr) {
ESP_LOGE(TAG, "no previous page");
@@ -833,6 +889,7 @@ void DisplayPage::show_prev() {
}
this->prev_->show();
}
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }
@@ -868,6 +925,5 @@ const LogString *text_align_to_string(TextAlign textalign) {
return LOG_STR("UNKNOWN");
}
}
} // namespace display
} // namespace esphome

View File

@@ -4,8 +4,10 @@ import pkgutil
from esphome import core, pins
import esphome.codegen as cg
from esphome.components import display, spi
from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation
from esphome.components.mipi import flatten_sequence, map_sequence
import esphome.config_validation as cv
from esphome.config_validation import update_interval
from esphome.const import (
CONF_BUSY_PIN,
CONF_CS_PIN,
@@ -13,15 +15,25 @@ from esphome.const import (
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_FULL_UPDATE_EVERY,
CONF_HEIGHT,
CONF_ID,
CONF_INIT_SEQUENCE,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_PAGES,
CONF_RESET_DURATION,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_UPDATE_INTERVAL,
CONF_WIDTH,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
from . import models
@@ -32,8 +44,9 @@ CONF_INIT_SEQUENCE_ID = "init_sequence_id"
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
EPaperBase = epaper_spi_ns.class_(
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.Display
)
Transform = epaper_spi_ns.enum("Transform")
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
@@ -52,6 +65,8 @@ DIMENSION_SCHEMA = cv.Schema(
}
)
TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
@@ -73,7 +88,18 @@ def model_schema(config):
)
.extend(
{
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
cv.Optional(
CONF_UPDATE_INTERVAL, default=cv.UNDEFINED
): update_interval,
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
}
),
cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255),
model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema,
cv.GenerateID(): cv.declare_id(class_name),
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
@@ -111,9 +137,29 @@ def customise_schema(config):
CONFIG_SCHEMA = customise_schema
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"epaper_spi", require_miso=False, require_mosi=True
)
def _final_validate(config):
spi.final_validate_device_schema(
"epaper_spi", require_miso=False, require_mosi=True
)(config)
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
config[CONF_UPDATE_INTERVAL] = core.TimePeriod(
seconds=60
).total_milliseconds
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
@@ -137,7 +183,9 @@ async def to_code(config):
init_sequence_length,
)
await display.register_display(var, config)
# Rotation is handled by setting the transform
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
await display.register_display(var, display_config)
await spi.register_spi_device(var, config)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
@@ -148,11 +196,35 @@ async def to_code(config):
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
if CONF_RESET_PIN in config:
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
if reset_pin := config.get(CONF_RESET_PIN):
reset = await cg.gpio_pin_expression(reset_pin)
cg.add(var.set_reset_pin(reset))
if CONF_BUSY_PIN in config:
busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
if busy_pin := config.get(CONF_BUSY_PIN):
busy = await cg.gpio_pin_expression(busy_pin)
cg.add(var.set_busy_pin(busy))
cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY]))
if CONF_RESET_DURATION in config:
cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))
if transform := config.get(CONF_TRANSFORM):
transform[CONF_SWAP_XY] = False
else:
transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS}
rotation = config[CONF_ROTATION]
if rotation == 180:
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
elif rotation == 90:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
elif rotation == 270:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
transform_str = "|".join(
{
str(getattr(Transform, x.upper()))
for x in TRANSFORM_OPTIONS
if transform.get(x)
}
)
if transform_str:
cg.add(var.set_transform(RawExpression(transform_str)))

View File

@@ -9,9 +9,8 @@ namespace esphome::epaper_spi {
static const char *const TAG = "epaper_spi";
static constexpr const char *const EPAPER_STATE_STRINGS[] = {
"IDLE", "UPDATE", "RESET", "RESET_END",
"SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP",
"IDLE", "UPDATE", "RESET", "RESET_END", "SHOULD_WAIT", "INITIALISE",
"TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP",
};
const char *EPaperBase::epaper_state_to_string_() {
@@ -69,8 +68,8 @@ void EPaperBase::data(uint8_t value) {
// The command is the first byte, length is the length of data only in the second byte, followed by the data.
// [COMMAND, LENGTH, DATA...]
void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) {
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
format_hex_pretty(ptr, length, '.', false).c_str());
ESP_LOGV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
format_hex_pretty(ptr, length, '.', false).c_str());
this->dc_pin_->digital_write(false);
this->enable();
@@ -89,7 +88,7 @@ bool EPaperBase::is_idle_() const {
return !this->busy_pin_->digital_read();
}
bool EPaperBase::reset_() const {
bool EPaperBase::reset() {
if (this->reset_pin_ != nullptr) {
if (this->state_ == EPaperState::RESET) {
this->reset_pin_->digital_write(false);
@@ -105,16 +104,16 @@ void EPaperBase::update() {
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
return;
}
this->set_state_(EPaperState::RESET);
this->set_state_(EPaperState::UPDATE);
this->enable_loop();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
this->update_start_time_ = millis();
#endif
}
void EPaperBase::wait_for_idle_(bool should_wait) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (should_wait) {
this->waiting_for_idle_start_ = millis();
this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_;
}
this->waiting_for_idle_start_ = millis();
#endif
this->waiting_for_idle_ = should_wait;
}
@@ -138,7 +137,9 @@ void EPaperBase::loop() {
if (this->waiting_for_idle_) {
if (this->is_idle_()) {
this->waiting_for_idle_ = false;
ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
#endif
} else {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (now - this->waiting_for_idle_last_print_ >= 1000) {
@@ -164,23 +165,27 @@ void EPaperBase::process_state_() {
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
switch (this->state_) {
default:
ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
this->disable_loop();
ESP_LOGE(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
this->set_state_(EPaperState::IDLE);
break;
case EPaperState::IDLE:
this->disable_loop();
break;
case EPaperState::RESET:
case EPaperState::RESET_END:
if (this->reset_()) {
this->set_state_(EPaperState::UPDATE);
if (this->reset()) {
this->set_state_(EPaperState::INITIALISE);
} else {
this->set_state_(EPaperState::RESET_END);
this->set_state_(EPaperState::RESET_END, this->reset_duration_);
}
break;
case EPaperState::UPDATE:
this->do_update_(); // Calls ESPHome (current page) lambda
this->set_state_(EPaperState::INITIALISE);
if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) {
this->set_state_(EPaperState::IDLE);
return;
}
this->set_state_(EPaperState::RESET);
break;
case EPaperState::INITIALISE:
this->initialise_();
@@ -190,6 +195,10 @@ void EPaperBase::process_state_() {
if (!this->transfer_data()) {
return; // Not done yet, come back next loop
}
this->x_low_ = this->width_;
this->x_high_ = 0;
this->y_low_ = this->height_;
this->y_high_ = 0;
this->set_state_(EPaperState::POWER_ON);
break;
case EPaperState::POWER_ON:
@@ -197,7 +206,8 @@ void EPaperBase::process_state_() {
this->set_state_(EPaperState::REFRESH_SCREEN);
break;
case EPaperState::REFRESH_SCREEN:
this->refresh_screen();
this->refresh_screen(this->update_count_ != 0);
this->update_count_ = (this->update_count_ + 1) % this->full_update_every_;
this->set_state_(EPaperState::POWER_OFF);
break;
case EPaperState::POWER_OFF:
@@ -207,6 +217,7 @@ void EPaperBase::process_state_() {
case EPaperState::DEEP_SLEEP:
this->deep_sleep();
this->set_state_(EPaperState::IDLE);
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
break;
}
}
@@ -222,6 +233,9 @@ void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
}
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
if (state == EPaperState::IDLE) {
this->disable_loop();
}
}
void EPaperBase::start_command_() {
@@ -260,20 +274,73 @@ void EPaperBase::initialise_() {
this->mark_failed();
return;
}
ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args);
this->cmd_data(cmd, sequence + index, num_args);
index += num_args;
}
}
}
/**
* Check and rotate coordinates based on the transform flags.
* @param x
* @param y
* @return false if the coordinates are out of bounds
*/
bool EPaperBase::rotate_coordinates_(int &x, int &y) const {
if (!this->get_clipping().inside(x, y))
return false;
if (this->transform_ & SWAP_XY)
std::swap(x, y);
if (this->transform_ & MIRROR_X)
x = this->width_ - x - 1;
if (this->transform_ & MIRROR_Y)
y = this->height_ - y - 1;
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
return false;
return true;
}
/**
* Default implementation for monochrome displays where 8 pixels are packed to a byte.
* @param x
* @param y
* @param color
*/
void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
if (!rotate_coordinates_(x, y))
return;
const size_t pixel_position = y * this->width_ + x;
const size_t byte_position = pixel_position / 8;
const uint8_t bit_position = pixel_position % 8;
const uint8_t pixel_bit = 0x80 >> bit_position;
const auto original = this->buffer_[byte_position];
if ((color_to_bit(color) == 0)) {
this->buffer_[byte_position] = original & ~pixel_bit;
} else {
this->buffer_[byte_position] = original | pixel_bit;
}
this->x_low_ = clamp_at_most(this->x_low_, x);
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
this->y_low_ = clamp_at_most(this->y_low_, y);
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
}
void EPaperBase::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG, " Model: %s", this->name_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_PIN(" CS Pin: ", this->cs_);
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG,
" SPI Data Rate: %uMHz\n"
" Full update every: %d\n"
" Swap X/Y: %s\n"
" Mirror X: %s\n"
" Mirror Y: %s",
(unsigned) (this->data_rate_ / 1000000), this->full_update_every_, YESNO(this->transform_ & SWAP_XY),
YESNO(this->transform_ & MIRROR_X), YESNO(this->transform_ & MIRROR_Y));
}
} // namespace esphome::epaper_spi

View File

@@ -5,8 +5,6 @@
#include "esphome/components/split_buffer/split_buffer.h"
#include "esphome/core/component.h"
#include <queue>
namespace esphome::epaper_spi {
using namespace display;
@@ -25,10 +23,16 @@ enum class EPaperState : uint8_t {
DEEP_SLEEP, // deep sleep the display
};
static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
static constexpr uint8_t NONE = 0;
static constexpr uint8_t MIRROR_X = 1;
static constexpr uint8_t MIRROR_Y = 2;
static constexpr uint8_t SWAP_XY = 4;
static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
static constexpr size_t MAX_TRANSFER_SIZE = 128;
static constexpr uint8_t DELAY_FLAG = 0xFF;
class EPaperBase : public DisplayBuffer,
class EPaperBase : public Display,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> {
public:
@@ -45,6 +49,8 @@ class EPaperBase : public DisplayBuffer,
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
void set_transform(uint8_t transform) { this->transform_ = transform; }
void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; }
void dump_config() override;
void command(uint8_t value);
@@ -60,20 +66,47 @@ class EPaperBase : public DisplayBuffer,
DisplayType get_display_type() override { return this->display_type_; };
// Default implementations for monochrome displays
static uint8_t color_to_bit(Color color) {
// It's always a shade of gray. Map to BLACK or WHITE.
// We split the luminance at a suitable point
if ((static_cast<int>(color.r) + color.g + color.b) > 512) {
return 1;
}
return 0;
}
void fill(Color color) override {
auto pixel_color = color_to_bit(color) ? 0xFF : 0x00;
// We store 8 pixels per byte
this->buffer_.fill(pixel_color);
this->x_high_ = this->width_;
this->y_high_ = this->height_;
this->x_low_ = 0;
this->y_low_ = 0;
}
void clear() override {
// clear buffer to white, just like real paper.
this->fill(COLOR_ON);
}
protected:
int get_height_internal() override { return this->height_; };
int get_width_internal() override { return this->width_; };
int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; }
void draw_pixel_at(int x, int y, Color color) override;
void process_state_();
const char *epaper_state_to_string_();
bool is_idle_() const;
void setup_pins_() const;
bool reset_() const;
virtual bool reset();
void initialise_();
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length);
virtual int get_width_controller() { return this->get_width_internal(); };
bool rotate_coordinates_(int &x, int &y) const;
/**
* Methods that must be implemented by concrete classes to control the display
@@ -86,7 +119,7 @@ class EPaperBase : public DisplayBuffer,
/**
* Refresh the screen after data transfer
*/
virtual void refresh_screen() = 0;
virtual void refresh_screen(bool partial) = 0;
/**
* Power the display on
@@ -118,24 +151,31 @@ class EPaperBase : public DisplayBuffer,
DisplayType display_type_;
size_t buffer_length_{};
size_t current_data_index_{0}; // used by data transfer to track progress
uint32_t reset_duration_{200};
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
uint32_t transfer_start_time_{};
uint32_t waiting_for_idle_last_print_{0};
uint32_t waiting_for_idle_start_{0};
#endif
size_t current_data_index_{}; // used by data transfer to track progress
split_buffer::SplitBuffer buffer_{};
GPIOPin *dc_pin_{};
GPIOPin *busy_pin_{};
GPIOPin *reset_pin_{};
bool waiting_for_idle_{};
uint32_t delay_until_{};
uint8_t transform_{};
uint8_t update_count_{};
// these values represent the bounds of the updated buffer. Note that x_high and y_high
// point to the pixel past the last one updated, i.e. may range up to width/height.
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
bool waiting_for_idle_{false};
uint32_t delay_until_{0};
split_buffer::SplitBuffer buffer_;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
uint32_t waiting_for_idle_last_print_{};
uint32_t waiting_for_idle_start_{};
#endif
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
uint32_t update_start_time_{};
#endif
// properties with specific initialisers go last
EPaperState state_{EPaperState::IDLE};
uint32_t reset_duration_{10};
uint8_t full_update_every_{1};
};
} // namespace esphome::epaper_spi

View File

@@ -6,7 +6,6 @@
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.6c";
static constexpr size_t MAX_TRANSFER_SIZE = 128;
static constexpr unsigned char GRAY_THRESHOLD = 50;
enum E6Color {
@@ -75,24 +74,24 @@ static uint8_t color_to_hex(Color color) {
}
void EPaperSpectraE6::power_on() {
ESP_LOGD(TAG, "Power on");
ESP_LOGV(TAG, "Power on");
this->command(0x04);
}
void EPaperSpectraE6::power_off() {
ESP_LOGD(TAG, "Power off");
ESP_LOGV(TAG, "Power off");
this->command(0x02);
this->data(0x00);
}
void EPaperSpectraE6::refresh_screen() {
ESP_LOGD(TAG, "Refresh");
void EPaperSpectraE6::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
}
void EPaperSpectraE6::deep_sleep() {
ESP_LOGD(TAG, "Deep sleep");
ESP_LOGV(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
}
@@ -109,12 +108,11 @@ void EPaperSpectraE6::clear() {
this->fill(COLOR_ON);
}
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
void HOT EPaperSpectraE6::draw_pixel_at(int x, int y, Color color) {
if (!this->rotate_coordinates_(x, y))
return;
auto pixel_bits = color_to_hex(color);
uint32_t pixel_position = x + y * this->get_width_controller();
uint32_t pixel_position = x + y * this->get_width_internal();
uint32_t byte_position = pixel_position / 2;
auto original = this->buffer_[byte_position];
if ((pixel_position & 1) != 0) {
@@ -128,10 +126,6 @@ bool HOT EPaperSpectraE6::transfer_data() {
const uint32_t start_time = App.get_loop_component_start_time();
const size_t buffer_length = this->buffer_length_;
if (this->current_data_index_ == 0) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
this->transfer_start_time_ = millis();
#endif
ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis());
this->command(0x10);
}
@@ -160,7 +154,6 @@ bool HOT EPaperSpectraE6::transfer_data() {
this->end_data_();
}
this->current_data_index_ = 0;
ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_);
return true;
}
} // namespace esphome::epaper_spi

View File

@@ -16,11 +16,11 @@ class EPaperSpectraE6 : public EPaperBase {
void clear() override;
protected:
void refresh_screen() override;
void refresh_screen(bool partial) override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void draw_pixel_at(int x, int y, Color color) override;
bool transfer_data() override;
};

View File

@@ -0,0 +1,86 @@
#include "epaper_spi_ssd1677.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.ssd1677";
void EPaperSSD1677::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
this->command(0x22);
this->data(partial ? 0xFF : 0xF7);
this->command(0x20);
}
void EPaperSSD1677::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
this->command(0x10);
}
bool EPaperSSD1677::reset() {
if (EPaperBase::reset()) {
this->command(0x12);
return true;
}
return false;
}
bool HOT EPaperSSD1677::transfer_data() {
auto start_time = millis();
if (this->current_data_index_ == 0) {
uint8_t data[4]{};
// round to byte boundaries
this->x_low_ &= ~7;
this->y_low_ &= ~7;
this->x_high_ += 7;
this->x_high_ &= ~7;
this->y_high_ += 7;
this->y_high_ &= ~7;
data[0] = this->x_low_;
data[1] = this->x_low_ / 256;
data[2] = this->x_high_ - 1;
data[3] = (this->x_high_ - 1) / 256;
cmd_data(0x4E, data, 2);
cmd_data(0x44, data, sizeof(data));
data[0] = this->y_low_;
data[1] = this->y_low_ / 256;
data[2] = this->y_high_ - 1;
data[3] = (this->y_high_ - 1) / 256;
cmd_data(0x4F, data, 2);
this->cmd_data(0x45, data, sizeof(data));
// for monochrome, we still need to clear the red data buffer at least once to prevent it
// causing dirty pixels after partial refresh.
this->command(this->send_red_ ? 0x26 : 0x24);
this->current_data_index_ = this->y_low_; // actually current line
}
size_t row_length = (this->x_high_ - this->x_low_) / 8;
FixedVector<uint8_t> bytes_to_send{};
bytes_to_send.init(row_length);
ESP_LOGV(TAG, "Writing bytes at line %zu at %ums", this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
size_t data_idx = (this->current_data_index_ * this->width_ + this->x_low_) / 8;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->send_red_ ? 0 : this->buffer_[data_idx++];
}
++this->current_data_index_;
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->end_data_();
return false;
}
}
this->end_data_();
this->current_data_index_ = 0;
if (this->send_red_) {
this->send_red_ = false;
return false;
}
return true;
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,25 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
class EPaperSSD1677 : public EPaperBase {
public:
EPaperSSD1677(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_ = width * height / 8; // 8 pixels per byte
}
protected:
void refresh_screen(bool partial) override;
void power_on() override {}
void power_off() override{};
void deep_sleep() override;
bool reset() override;
bool transfer_data() override;
bool send_red_{true};
};
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,42 @@
from esphome.const import CONF_DATA_RATE
from . import EpaperModel
class SSD1677(EpaperModel):
def __init__(self, name, class_name="EPaperSSD1677", **kwargs):
if CONF_DATA_RATE not in kwargs:
kwargs[CONF_DATA_RATE] = "20MHz"
super().__init__(name, class_name, **kwargs)
# fmt: off
def get_init_sequence(self, config: dict):
width, _height = self.get_dimensions(config)
return (
(0x18, 0x80), # Select internal Temp sensor
(0x0C, 0xAE, 0xC7, 0xC3, 0xC0, 0x80), # inrush current level 2
(0x01, (width - 1) % 256, (width - 1) // 256, 0x02), # Set column gate limit
(0x3C, 0x01), # Set border waveform
(0x11, 3), # Set transform
)
ssd1677 = SSD1677("ssd1677")
ssd1677.extend(
"seeed-ee04-mono-4.26",
width=800,
height=480,
mirror_x=True,
cs_pin=44,
dc_pin=10,
reset_pin=38,
busy_pin={
"number": 4,
"inverted": False,
"mode": {
"input": True,
"pulldown": True,
},
},
)

View File

@@ -37,6 +37,7 @@ from esphome.const import (
__version__,
)
from esphome.core import CORE, HexInt, TimePeriod
from esphome.coroutine import CoroPriority, coroutine_with_priority
import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, write_file_if_changed
from esphome.types import ConfigType
@@ -262,15 +263,32 @@ def add_idf_component(
"deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report "
"an issue to the external_component author and ask them to update it."
)
components_registry = CORE.data[KEY_ESP32][KEY_COMPONENTS]
if components:
for comp in components:
CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = {
existing = components_registry.get(comp)
if existing and existing.get(KEY_REF) != ref:
_LOGGER.warning(
"IDF component %s version conflict %s replaced by %s",
comp,
existing.get(KEY_REF),
ref,
)
components_registry[comp] = {
KEY_REPO: repo,
KEY_REF: ref,
KEY_PATH: f"{path}/{comp}" if path else comp,
}
else:
CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = {
existing = components_registry.get(name)
if existing and existing.get(KEY_REF) != ref:
_LOGGER.warning(
"IDF component %s version conflict %s replaced by %s",
name,
existing.get(KEY_REF),
ref,
)
components_registry[name] = {
KEY_REPO: repo,
KEY_REF: ref,
KEY_PATH: path,
@@ -592,6 +610,14 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True
def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
if "^" not in value:
raise cv.Invalid(f"Invalid IDF component shorthand '{value}'")
name, ref = value.split("^", 1)
return {CONF_NAME: name, CONF_REF: ref}
def _validate_idf_component(config: ConfigType) -> ConfigType:
"""Validate IDF component config and warn about deprecated options."""
if CONF_REFRESH in config:
@@ -659,14 +685,19 @@ FRAMEWORK_SCHEMA = cv.Schema(
),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REF): cv.string,
cv.Optional(CONF_PATH): cv.string,
cv.Optional(CONF_REFRESH): cv.All(cv.string, cv.source_refresh),
}
cv.Any(
cv.All(cv.string_strict, _parse_idf_component),
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REF): cv.string,
cv.Optional(CONF_PATH): cv.string,
cv.Optional(CONF_REFRESH): cv.All(
cv.string, cv.source_refresh
),
}
),
),
_validate_idf_component,
)
@@ -851,6 +882,18 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_yaml_idf_components(components: list[ConfigType]):
"""Add IDF components from YAML config with final priority to override code-added components."""
for component in components:
add_idf_component(
name=component[CONF_NAME],
repo=component.get(CONF_SOURCE),
ref=component.get(CONF_REF),
path=component.get(CONF_PATH),
)
async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
@@ -1097,13 +1140,10 @@ async def to_code(config):
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
for component in conf[CONF_COMPONENTS]:
add_idf_component(
name=component[CONF_NAME],
repo=component.get(CONF_SOURCE),
ref=component.get(CONF_REF),
path=component.get(CONF_PATH),
)
# Components from YAML are added in a separate coroutine with FINAL priority
# Schedule it to run after all other components
if conf[CONF_COMPONENTS]:
CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS])
APP_PARTITION_SIZES = {

View File

@@ -38,7 +38,7 @@ void BLECharacteristic::parse_descriptors() {
}
if (status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d",
this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status);
this->service->client->get_connection_index(), this->service->client->address_str(), status);
break;
}
if (count == 0) {
@@ -51,7 +51,7 @@ void BLECharacteristic::parse_descriptors() {
desc->characteristic = this;
this->descriptors.push_back(desc);
ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(),
this->service->client->address_str().c_str(), desc->uuid.to_string().c_str(), desc->handle);
this->service->client->address_str(), desc->uuid.to_string().c_str(), desc->handle);
offset++;
}
}
@@ -84,7 +84,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size,
new_val, write_type, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%d] [%s] Error sending write value to BLE gattc server, status=%d",
this->service->client->get_connection_index(), this->service->client->address_str().c_str(), status);
this->service->client->get_connection_index(), this->service->client->address_str(), status);
}
return status;
}

View File

@@ -41,7 +41,7 @@ void BLEClientBase::setup() {
}
void BLEClientBase::set_state(espbt::ClientState st) {
ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st);
ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_, (int) st);
ESPBTClient::set_state(st);
}
@@ -71,7 +71,7 @@ void BLEClientBase::dump_config() {
ESP_LOGCONFIG(TAG,
" Address: %s\n"
" Auto-Connect: %s",
this->address_str().c_str(), TRUEFALSE(this->auto_connect_));
this->address_str(), TRUEFALSE(this->auto_connect_));
ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state()));
if (this->status_ == ESP_GATT_NO_RESOURCES) {
ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config.");
@@ -104,12 +104,11 @@ void BLEClientBase::connect() {
// Prevent duplicate connection attempts
if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
this->state_ == espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_,
this->address_str_.c_str(), espbt::client_state_to_string(this->state_));
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state_));
return;
}
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(),
this->remote_addr_type_);
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
this->paired_ = false;
// Enable loop for state processing
this->enable_loop();
@@ -135,13 +134,13 @@ esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda
void BLEClientBase::disconnect() {
if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_.c_str(),
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state_));
return;
}
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
this->address_str_.c_str());
this->address_str_);
this->want_disconnect_ = true;
return;
}
@@ -150,8 +149,7 @@ void BLEClientBase::disconnect() {
void BLEClientBase::unconditional_disconnect() {
// Disconnect without checking the state.
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(),
this->conn_id_);
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
if (this->state_ == espbt::ClientState::DISCONNECTING) {
this->log_error_("Already disconnecting");
return;
@@ -192,24 +190,23 @@ void BLEClientBase::release_services() {
}
void BLEClientBase::log_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_.c_str(), name);
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation,
status);
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, err);
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, err);
}
void BLEClientBase::log_connection_params_(const char *param_type) {
ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_, param_type);
}
void BLEClientBase::handle_connection_result_(esp_err_t ret) {
@@ -220,15 +217,15 @@ void BLEClientBase::handle_connection_result_(esp_err_t ret) {
}
void BLEClientBase::log_error_(const char *message) {
ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message);
}
void BLEClientBase::log_error_(const char *message, int code) {
ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_.c_str(), message, code);
ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_, message, code);
}
void BLEClientBase::log_warning_(const char *message) {
ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message);
}
void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
@@ -264,13 +261,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && esp_gattc_if != this->gattc_if_)
return false;
ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_,
this->address_str_.c_str(), event, esp_gattc_if);
ESP_LOGV(TAG, "[%d] [%s] gattc_event_handler: event=%d gattc_if=%d", this->connection_index_, this->address_str_,
event, esp_gattc_if);
switch (event) {
case ESP_GATTC_REG_EVT: {
if (param->reg.status == ESP_GATT_OK) {
ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_.c_str(),
ESP_LOGV(TAG, "[%d] [%s] gattc registered app id %d", this->connection_index_, this->address_str_,
this->app_id);
this->gattc_if_ = esp_gattc_if;
} else {
@@ -292,7 +289,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
// arriving after we've already transitioned to IDLE state.
if (this->state_ == espbt::ClientState::IDLE) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
this->address_str_.c_str(), param->open.status);
this->address_str_, param->open.status);
break;
}
@@ -301,7 +298,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
// because it means we have a bad assumption about how the
// ESP BT stack works.
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status);
this->address_str_, espbt::client_state_to_string(this->state_), param->open.status);
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->log_gattc_warning_("Connection open", param->open.status);
@@ -318,7 +315,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
// MTU negotiation already started in ESP_GATTC_CONNECT_EVT
this->set_state(espbt::ClientState::CONNECTED);
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_);
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// Cached connections already connected with medium parameters, no update needed
// only set our state, subclients might have more stuff to do yet.
@@ -354,8 +351,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->state_ == espbt::ClientState::CONNECTED) {
this->log_warning_("Remote closed during discovery");
} else {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_,
this->address_str_.c_str(), param->disconnect.reason);
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
param->disconnect.reason);
}
this->release_services();
this->set_state(espbt::ClientState::IDLE);
@@ -366,12 +363,12 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (this->conn_id_ != param->cfg_mtu.conn_id)
return false;
if (param->cfg_mtu.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_,
this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status);
ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_, this->address_str_,
param->cfg_mtu.mtu, param->cfg_mtu.status);
// No state change required here - disconnect event will follow if needed.
break;
}
ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(),
ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_,
param->cfg_mtu.status, param->cfg_mtu.mtu);
this->mtu_ = param->cfg_mtu.mtu;
break;
@@ -415,14 +412,14 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
#ifdef USE_ESP32_BLE_DEVICE
for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_,
svc->uuid.to_string().c_str());
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_,
this->address_str_.c_str(), svc->start_handle, svc->end_handle);
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_,
svc->start_handle, svc->end_handle);
}
#endif
}
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str());
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
this->state_ = espbt::ClientState::ESTABLISHED;
break;
}
@@ -503,7 +500,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
default:
// ideally would check all other events for matching conn_id
ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event);
ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event);
break;
}
return true;
@@ -520,7 +517,7 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
case ESP_GAP_BLE_SEC_REQ_EVT:
if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
return;
ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event);
ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_, event);
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
break;
// This event is sent once authentication has completed
@@ -529,13 +526,13 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
return;
esp_bd_addr_t bd_addr;
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_.c_str(),
ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_,
format_hex(bd_addr, 6).c_str());
if (!param->ble_security.auth_cmpl.success) {
this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason);
} else {
this->paired_ = true;
ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_.c_str(),
ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_,
param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode);
}
break;
@@ -598,7 +595,7 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) {
}
}
ESP_LOGW(TAG, "[%d] [%s] Cannot parse characteristic value of type 0x%x length %d", this->connection_index_,
this->address_str_.c_str(), value[0], length);
this->address_str_, value[0], length);
return NAN;
}

View File

@@ -10,7 +10,6 @@
#endif
#include <array>
#include <string>
#include <vector>
#include <esp_bt_defs.h>
@@ -23,6 +22,7 @@ namespace esphome::esp32_ble_client {
namespace espbt = esphome::esp32_ble_tracker;
static const int UNSET_CONN_ID = 0xFFFF;
static constexpr size_t MAC_ADDR_STR_LEN = 18; // "AA:BB:CC:DD:EE:FF\0"
class BLEClientBase : public espbt::ESPBTClient, public Component {
public:
@@ -58,14 +58,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
this->remote_bda_[4] = (address >> 8) & 0xFF;
this->remote_bda_[5] = (address >> 0) & 0xFF;
if (address == 0) {
this->address_str_ = "";
this->address_str_[0] = '\0';
} else {
char buf[18];
format_mac_addr_upper(this->remote_bda_, buf);
this->address_str_ = buf;
format_mac_addr_upper(this->remote_bda_, this->address_str_);
}
}
const std::string &address_str() const { return this->address_str_; }
const char *address_str() const { return this->address_str_; }
#ifdef USE_ESP32_BLE_DEVICE
BLEService *get_service(espbt::ESPBTUUID uuid);
@@ -104,7 +102,6 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
uint64_t address_{0};
// Group 2: Container types (grouped for memory optimization)
std::string address_str_{};
#ifdef USE_ESP32_BLE_DEVICE
std::vector<BLEService *> services_;
#endif
@@ -113,8 +110,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
int gattc_if_;
esp_gatt_status_t status_{ESP_GATT_OK};
// Group 4: Arrays (6 bytes)
esp_bd_addr_t remote_bda_;
// Group 4: Arrays
char address_str_[MAC_ADDR_STR_LEN]{}; // 18 bytes: "AA:BB:CC:DD:EE:FF\0"
esp_bd_addr_t remote_bda_; // 6 bytes
// Group 5: 2-byte types
uint16_t conn_id_{UNSET_CONN_ID};

View File

@@ -51,7 +51,7 @@ void BLEService::parse_characteristics() {
}
if (status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->client->get_connection_index(),
this->client->address_str().c_str(), status);
this->client->address_str(), status);
break;
}
if (count == 0) {
@@ -65,7 +65,7 @@ void BLEService::parse_characteristics() {
characteristic->service = this;
this->characteristics.push_back(characteristic);
ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(),
this->client->address_str().c_str(), characteristic->uuid.to_string().c_str(), characteristic->handle,
this->client->address_str(), characteristic->uuid.to_string().c_str(), characteristic->handle,
characteristic->properties);
offset++;
}

View File

@@ -22,6 +22,11 @@ constexpr size_t CHUNK_SIZE = 1500;
void Esp32HostedUpdate::setup() {
this->update_info_.title = "ESP32 Hosted Coprocessor";
// if wifi is not present, connect to the coprocessor
#ifndef USE_WIFI
esp_hosted_connect_to_slave(); // NOLINT
#endif
// get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {

View File

@@ -6,10 +6,12 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_FULL_UPDATE_EVERY,
CONF_ID,
CONF_IGNORE_STRAPPING_WARNING,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_NUMBER,
CONF_OE_PIN,
CONF_PAGES,
CONF_TRANSFORM,
@@ -101,14 +103,21 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema,
cv.Optional(
CONF_CL_PIN,
default={CONF_NUMBER: 0, CONF_IGNORE_STRAPPING_WARNING: True},
): pins.internal_gpio_output_pin_schema,
cv.Optional(
CONF_LE_PIN,
default={CONF_NUMBER: 2, CONF_IGNORE_STRAPPING_WARNING: True},
): pins.internal_gpio_output_pin_schema,
# Data pins
cv.Optional(
CONF_DISPLAY_DATA_0_PIN, default=4
): pins.internal_gpio_output_pin_schema,
cv.Optional(
CONF_DISPLAY_DATA_1_PIN, default=5
CONF_DISPLAY_DATA_1_PIN,
default={CONF_NUMBER: 5, CONF_IGNORE_STRAPPING_WARNING: True},
): pins.internal_gpio_output_pin_schema,
cv.Optional(
CONF_DISPLAY_DATA_2_PIN, default=18

View File

@@ -108,7 +108,7 @@ LV_CONF_H_FORMAT = """\
def generate_lv_conf_h():
definitions = [as_macro(m, v) for m, v in df.lv_defines.items()]
definitions = [as_macro(m, v) for m, v in df.get_data(df.KEY_LV_DEFINES).items()]
definitions.sort()
return LV_CONF_H_FORMAT.format("\n".join(definitions))
@@ -140,11 +140,11 @@ def multi_conf_validate(configs: list[dict]):
)
def final_validation(configs):
if len(configs) != 1:
multi_conf_validate(configs)
def final_validation(config_list):
if len(config_list) != 1:
multi_conf_validate(config_list)
global_config = full_config.get()
for config in configs:
for config in config_list:
if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
for display_id in config[df.CONF_DISPLAYS]:
@@ -190,6 +190,14 @@ def final_validation(configs):
raise cv.Invalid(
f"Widget '{w}' does not have any dynamic properties to refresh",
)
# Do per-widget type final validation for update actions
for widget_type, update_configs in df.get_data(df.KEY_UPDATED_WIDGETS).items():
for conf in update_configs:
for id_conf in conf.get(CONF_ID, ()):
name = id_conf[CONF_ID]
path = global_config.get_path_for_id(name)
widget_conf = global_config.get_config_for_path(path[:-1])
widget_type.final_validate(name, conf, widget_conf, path[1:])
async def to_code(configs):
@@ -276,6 +284,7 @@ async def to_code(configs):
config[df.CONF_FULL_REFRESH],
config[CONF_DRAW_ROUNDING],
config[df.CONF_RESUME_ON_INPUT],
config[df.CONF_UPDATE_WHEN_DISPLAY_IDLE],
)
await cg.register_component(lv_component, config)
Widget.create(config[CONF_ID], lv_component, LvScrActType(), config)
@@ -373,6 +382,9 @@ LVGL_SCHEMA = cv.All(
df.CONF_DEFAULT_FONT, default="montserrat_14"
): lvalid.lv_font,
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
cv.Optional(
df.CONF_UPDATE_WHEN_DISPLAY_IDLE, default=False
): cv.boolean,
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
cv.Optional(CONF_LOG_LEVEL, default="WARN"): cv.one_of(

View File

@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
from esphome.core import ID, Lambda
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@@ -20,11 +20,27 @@ from .helpers import requires_component
LOGGER = logging.getLogger(__name__)
lvgl_ns = cg.esphome_ns.namespace("lvgl")
lv_defines = {} # Dict of #defines to provide as build flags
DOMAIN = "lvgl"
KEY_LV_DEFINES = "lv_defines"
KEY_UPDATED_WIDGETS = "updated_widgets"
def get_data(key, default=None):
"""
Get a data structure from the global data store by key
:param key: A key for the data
:param default: The default data - the default is an empty dict
:return:
"""
return CORE.data.setdefault(DOMAIN, {}).setdefault(
key, default if default is not None else {}
)
def add_define(macro, value="1"):
if macro in lv_defines and lv_defines[macro] != value:
lv_defines = get_data(KEY_LV_DEFINES)
value = str(value)
if lv_defines.setdefault(macro, value) != value:
LOGGER.error(
"Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
)
@@ -279,6 +295,8 @@ KEYBOARD_MODES = LvConstant(
)
ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE")
TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL")
SCROLL_DIRECTIONS = TILE_DIRECTIONS.extend("NONE")
SNAP_DIRECTIONS = LvConstant("LV_SCROLL_SNAP_", "NONE", "START", "END", "CENTER")
CHILD_ALIGNMENTS = LvConstant(
"LV_ALIGN_",
"TOP_LEFT",
@@ -511,6 +529,9 @@ CONF_ROLLOVER = "rollover"
CONF_ROOT_BACK_BTN = "root_back_btn"
CONF_SCALE_LINES = "scale_lines"
CONF_SCROLLBAR_MODE = "scrollbar_mode"
CONF_SCROLL_DIR = "scroll_dir"
CONF_SCROLL_SNAP_X = "scroll_snap_x"
CONF_SCROLL_SNAP_Y = "scroll_snap_y"
CONF_SELECTED_INDEX = "selected_index"
CONF_SELECTED_TEXT = "selected_text"
CONF_SHOW_SNOW = "show_snow"
@@ -537,6 +558,7 @@ CONF_TOUCHSCREENS = "touchscreens"
CONF_TRANSPARENCY_KEY = "transparency_key"
CONF_THEME = "theme"
CONF_UPDATE_ON_RELEASE = "update_on_release"
CONF_UPDATE_WHEN_DISPLAY_IDLE = "update_when_display_idle"
CONF_VISIBLE_ROW_COUNT = "visible_row_count"
CONF_WIDGET = "widget"
CONF_WIDGETS = "widgets"

View File

@@ -172,10 +172,14 @@ class DirectionalLayout(FlexLayout):
def validate(self, config):
assert config[CONF_LAYOUT].lower() == self.direction
config[CONF_LAYOUT] = {
layout = {
**FLEX_HV_STYLE,
CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(),
}
if pad_all := config.get("pad_all"):
layout[CONF_PAD_ROW] = pad_all
layout[CONF_PAD_COLUMN] = pad_all
config[CONF_LAYOUT] = layout
return config

View File

@@ -40,7 +40,7 @@ from .helpers import (
lv_fonts_used,
requires_component,
)
from .types import lv_font_t, lv_gradient_t
from .types import lv_gradient_t
opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER")
@@ -498,7 +498,9 @@ class LvFont(LValidator):
esphome_fonts_used.add(fontval)
return requires_component("font")(fontval)
super().__init__(validator, lv_font_t)
# Use font::Font* as return type for lambdas returning ESPHome fonts
# The inline overloads in lvgl_esphome.h handle conversion to lv_font_t*
super().__init__(validator, Font.operator("ptr"))
async def process(self, value, args=()):
if is_lv_font(value):

View File

@@ -106,6 +106,7 @@ void LvglComponent::dump_config() {
this->disp_drv_.hor_res, this->disp_drv_.ver_res, 100 / this->buffer_frac_, this->rotation,
(int) this->draw_rounding);
}
void LvglComponent::set_paused(bool paused, bool show_snow) {
this->paused_ = paused;
this->show_snow_ = show_snow;
@@ -124,32 +125,38 @@ void LvglComponent::esphome_lvgl_init() {
lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id());
}
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
lv_obj_add_event_cb(obj, callback, event, nullptr);
}
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
lv_event_code_t event2) {
add_event_cb(obj, callback, event1);
add_event_cb(obj, callback, event2);
}
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
lv_event_code_t event2, lv_event_code_t event3) {
add_event_cb(obj, callback, event1);
add_event_cb(obj, callback, event2);
add_event_cb(obj, callback, event3);
}
void LvglComponent::add_page(LvPageType *page) {
this->pages_.push_back(page);
page->set_parent(this);
lv_disp_set_default(this->disp_);
page->setup(this->pages_.size() - 1);
}
void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) {
if (index >= this->pages_.size())
return;
this->current_page_ = index;
lv_scr_load_anim(this->pages_[this->current_page_]->obj, anim, time, 0, false);
}
void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) {
if (this->pages_.empty() || (this->current_page_ == this->pages_.size() - 1 && !this->page_wrap_))
return;
@@ -158,6 +165,7 @@ void LvglComponent::show_next_page(lv_scr_load_anim_t anim, uint32_t time) {
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time);
}
void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
if (this->pages_.empty() || (this->current_page_ == 0 && !this->page_wrap_))
return;
@@ -166,8 +174,10 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) {
} while (this->pages_[this->current_page_]->skip); // skip empty pages()
this->show_page(this->current_page_, anim, time);
}
size_t LvglComponent::get_current_page() const { return this->current_page_; }
bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; }
void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
auto width = lv_area_get_width(area);
auto height = lv_area_get_height(area);
@@ -222,7 +232,7 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) {
}
void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
if (!this->paused_) {
if (!this->is_paused()) {
auto now = millis();
this->draw_buffer_(area, color_p);
ESP_LOGVV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area),
@@ -230,6 +240,7 @@ void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv
}
lv_disp_flush_ready(disp_drv);
}
IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeout) : timeout_(std::move(timeout)) {
parent->add_on_idle_callback([this](uint32_t idle_time) {
if (!this->is_idle_ && idle_time > this->timeout_.value()) {
@@ -377,6 +388,27 @@ void LvKeyboardType::set_obj(lv_obj_t *lv_obj) {
}
#endif // USE_LVGL_KEYBOARD
void LvglComponent::draw_end_() {
if (this->draw_end_callback_ != nullptr)
this->draw_end_callback_->trigger();
if (this->update_when_display_idle_) {
for (auto *disp : this->displays_)
disp->update();
}
}
bool LvglComponent::is_paused() const {
if (this->paused_)
return true;
if (this->update_when_display_idle_) {
for (auto *disp : this->displays_) {
if (!disp->is_idle())
return true;
}
}
return false;
}
void LvglComponent::write_random_() {
int iterations = 6 - lv_disp_get_inactive_time(this->disp_) / 60000;
if (iterations <= 0)
@@ -426,12 +458,13 @@ void LvglComponent::write_random_() {
* presses a key or clicks on the screen.
*/
LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh,
int draw_rounding, bool resume_on_input)
int draw_rounding, bool resume_on_input, bool update_when_display_idle)
: draw_rounding(draw_rounding),
displays_(std::move(displays)),
buffer_frac_(buffer_frac),
full_refresh_(full_refresh),
resume_on_input_(resume_on_input) {
resume_on_input_(resume_on_input),
update_when_display_idle_(update_when_display_idle) {
lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0);
lv_disp_drv_init(&this->disp_drv_);
this->disp_drv_.draw_buf = &this->draw_buf_;
@@ -487,7 +520,7 @@ void LvglComponent::setup() {
if (this->draw_start_callback_ != nullptr) {
this->disp_drv_.render_start_cb = render_start_cb;
}
if (this->draw_end_callback_ != nullptr) {
if (this->draw_end_callback_ != nullptr || this->update_when_display_idle_) {
this->disp_drv_.monitor_cb = monitor_cb;
}
#if LV_USE_LOG
@@ -509,14 +542,15 @@ void LvglComponent::setup() {
void LvglComponent::update() {
// update indicators
if (this->paused_) {
if (this->is_paused()) {
return;
}
this->idle_callbacks_.call(lv_disp_get_inactive_time(this->disp_));
}
void LvglComponent::loop() {
if (this->paused_) {
if (this->show_snow_)
if (this->is_paused()) {
if (this->paused_ && this->show_snow_)
this->write_random_();
} else {
lv_timer_handler_run_in_period(5);

View File

@@ -151,7 +151,7 @@ class LvglComponent : public PollingComponent {
public:
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
bool resume_on_input);
bool resume_on_input, bool update_when_display_idle);
static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
@@ -171,7 +171,9 @@ class LvglComponent : public PollingComponent {
// @param paused If true, pause the display. If false, resume the display.
// @param show_snow If true, show the snow effect when paused.
void set_paused(bool paused, bool show_snow);
bool is_paused() const { return this->paused_; }
// Returns true if the display is explicitly paused, or a blocking display update is in progress.
bool is_paused() const;
// If the display is paused and we have resume_on_input_ set to true, resume the display.
void maybe_wakeup() {
if (this->paused_ && this->resume_on_input_) {
@@ -210,10 +212,10 @@ class LvglComponent : public PollingComponent {
void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; }
protected:
// these functions are never called unless the callbacks are non-null since the
// LVGL callbacks that call them are not set unless the start/end callbacks are non-null
void draw_end_();
// Not checking for non-null callback since the
// LVGL callback that calls it is not set in that case
void draw_start_() const { this->draw_start_callback_->trigger(); }
void draw_end_() const { this->draw_end_callback_->trigger(); }
void write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_t *ptr);
@@ -222,6 +224,7 @@ class LvglComponent : public PollingComponent {
size_t buffer_frac_{1};
bool full_refresh_{};
bool resume_on_input_{};
bool update_when_display_idle_{};
lv_disp_draw_buf_t draw_buf_{};
lv_disp_drv_t disp_drv_{};

View File

@@ -1,6 +1,9 @@
from collections.abc import Callable
from esphome import config_validation as cv
from esphome.automation import Trigger, validate_automation
from esphome.components.time import RealTimeClock
from esphome.config_validation import prepend_path
from esphome.const import (
CONF_ARGS,
CONF_FORMAT,
@@ -19,7 +22,14 @@ from esphome.core import TimePeriod
from esphome.core.config import StartupTrigger
from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
from .defines import (
CONF_SCROLL_DIR,
CONF_SCROLL_SNAP_X,
CONF_SCROLL_SNAP_Y,
CONF_SCROLLBAR_MODE,
CONF_TIME_FORMAT,
LV_GRAD_DIR,
)
from .helpers import CONF_IF_NAN, requires_component, validate_printf
from .layout import (
FLEX_OBJ_SCHEMA,
@@ -233,9 +243,19 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
).one_of,
cv.Optional(CONF_SCROLL_DIR): df.SCROLL_DIRECTIONS.one_of,
cv.Optional(CONF_SCROLL_SNAP_X): df.SNAP_DIRECTIONS.one_of,
cv.Optional(CONF_SCROLL_SNAP_Y): df.SNAP_DIRECTIONS.one_of,
}
)
OBJ_PROPERTIES = {
CONF_SCROLL_SNAP_X,
CONF_SCROLL_SNAP_Y,
CONF_SCROLL_DIR,
CONF_SCROLLBAR_MODE,
}
# Also allow widget specific properties for use in style definitions
FULL_STYLE_SCHEMA = STYLE_SCHEMA.extend(
{
@@ -293,19 +313,36 @@ def automation_schema(typ: LvType):
}
def base_update_schema(widget_type, parts):
def _update_widget(widget_type: WidgetType) -> Callable[[dict], dict]:
"""
Create a schema for updating a widgets style properties, states and flags
During validation of update actions, create a map of action types to affected widgets
for use in final validation.
:param widget_type:
:return:
"""
def validator(value: dict) -> dict:
df.get_data(df.KEY_UPDATED_WIDGETS).setdefault(widget_type, []).append(value)
return value
return validator
def base_update_schema(widget_type: WidgetType | LvType, parts):
"""
Create a schema for updating a widget's style properties, states and flags.
:param widget_type: The type of the ID
:param parts: The allowable parts to specify
:return:
"""
return part_schema(parts).extend(
w_type = widget_type.w_type if isinstance(widget_type, WidgetType) else widget_type
schema = part_schema(parts).extend(
{
cv.Required(CONF_ID): cv.ensure_list(
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(widget_type),
cv.Required(CONF_ID): cv.use_id(w_type),
},
key=CONF_ID,
)
@@ -314,11 +351,9 @@ def base_update_schema(widget_type, parts):
}
)
def create_modify_schema(widget_type):
return base_update_schema(widget_type.w_type, widget_type.parts).extend(
widget_type.modify_schema
)
if isinstance(widget_type, WidgetType):
schema.add_extra(_update_widget(widget_type))
return schema
def obj_schema(widget_type: WidgetType):
@@ -422,7 +457,10 @@ def any_widget_schema(extras=None):
def validator(value):
if isinstance(value, dict):
# Convert to list
is_dict = True
value = [{k: v} for k, v in value.items()]
else:
is_dict = False
if not isinstance(value, list):
raise cv.Invalid("Expected a list of widgets")
result = []
@@ -443,7 +481,9 @@ def any_widget_schema(extras=None):
)
# Apply custom validation
value = widget_type.validate(value or {})
result.append({key: container_validator(value)})
path = [key] if is_dict else [index, key]
with prepend_path(path):
result.append({key: container_validator(value)})
return result
return validator

View File

@@ -152,18 +152,18 @@ class WidgetType:
# Local import to avoid circular import
from .automation import update_to_code
from .schemas import WIDGET_TYPES, create_modify_schema
from .schemas import WIDGET_TYPES, base_update_schema
if not is_mock:
if self.name in WIDGET_TYPES:
raise EsphomeError(f"Duplicate definition of widget type '{self.name}'")
WIDGET_TYPES[self.name] = self
# Register the update action automatically
# Register the update action automatically, adding widget-specific properties
register_action(
f"lvgl.{self.name}.update",
ObjUpdateAction,
create_modify_schema(self),
base_update_schema(self, self.parts).extend(self.modify_schema),
)(update_to_code)
@property
@@ -182,7 +182,6 @@ class WidgetType:
Generate code for a given widget
:param w: The widget
:param config: Its configuration
:return: Generated code as a list of text lines
"""
async def obj_creator(self, parent: MockObjClass, config: dict):
@@ -228,6 +227,15 @@ class WidgetType:
"""
return value
def final_validate(self, widget, update_config, widget_config, path):
"""
Allow final validation for a given widget type update action
:param widget: A widget
:param update_config: The configuration for the update action
:param widget_config: The configuration for the widget itself
:param path: The path to the widget, for error reporting
"""
class NumberType(WidgetType):
def get_max(self, config: dict):

View File

@@ -21,7 +21,6 @@ from ..defines import (
CONF_MAIN,
CONF_PAD_COLUMN,
CONF_PAD_ROW,
CONF_SCROLLBAR_MODE,
CONF_STYLES,
CONF_WIDGETS,
OBJ_FLAGS,
@@ -45,7 +44,7 @@ from ..lvcode import (
lv_obj,
lv_Pvariable,
)
from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES
from ..schemas import ALL_STYLES, OBJ_PROPERTIES, STYLE_REMAP, WIDGET_TYPES
from ..types import LV_STATE, LvType, WidgetType, lv_coord_t, lv_obj_t, lv_obj_t_ptr
EVENT_LAMB = "event_lamb__"
@@ -383,7 +382,7 @@ async def set_obj_properties(w: Widget, config):
clrs = join_enums(flag_clr, "LV_OBJ_FLAG_")
w.clear_flag(clrs)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_)
flag = f"LV_OBJ_FLAG_{key.upper()}"
with LvConditional(call_lambda(lamb)) as cond:
w.add_flag(flag)
@@ -408,13 +407,14 @@ async def set_obj_properties(w: Widget, config):
clears = join_enums(clears, "LV_STATE_")
w.clear_state(clears)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_)
state = f"LV_STATE_{key.upper()}"
with LvConditional(call_lambda(lamb)) as cond:
w.add_state(state)
cond.else_()
w.clear_state(state)
await w.set_property(CONF_SCROLLBAR_MODE, config, lv_name="obj")
for property in OBJ_PROPERTIES:
await w.set_property(property, config, lv_name="obj")
async def add_widgets(parent: Widget, config: dict):

View File

@@ -1,20 +1,52 @@
from esphome.const import CONF_BUTTON
from esphome import config_validation as cv
from esphome.const import CONF_BUTTON, CONF_TEXT
from esphome.cpp_generator import MockObj
from ..defines import CONF_MAIN
from ..defines import CONF_MAIN, CONF_WIDGETS
from ..helpers import add_lv_use
from ..lv_validation import lv_text
from ..lvcode import lv, lv_expr
from ..schemas import TEXT_SCHEMA
from ..types import LvBoolean, WidgetType
from . import Widget
from .label import label_spec
lv_button_t = LvBoolean("lv_btn_t")
class ButtonType(WidgetType):
def __init__(self):
super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn")
super().__init__(
CONF_BUTTON, lv_button_t, (CONF_MAIN,), schema=TEXT_SCHEMA, lv_name="btn"
)
def validate(self, value):
if CONF_TEXT in value:
if CONF_WIDGETS in value:
raise cv.Invalid("Cannot use both text and widgets in a button")
add_lv_use("label")
return value
def get_uses(self):
return ("btn",)
async def to_code(self, w, config):
return []
def on_create(self, var: MockObj, config: dict):
if CONF_TEXT in config:
lv.label_create(var)
return var
async def to_code(self, w: Widget, config):
if text := config.get(CONF_TEXT):
label_widget = Widget.create(
None, lv_expr.obj_get_child(w.obj, 0), label_spec
)
await label_widget.set_property(CONF_TEXT, await lv_text.process(text))
def final_validate(self, widget, update_config, widget_config, path):
if CONF_TEXT in update_config and CONF_TEXT not in widget_config:
raise cv.Invalid(
"Button must have 'text:' configured to allow updating text", path
)
button_spec = ButtonType()

View File

@@ -6,7 +6,7 @@ from esphome.core import Lambda
from ..defines import CONF_MAIN, call_lambda
from ..lvcode import lv_add
from ..schemas import point_schema
from ..types import LvCompound, LvType
from ..types import LvCompound, LvType, lv_coord_t
from . import Widget, WidgetType
CONF_LINE = "line"
@@ -23,9 +23,7 @@ LINE_SCHEMA = {
async def process_coord(coord):
if isinstance(coord, Lambda):
coord = call_lambda(
await cg.process_lambda(coord, [], return_type="lv_coord_t")
)
coord = call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t))
if not coord.endswith("()"):
coord = f"static_cast<lv_coord_t>({coord})"
return cg.RawExpression(coord)

View File

@@ -1,3 +1,4 @@
import logging
from pathlib import Path
from esphome import git, yaml_util
@@ -20,18 +21,41 @@ from esphome.const import (
)
from esphome.core import EsphomeError
_LOGGER = logging.getLogger(__name__)
DOMAIN = CONF_PACKAGES
def validate_git_package(config: dict):
if CONF_URL not in config:
return config
config = BASE_SCHEMA(config)
new_config = config
def valid_package_contents(package_config: dict):
"""Validates that a package_config that will be merged looks as much as possible to a valid config
to fail early on obvious mistakes."""
if isinstance(package_config, dict):
if CONF_URL in package_config:
# If a URL key is found, then make sure the config conforms to a remote package schema:
return REMOTE_PACKAGE_SCHEMA(package_config)
# Validate manually since Voluptuous would regenerate dicts and lose metadata
# such as ESPHomeDataBase
for k, v in package_config.items():
if not isinstance(k, str):
raise cv.Invalid("Package content keys must be strings")
if isinstance(v, (dict, list)):
continue # e.g. script: [] or logger: {level: debug}
if v is None:
continue # e.g. web_server:
raise cv.Invalid("Invalid component content in package definition")
return package_config
raise cv.Invalid("Package contents must be a dict")
def expand_file_to_files(config: dict):
if CONF_FILE in config:
new_config = config
new_config[CONF_FILES] = [config[CONF_FILE]]
del new_config[CONF_FILE]
return new_config
return new_config
return config
def validate_yaml_filename(value):
@@ -45,7 +69,7 @@ def validate_yaml_filename(value):
def validate_source_shorthand(value):
if not isinstance(value, str):
raise cv.Invalid("Shorthand only for strings")
raise cv.Invalid("Git URL shorthand only for strings")
git_file = git.GitFile.from_shorthand(value)
@@ -56,10 +80,17 @@ def validate_source_shorthand(value):
if git_file.ref:
conf[CONF_REF] = git_file.ref
return BASE_SCHEMA(conf)
return REMOTE_PACKAGE_SCHEMA(conf)
BASE_SCHEMA = cv.All(
def deprecate_single_package(config):
_LOGGER.warning(
"Including a single package under `packages:` is deprecated. Use a list instead."
)
return config
REMOTE_PACKAGE_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_URL): cv.url,
@@ -90,23 +121,30 @@ BASE_SCHEMA = cv.All(
}
),
cv.has_at_least_one_key(CONF_FILE, CONF_FILES),
expand_file_to_files,
)
PACKAGE_SCHEMA = cv.All(
cv.Any(validate_source_shorthand, BASE_SCHEMA, dict), validate_git_package
PACKAGE_SCHEMA = cv.Any( # A package definition is either:
validate_source_shorthand, # A git URL shorthand string that expands to a remote package schema, or
REMOTE_PACKAGE_SCHEMA, # a valid remote package schema, or
valid_package_contents, # Something that at least looks like an actual package, e.g. {wifi:{ssid: xxx}}
# which will have to be fully validated later as per each component's schema.
)
CONFIG_SCHEMA = cv.Any(
CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either:
cv.Schema(
{
str: PACKAGE_SCHEMA,
str: PACKAGE_SCHEMA, # a named dict of package definitions, or
}
),
[PACKAGE_SCHEMA],
[PACKAGE_SCHEMA], # a list of package definitions, or
cv.All( # a single package definition (deprecated)
cv.ensure_list(PACKAGE_SCHEMA), deprecate_single_package
),
)
def _process_base_package(config: dict, skip_update: bool = False) -> dict:
def _process_remote_package(config: dict, skip_update: bool = False) -> dict:
# When skip_update is True, use NEVER_REFRESH to prevent updates
actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH]
repo_dir, revert = git.clone_or_update(
@@ -185,7 +223,7 @@ def _process_base_package(config: dict, skip_update: bool = False) -> dict:
def _process_package(package_config, config, skip_update: bool = False):
recursive_package = package_config
if CONF_URL in package_config:
package_config = _process_base_package(package_config, skip_update)
package_config = _process_remote_package(package_config, skip_update)
if isinstance(package_config, dict):
recursive_package = do_packages_pass(package_config, skip_update)
return merge_config(recursive_package, config)

View File

@@ -141,6 +141,24 @@ void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, st
}
}
#ifdef USE_ESP8266
void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name,
EntityBase *obj, std::string &area, std::string &node,
std::string &friendly_name) {
#else
void PrometheusHandler::print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj,
std::string &area, std::string &node, std::string &friendly_name) {
#endif
stream->print(metric_name);
stream->print(ESPHOME_F("{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
}
// Type-specific implementation
#ifdef USE_SENSOR
void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) {
@@ -303,13 +321,7 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
if (obj->is_internal() && !this->include_internal_)
return;
// State
stream->print(ESPHOME_F("esphome_light_state{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
print_metric_labels_(stream, ESPHOME_F("esphome_light_state"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\"} "));
stream->print(obj->remote_values.is_on());
stream->print(ESPHOME_F("\n"));
@@ -318,78 +330,45 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat
float brightness, r, g, b, w;
color.as_brightness(&brightness);
color.as_rgbw(&r, &g, &b, &w);
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"brightness\"} "));
stream->print(brightness);
stream->print(ESPHOME_F("\n"));
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"r\"} "));
stream->print(r);
stream->print(ESPHOME_F("\n"));
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"g\"} "));
stream->print(g);
stream->print(ESPHOME_F("\n"));
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"b\"} "));
stream->print(b);
stream->print(ESPHOME_F("\n"));
stream->print(ESPHOME_F("esphome_light_color{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",channel=\"w\"} "));
stream->print(w);
stream->print(ESPHOME_F("\n"));
// Effect
std::string effect = obj->get_effect_name();
if (effect == "None") {
stream->print(ESPHOME_F("esphome_light_effect_active{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(ESPHOME_F("\",effect=\"None\"} 0\n"));
} else {
stream->print(ESPHOME_F("esphome_light_effect_active{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(ESPHOME_F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
if (obj->get_traits().supports_color_capability(light::ColorCapability::BRIGHTNESS)) {
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"brightness\"} "));
stream->print(brightness);
stream->print(ESPHOME_F("\n"));
}
if (obj->get_traits().supports_color_capability(light::ColorCapability::RGB)) {
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"r\"} "));
stream->print(r);
stream->print(ESPHOME_F("\n"));
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"g\"} "));
stream->print(g);
stream->print(ESPHOME_F("\n"));
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"b\"} "));
stream->print(b);
stream->print(ESPHOME_F("\n"));
}
if (obj->get_traits().supports_color_capability(light::ColorCapability::WHITE)) {
print_metric_labels_(stream, ESPHOME_F("esphome_light_color"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",channel=\"w\"} "));
stream->print(w);
stream->print(ESPHOME_F("\n"));
}
// Skip effect metrics if light has no effects
if (!obj->get_effects().empty()) {
// Effect
std::string effect = obj->get_effect_name();
print_metric_labels_(stream, ESPHOME_F("esphome_light_effect_active"), obj, area, node, friendly_name);
stream->print(ESPHOME_F("\",effect=\""));
stream->print(effect.c_str());
stream->print(ESPHOME_F("\"} 1\n"));
// Only vary based on effect
if (effect == "None") {
stream->print(ESPHOME_F("None\"} 0\n"));
} else {
stream->print(effect.c_str());
stream->print(ESPHOME_F("\"} 1\n"));
}
}
}
#endif

View File

@@ -66,6 +66,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
void add_area_label_(AsyncResponseStream *stream, std::string &area);
void add_node_label_(AsyncResponseStream *stream, std::string &node);
void add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name);
/// Print metric name and common labels (id, area, node, friendly_name, name)
#ifdef USE_ESP8266
void print_metric_labels_(AsyncResponseStream *stream, const __FlashStringHelper *metric_name, EntityBase *obj,
std::string &area, std::string &node, std::string &friendly_name);
#else
void print_metric_labels_(AsyncResponseStream *stream, const char *metric_name, EntityBase *obj, std::string &area,
std::string &node, std::string &friendly_name);
#endif
#ifdef USE_SENSOR
/// Return the type for prometheus

View File

@@ -14,7 +14,7 @@ void PVVXDisplay::dump_config() {
" Service UUID : %s\n"
" Characteristic UUID : %s\n"
" Auto clear : %s",
this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(),
this->parent_->address_str(), this->service_uuid_.to_string().c_str(),
this->char_uuid_.to_string().c_str(), YESNO(this->auto_clear_enabled_));
#ifdef USE_TIME
ESP_LOGCONFIG(TAG, " Set time on connection: %s", YESNO(this->time_ != nullptr));
@@ -28,12 +28,12 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
switch (event) {
case ESP_GATTC_OPEN_EVT:
if (param->open.status == ESP_GATT_OK) {
ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str().c_str());
ESP_LOGV(TAG, "[%s] Connected successfully!", this->parent_->address_str());
this->delayed_disconnect_();
}
break;
case ESP_GATTC_DISCONNECT_EVT:
ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str().c_str());
ESP_LOGV(TAG, "[%s] Disconnected", this->parent_->address_str());
this->connection_established_ = false;
this->cancel_timeout("disconnect");
this->char_handle_ = 0;
@@ -41,7 +41,7 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(this->service_uuid_, this->char_uuid_);
if (chr == nullptr) {
ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Characteristic not found.", this->parent_->address_str());
break;
}
this->connection_established_ = true;
@@ -66,11 +66,11 @@ void PVVXDisplay::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb
return;
if (param->ble_security.auth_cmpl.success) {
ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str().c_str());
ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str());
// Now that pairing is complete, perform the pending writes
this->sync_time_and_display_();
} else {
ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str());
}
break;
}
@@ -89,22 +89,20 @@ void PVVXDisplay::update() {
void PVVXDisplay::display() {
if (!this->parent_->enabled) {
ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str().c_str());
ESP_LOGD(TAG, "[%s] BLE client not enabled. Init connection.", this->parent_->address_str());
this->parent_->set_enabled(true);
return;
}
if (!this->connection_established_) {
ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.",
this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.", this->parent_->address_str());
return;
}
if (!this->char_handle_) {
ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.",
this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] No ble handle to BLE client. State update can not be written.", this->parent_->address_str());
return;
}
ESP_LOGD(TAG, "[%s] Send to display: bignum %d, smallnum: %d, cfg: 0x%02x, validity period: %u.",
this->parent_->address_str().c_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_);
this->parent_->address_str(), this->bignum_, this->smallnum_, this->cfg_, this->validity_period_);
uint8_t blk[8] = {};
blk[0] = 0x22;
blk[1] = this->bignum_ & 0xff;
@@ -128,16 +126,16 @@ void PVVXDisplay::setcfgbit_(uint8_t bit, bool value) {
void PVVXDisplay::send_to_setup_char_(uint8_t *blk, size_t size) {
if (!this->connection_established_) {
ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Not connected to BLE client.", this->parent_->address_str());
return;
}
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, size,
blk, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
} else {
ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str().c_str(), size);
ESP_LOGV(TAG, "[%s] send %u bytes", this->parent_->address_str(), size);
this->delayed_disconnect_();
}
}
@@ -161,21 +159,21 @@ void PVVXDisplay::sync_time_() {
if (this->time_ == nullptr)
return;
if (!this->connection_established_) {
ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Not connected to BLE client. Time can not be synced.", this->parent_->address_str());
return;
}
if (!this->char_handle_) {
ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] No ble handle to BLE client. Time can not be synced.", this->parent_->address_str());
return;
}
auto time = this->time_->now();
if (!time.is_valid()) {
ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Time is not yet valid. Time can not be synced.", this->parent_->address_str());
return;
}
time.recalc_timestamp_utc(true); // calculate timestamp of local time
uint8_t blk[5] = {};
ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str().c_str(), time.timestamp);
ESP_LOGD(TAG, "[%s] Sync time with timestamp %" PRIu64 ".", this->parent_->address_str(), time.timestamp);
blk[0] = 0x23;
blk[1] = time.timestamp & 0xff;
blk[2] = (time.timestamp >> 8) & 0xff;

View File

@@ -46,14 +46,14 @@ template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts..
// execute this script using a tuple that contains the arguments
void execute_tuple(const std::tuple<Ts...> &tuple) {
this->execute_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
this->execute_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
}
// Internal function to give scripts readable names.
void set_name(const LogString *name) { name_ = name; }
protected:
template<int... S> void execute_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
template<size_t... S> void execute_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
this->execute(std::get<S>(tuple)...);
}
@@ -157,7 +157,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
this->trigger_tuple_(*tuple_ptr, typename gens<sizeof...(Ts)>::type());
this->trigger_tuple_(*tuple_ptr, std::make_index_sequence<sizeof...(Ts)>{});
}
}
@@ -174,7 +174,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
}
}
template<int... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
template<size_t... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
this->trigger(std::get<S>(tuple)...);
}
@@ -313,7 +313,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
// play_next_() can trigger more items to be queued
if (!this->param_queue_.empty()) {
auto &params = this->param_queue_.front();
this->play_next_tuple_(params, typename gens<sizeof...(Ts)>::type());
this->play_next_tuple_(params, std::make_index_sequence<sizeof...(Ts)>{});
this->param_queue_.pop_front();
} else {
// Queue is now empty - disable loop until next play_complex
@@ -330,7 +330,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
}
protected:
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
this->play_next_(std::get<S>(tuple)...);
}

View File

@@ -654,7 +654,7 @@ void ThermostatClimate::trigger_supplemental_action_() {
void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction action) {
// setup_complete_ helps us ensure an action is called immediately after boot
if ((action == this->humidification_action_) && this->setup_complete_) {
if ((action == this->humidification_action) && this->setup_complete_) {
// already in target mode
return;
}
@@ -683,7 +683,7 @@ void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction
this->prev_humidity_control_trigger_->stop_action();
this->prev_humidity_control_trigger_ = nullptr;
}
this->humidification_action_ = action;
this->humidification_action = action;
this->prev_humidity_control_trigger_ = trig;
if (trig != nullptr) {
trig->trigger();
@@ -1114,7 +1114,7 @@ bool ThermostatClimate::dehumidification_required_() {
}
// if we get here, the current humidity is between target + hysteresis and target - hysteresis,
// so the action should not change
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
}
bool ThermostatClimate::humidification_required_() {
@@ -1127,7 +1127,7 @@ bool ThermostatClimate::humidification_required_() {
}
// if we get here, the current humidity is between target - hysteresis and target + hysteresis,
// so the action should not change
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
return this->humidification_action == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
}
void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) {

View File

@@ -207,6 +207,9 @@ class ThermostatClimate : public climate::Climate, public Component {
void validate_target_temperature_high();
void validate_target_humidity();
/// The current humidification action
HumidificationAction humidification_action{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE};
protected:
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override;
@@ -301,9 +304,6 @@ class ThermostatClimate : public climate::Climate, public Component {
/// The current supplemental action
climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF};
/// The current humidification action
HumidificationAction humidification_action_{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE};
/// Default standard preset to use on start up
climate::ClimatePreset default_preset_{};

View File

@@ -11,10 +11,26 @@
namespace esphome {
// C++20 std::index_sequence is now used for tuple unpacking
// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility
// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
template<int...> struct seq {}; // NOLINT
template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {}; // NOLINT
template<int... S> struct gens<0, S...> { using type = seq<S...>; }; // NOLINT
// Remove before 2026.6.0
// NOLINTBEGIN(readability-identifier-naming)
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
template<int...> struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {};
template<int N, int... S>
struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens
: gens<N - 1, N - 1, S...> {};
template<int... S> struct gens<0, S...> { using type = seq<S...>; };
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
// NOLINTEND(readability-identifier-naming)
#define TEMPLATABLE_VALUE_(type, name) \
protected: \
@@ -152,11 +168,11 @@ template<typename... Ts> class Condition {
/// Call check with a tuple of values as parameter.
bool check_tuple(const std::tuple<Ts...> &tuple) {
return this->check_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
return this->check_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
}
protected:
template<int... S> bool check_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
template<size_t... S> bool check_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
return this->check(std::get<S>(tuple)...);
}
};
@@ -231,11 +247,11 @@ template<typename... Ts> class Action {
}
}
}
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
this->play_next_(std::get<S>(tuple)...);
}
void play_next_tuple_(const std::tuple<Ts...> &tuple) {
this->play_next_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
this->play_next_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
}
virtual void stop() {}
@@ -277,7 +293,9 @@ template<typename... Ts> class ActionList {
if (this->actions_begin_ != nullptr)
this->actions_begin_->play_complex(x...);
}
void play_tuple(const std::tuple<Ts...> &tuple) { this->play_tuple_(tuple, typename gens<sizeof...(Ts)>::type()); }
void play_tuple(const std::tuple<Ts...> &tuple) {
this->play_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
}
void stop() {
if (this->actions_begin_ != nullptr)
this->actions_begin_->stop_complex();
@@ -298,7 +316,7 @@ template<typename... Ts> class ActionList {
}
protected:
template<int... S> void play_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
template<size_t... S> void play_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
this->play(std::get<S>(tuple)...);
}

View File

@@ -659,7 +659,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
async def process_lambda(
value: Lambda,
parameters: TemplateArgsType,
capture: str = "=",
capture: str = "",
return_type: SafeExpType = None,
) -> LambdaExpression | None:
"""Process the given lambda value into a LambdaExpression.
@@ -702,12 +702,6 @@ async def process_lambda(
parts[i * 3 + 1] = var
parts[i * 3 + 2] = ""
# All id() references are global variables in generated C++ code.
# Global variables should not be captured - they're accessible everywhere.
# Use empty capture instead of capture-by-value.
if capture == "=":
capture = ""
if isinstance(value, ESPHomeDataBase) and value.esp_range is not None:
location = value.esp_range.start_mark
location.line += value.content_offset

View File

@@ -1,8 +1,12 @@
from collections.abc import Callable
import importlib
import logging
import os
from pathlib import Path
import re
import shutil
import stat
from types import TracebackType
from esphome import loader
from esphome.config import iter_component_configs, iter_components
@@ -301,9 +305,24 @@ def clean_cmake_cache():
pioenvs_cmake_path.unlink()
def clean_build(clear_pio_cache: bool = True):
import shutil
def _rmtree_error_handler(
func: Callable[[str], object],
path: str,
exc_info: tuple[type[BaseException], BaseException, TracebackType | None],
) -> None:
"""Error handler for shutil.rmtree to handle read-only files on Windows.
On Windows, git pack files and other files may be marked read-only,
causing shutil.rmtree to fail with "Access is denied". This handler
removes the read-only flag and retries the deletion.
"""
if os.access(path, os.W_OK):
raise exc_info[1].with_traceback(exc_info[2])
os.chmod(path, stat.S_IWUSR | stat.S_IRUSR)
func(path)
def clean_build(clear_pio_cache: bool = True):
# Allow skipping cache cleaning for integration tests
if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"):
_LOGGER.warning("Skipping build cleaning (ESPHOME_SKIP_CLEAN_BUILD set)")
@@ -312,11 +331,11 @@ def clean_build(clear_pio_cache: bool = True):
pioenvs = CORE.relative_pioenvs_path()
if pioenvs.is_dir():
_LOGGER.info("Deleting %s", pioenvs)
shutil.rmtree(pioenvs)
shutil.rmtree(pioenvs, onerror=_rmtree_error_handler)
piolibdeps = CORE.relative_piolibdeps_path()
if piolibdeps.is_dir():
_LOGGER.info("Deleting %s", piolibdeps)
shutil.rmtree(piolibdeps)
shutil.rmtree(piolibdeps, onerror=_rmtree_error_handler)
dependencies_lock = CORE.relative_build_path("dependencies.lock")
if dependencies_lock.is_file():
_LOGGER.info("Deleting %s", dependencies_lock)
@@ -337,12 +356,10 @@ def clean_build(clear_pio_cache: bool = True):
cache_dir = Path(config.get("platformio", "cache_dir"))
if cache_dir.is_dir():
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
shutil.rmtree(cache_dir)
shutil.rmtree(cache_dir, onerror=_rmtree_error_handler)
def clean_all(configuration: list[str]):
import shutil
data_dirs = []
for config in configuration:
item = Path(config)
@@ -364,7 +381,7 @@ def clean_all(configuration: list[str]):
if item.is_file() and not item.name.endswith(".json"):
item.unlink()
elif item.is_dir() and item.name != "storage":
shutil.rmtree(item)
shutil.rmtree(item, onerror=_rmtree_error_handler)
# Clean PlatformIO project files
try:
@@ -378,7 +395,7 @@ def clean_all(configuration: list[str]):
path = Path(config.get("platformio", pio_dir))
if path.is_dir():
_LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path)
shutil.rmtree(path)
shutil.rmtree(path, onerror=_rmtree_error_handler)
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome

View File

@@ -95,7 +95,7 @@ def test_package_invalid_dict(basic_esphome, basic_wifi):
@pytest.mark.parametrize(
"package",
"packages",
[
{"package1": "github://esphome/non-existant-repo/file1.yml@main"},
{"package2": "github://esphome/non-existant-repo/file1.yml"},
@@ -107,12 +107,12 @@ def test_package_invalid_dict(basic_esphome, basic_wifi):
],
],
)
def test_package_shorthand(package):
CONFIG_SCHEMA(package)
def test_package_shorthand(packages):
CONFIG_SCHEMA(packages)
@pytest.mark.parametrize(
"package",
"packages",
[
# not github
{"package1": "someplace://esphome/non-existant-repo/file1.yml@main"},
@@ -133,9 +133,9 @@ def test_package_shorthand(package):
[3],
],
)
def test_package_invalid(package):
def test_package_invalid(packages):
with pytest.raises(cv.Invalid):
CONFIG_SCHEMA(package)
CONFIG_SCHEMA(packages)
def test_package_include(basic_wifi, basic_esphome):
@@ -155,6 +155,33 @@ def test_package_include(basic_wifi, basic_esphome):
assert actual == expected
def test_single_package(
basic_esphome,
basic_wifi,
caplog: pytest.LogCaptureFixture,
):
"""
Tests the simple case where a single package is added to the top-level config as is.
In this test, the CONF_WIFI config is expected to be simply added to the top-level config.
This tests the case where the user just put packages: !include package.yaml, not
part of a list or mapping of packages.
This behavior is deprecated, the test also checks if a warning is issued.
"""
config = {CONF_ESPHOME: basic_esphome, CONF_PACKAGES: {CONF_WIFI: basic_wifi}}
expected = {CONF_ESPHOME: basic_esphome, CONF_WIFI: basic_wifi}
with caplog.at_level("WARNING"):
actual = packages_pass(config)
assert actual == expected
assert (
"Including a single package under `packages:` is deprecated. Use a list instead."
in caplog.text
)
def test_package_append(basic_wifi, basic_esphome):
"""
Tests the case where a key is present in both a package and top-level config.

View File

@@ -19,3 +19,8 @@ display:
- platform: epaper_spi
model: seeed-reterminal-e1002
- platform: epaper_spi
model: seeed-ee04-mono-4.26
# Override pins to avoid conflict with other display configs
busy_pin: 43
dc_pin: 42

View File

@@ -4,6 +4,10 @@ esp32:
cpu_frequency: 400MHz
framework:
type: esp-idf
components:
- espressif/mdns^1.8.2
- name: espressif/esp_hosted
ref: 2.6.6
advanced:
enable_idf_experimental_features: yes

View File

@@ -16,6 +16,18 @@ binary_sensor:
platform: template
- id: left_sensor
platform: template
- platform: lvgl
id: button_checker
name: LVGL button
widget: button_button
on_state:
then:
- lvgl.checkbox.update:
id: checkbox_id
state:
checked: !lambda |-
auto y = x; // block inlining of one line return
return y;
lvgl:
log_level: debug
@@ -414,6 +426,14 @@ lvgl:
logger.log: Long pressed repeated
- buttons:
- id: button_e
- button:
id: button_with_text
text: Button
on_click:
lvgl.button.update:
id: button_with_text
text: Clicked
- button:
layout: 2x1
id: button_button
@@ -537,6 +557,9 @@ lvgl:
- tileview:
id: tileview_id
scrollbar_mode: active
scroll_dir: all
scroll_elastic: true
scroll_momentum: true
on_value:
then:
- if:
@@ -546,7 +569,10 @@ lvgl:
- logger.log: "tile 1 is now showing"
tiles:
- id: tile_1
scroll_snap_y: center
scroll_snap_x: start
layout: vertical
pad_all: 6px
row: 0
column: 0
dir: ALL
@@ -1044,6 +1070,7 @@ lvgl:
opa: 0%
- id: page3
layout: Horizontal
pad_all: 6px
widgets:
- keyboard:
id: lv_keyboard

View File

@@ -60,6 +60,7 @@ display:
update_interval: never
lvgl:
update_when_display_idle: true
displays:
- tft_display
- second_display

View File

@@ -112,6 +112,25 @@ cover:
}
return COVER_CLOSED;
light:
- platform: binary
name: "Binary Light"
output: test_output
- platform: monochromatic
name: "Brightness Light"
output: test_output
- platform: rgb
name: "RGB Light"
red: test_output
green: test_output
blue: test_output
- platform: rgbw
name: "RGBW Light"
red: test_output
green: test_output
blue: test_output
white: test_output
lock:
- platform: template
id: template_lock1
@@ -122,6 +141,14 @@ lock:
return LOCK_STATE_UNLOCKED;
optimistic: true
output:
- platform: template
id: test_output
type: float
write_action:
- lambda: |-
// no-op for CI/build tests
(void)state;
select:
- platform: template
id: template_select1

View File

@@ -1,7 +1,9 @@
"""Test writer module functionality."""
from collections.abc import Callable
import os
from pathlib import Path
import stat
from typing import Any
from unittest.mock import MagicMock, patch
@@ -1062,3 +1064,109 @@ def test_clean_all_preserves_json_files(
# Verify logging mentions cleaning
assert "Cleaning" in caplog.text
assert str(build_dir) in caplog.text
@patch("esphome.writer.CORE")
def test_clean_build_handles_readonly_files(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_build handles read-only files (e.g., git pack files on Windows)."""
# Create directory structure with read-only files
pioenvs_dir = tmp_path / ".pioenvs"
pioenvs_dir.mkdir()
git_dir = pioenvs_dir / ".git" / "objects" / "pack"
git_dir.mkdir(parents=True)
# Create a read-only file (simulating git pack files on Windows)
readonly_file = git_dir / "pack-abc123.pack"
readonly_file.write_text("pack data")
os.chmod(readonly_file, stat.S_IRUSR) # Read-only
# Setup mocks
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps"
mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock"
# Verify file is read-only
assert not os.access(readonly_file, os.W_OK)
# Call the function - should not crash
with caplog.at_level("INFO"):
clean_build()
# Verify directory was removed despite read-only files
assert not pioenvs_dir.exists()
@patch("esphome.writer.CORE")
def test_clean_all_handles_readonly_files(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_all handles read-only files."""
from esphome.writer import clean_all
# Create config directory
config_dir = tmp_path / "config"
config_dir.mkdir()
build_dir = config_dir / ".esphome"
build_dir.mkdir()
# Create a subdirectory with read-only files
subdir = build_dir / "subdir"
subdir.mkdir()
readonly_file = subdir / "readonly.txt"
readonly_file.write_text("content")
os.chmod(readonly_file, stat.S_IRUSR) # Read-only
# Verify file is read-only
assert not os.access(readonly_file, os.W_OK)
# Call the function - should not crash
with caplog.at_level("INFO"):
clean_all([str(config_dir)])
# Verify directory was removed despite read-only files
assert not subdir.exists()
assert build_dir.exists() # .esphome dir itself is preserved
@patch("esphome.writer.CORE")
def test_clean_build_reraises_for_other_errors(
mock_core: MagicMock,
tmp_path: Path,
) -> None:
"""Test clean_build re-raises errors that are not read-only permission issues."""
# Create directory structure with a read-only subdirectory
# This prevents file deletion and triggers the error handler
pioenvs_dir = tmp_path / ".pioenvs"
pioenvs_dir.mkdir()
subdir = pioenvs_dir / "subdir"
subdir.mkdir()
test_file = subdir / "test.txt"
test_file.write_text("content")
# Make subdir read-only so files inside can't be deleted
os.chmod(subdir, stat.S_IRUSR | stat.S_IXUSR)
# Setup mocks
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = tmp_path / ".piolibdeps"
mock_core.relative_build_path.return_value = tmp_path / "dependencies.lock"
try:
# Mock os.access in writer module to return True (writable)
# This simulates a case where the error is NOT due to read-only permissions
# so the error handler should re-raise instead of trying to fix permissions
with (
patch("esphome.writer.os.access", return_value=True),
pytest.raises(PermissionError),
):
clean_build()
finally:
# Cleanup - restore write permission so tmp_path cleanup works
os.chmod(subdir, stat.S_IRWXU)