mirror of
https://github.com/esphome/esphome.git
synced 2026-01-25 14:02:08 -07:00
Compare commits
23 Commits
scheduler_
...
filter-pla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
037ef732a8 | ||
|
|
629ebdd794 | ||
|
|
fa2d206d74 | ||
|
|
4be240782e | ||
|
|
a7a5a0b9a2 | ||
|
|
9c85ec9182 | ||
|
|
23e58c1c7b | ||
|
|
b3955cd151 | ||
|
|
927d3715c1 | ||
|
|
a2d9941c62 | ||
|
|
caaa08d678 | ||
|
|
eb970cf44e | ||
|
|
083886c4b0 | ||
|
|
12a51ff047 | ||
|
|
b328758634 | ||
|
|
1207b9e995 | ||
|
|
e071380532 | ||
|
|
f071b6232a | ||
|
|
d443dbbf34 | ||
|
|
03a8ef71ff | ||
|
|
bda17180df | ||
|
|
ffae3501ab | ||
|
|
50bdcdee0c |
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/workflows/auto-label-pr.yml
vendored
2
.github/workflows/auto-label-pr.yml
vendored
@@ -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' }
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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_++;
|
||||
|
||||
@@ -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_++;
|
||||
}
|
||||
|
||||
@@ -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]))...);
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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, ¤t_vertex_x, ¤t_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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
86
esphome/components/epaper_spi/epaper_spi_ssd1677.cpp
Normal file
86
esphome/components/epaper_spi/epaper_spi_ssd1677.cpp
Normal 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
|
||||
25
esphome/components/epaper_spi/epaper_spi_ssd1677.h
Normal file
25
esphome/components/epaper_spi/epaper_spi_ssd1677.h
Normal 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
|
||||
42
esphome/components/epaper_spi/models/ssd1677.py
Normal file
42
esphome/components/epaper_spi/models/ssd1677.py
Normal 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,
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ¶ms = 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)...);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
|
||||
@@ -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)...);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -107,9 +107,32 @@ FILTER_PLATFORMIO_LINES = [
|
||||
r"Warning: DEPRECATED: 'esptool.py' is deprecated. Please use 'esptool' instead. The '.py' suffix will be removed in a future major release.",
|
||||
r"Warning: esp-idf-size exited with code 2",
|
||||
r"esp_idf_size: error: unrecognized arguments: --ng",
|
||||
r"Package configuration completed successfully",
|
||||
]
|
||||
|
||||
|
||||
class PlatformioLogFilter(logging.Filter):
|
||||
"""Filter to suppress noisy platformio log messages."""
|
||||
|
||||
_PATTERN = re.compile(
|
||||
r"|".join(r"(?:" + pattern + r")" for pattern in FILTER_PLATFORMIO_LINES)
|
||||
)
|
||||
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
# Only filter messages from platformio-related loggers
|
||||
if "platformio" not in record.name.lower():
|
||||
return True
|
||||
return self._PATTERN.match(record.getMessage()) is None
|
||||
|
||||
|
||||
def patch_platformio_logging() -> None:
|
||||
"""Add filter to root logger handlers to suppress noisy platformio messages."""
|
||||
root_logger = logging.getLogger()
|
||||
for handler in root_logger.handlers:
|
||||
if not any(isinstance(f, PlatformioLogFilter) for f in handler.filters):
|
||||
handler.addFilter(PlatformioLogFilter())
|
||||
|
||||
|
||||
def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ["PLATFORMIO_FORCE_COLOR"] = "true"
|
||||
os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute())
|
||||
@@ -130,6 +153,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
|
||||
patch_structhash()
|
||||
patch_file_downloader()
|
||||
if not CORE.verbose:
|
||||
patch_platformio_logging()
|
||||
return run_external_command(platformio.__main__.main, *cmd, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,7 @@ display:
|
||||
update_interval: never
|
||||
|
||||
lvgl:
|
||||
update_when_display_idle: true
|
||||
displays:
|
||||
- tft_display
|
||||
- second_display
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for platformio_api.py path functions."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
@@ -670,3 +671,152 @@ def test_process_stacktrace_bad_alloc(
|
||||
assert "Memory allocation of 512 bytes failed at 40201234" in caplog.text
|
||||
mock_decode_pc.assert_called_once_with(config, "40201234")
|
||||
assert state is False
|
||||
|
||||
|
||||
def test_platformio_log_filter_allows_non_platformio_messages() -> None:
|
||||
"""Test that non-platformio logger messages are allowed through."""
|
||||
log_filter = platformio_api.PlatformioLogFilter()
|
||||
record = logging.LogRecord(
|
||||
name="esphome.core",
|
||||
level=logging.INFO,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg="Some esphome message",
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
assert log_filter.filter(record) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"msg",
|
||||
[
|
||||
pytest.param(
|
||||
"Verbose mode can be enabled via `-v, --verbose` option", id="verbose_mode"
|
||||
),
|
||||
pytest.param("Found 5 compatible libraries", id="found_5_libs"),
|
||||
pytest.param("Found 123 compatible libraries", id="found_123_libs"),
|
||||
pytest.param("Building in release mode", id="release_mode"),
|
||||
pytest.param("Building in debug mode", id="debug_mode"),
|
||||
pytest.param("Merged 2 ELF section", id="merged_elf"),
|
||||
pytest.param("esptool.py v4.7.0", id="esptool_py"),
|
||||
pytest.param("esptool v4.8.1", id="esptool"),
|
||||
pytest.param("PLATFORM: espressif32 @ 6.4.0", id="platform"),
|
||||
pytest.param("Using cache: /path/to/cache", id="cache"),
|
||||
pytest.param("Package configuration completed successfully", id="pkg_config"),
|
||||
pytest.param("Scanning dependencies...", id="scanning_deps"),
|
||||
pytest.param("Installing dependencies", id="installing_deps"),
|
||||
pytest.param(
|
||||
"Library Manager: Already installed, built-in library", id="lib_manager"
|
||||
),
|
||||
pytest.param(
|
||||
"Memory Usage -> https://bit.ly/pio-memory-usage", id="memory_usage"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_platformio_log_filter_blocks_noisy_messages(msg: str) -> None:
|
||||
"""Test that noisy platformio messages are filtered out."""
|
||||
log_filter = platformio_api.PlatformioLogFilter()
|
||||
record = logging.LogRecord(
|
||||
name="platformio.builder",
|
||||
level=logging.INFO,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg=msg,
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
assert log_filter.filter(record) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"msg",
|
||||
[
|
||||
pytest.param("Compiling .pio/build/test/src/main.cpp.o", id="compiling"),
|
||||
pytest.param("Linking .pio/build/test/firmware.elf", id="linking"),
|
||||
pytest.param("Error: something went wrong", id="error"),
|
||||
pytest.param("warning: unused variable", id="warning"),
|
||||
],
|
||||
)
|
||||
def test_platformio_log_filter_allows_other_platformio_messages(msg: str) -> None:
|
||||
"""Test that non-noisy platformio messages are allowed through."""
|
||||
log_filter = platformio_api.PlatformioLogFilter()
|
||||
record = logging.LogRecord(
|
||||
name="platformio.builder",
|
||||
level=logging.INFO,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg=msg,
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
assert log_filter.filter(record) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"logger_name",
|
||||
[
|
||||
pytest.param("PLATFORMIO.builder", id="upper"),
|
||||
pytest.param("PlatformIO.core", id="mixed"),
|
||||
pytest.param("platformio.run", id="lower"),
|
||||
],
|
||||
)
|
||||
def test_platformio_log_filter_case_insensitive_logger_name(logger_name: str) -> None:
|
||||
"""Test that platformio logger name matching is case insensitive."""
|
||||
log_filter = platformio_api.PlatformioLogFilter()
|
||||
record = logging.LogRecord(
|
||||
name=logger_name,
|
||||
level=logging.INFO,
|
||||
pathname="",
|
||||
lineno=0,
|
||||
msg="Found 5 compatible libraries",
|
||||
args=(),
|
||||
exc_info=None,
|
||||
)
|
||||
assert log_filter.filter(record) is False
|
||||
|
||||
|
||||
def test_patch_platformio_logging_adds_filter() -> None:
|
||||
"""Test that patch_platformio_logging adds filter to all handlers."""
|
||||
test_handler = logging.StreamHandler()
|
||||
root_logger = logging.getLogger()
|
||||
original_handlers = root_logger.handlers.copy()
|
||||
|
||||
try:
|
||||
root_logger.addHandler(test_handler)
|
||||
|
||||
assert not any(
|
||||
isinstance(f, platformio_api.PlatformioLogFilter)
|
||||
for f in test_handler.filters
|
||||
)
|
||||
|
||||
platformio_api.patch_platformio_logging()
|
||||
|
||||
assert any(
|
||||
isinstance(f, platformio_api.PlatformioLogFilter)
|
||||
for f in test_handler.filters
|
||||
)
|
||||
finally:
|
||||
root_logger.handlers = original_handlers
|
||||
|
||||
|
||||
def test_patch_platformio_logging_no_duplicate_filters() -> None:
|
||||
"""Test that patch_platformio_logging doesn't add duplicate filters."""
|
||||
test_handler = logging.StreamHandler()
|
||||
root_logger = logging.getLogger()
|
||||
original_handlers = root_logger.handlers.copy()
|
||||
|
||||
try:
|
||||
root_logger.addHandler(test_handler)
|
||||
|
||||
platformio_api.patch_platformio_logging()
|
||||
platformio_api.patch_platformio_logging()
|
||||
|
||||
filter_count = sum(
|
||||
1
|
||||
for f in test_handler.filters
|
||||
if isinstance(f, platformio_api.PlatformioLogFilter)
|
||||
)
|
||||
assert filter_count == 1
|
||||
finally:
|
||||
root_logger.handlers = original_handlers
|
||||
|
||||
Reference in New Issue
Block a user