mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 17:18:40 +00:00
Merge remote-tracking branch 'upstream/dev' into 20250915-wifi-info-use-callbacks
This commit is contained in:
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
@@ -484,6 +484,7 @@ esphome/components/template/datetime/* @rfdarter
|
||||
esphome/components/template/event/* @nohat
|
||||
esphome/components/template/fan/* @ssieb
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/thermopro_ble/* @sittner
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @esphome/core
|
||||
esphome/components/tinyusb/* @kbx81
|
||||
|
||||
+1
-1
@@ -1319,7 +1319,7 @@ def parse_args(argv):
|
||||
"clean-all", help="Clean all build and platform files."
|
||||
)
|
||||
parser_clean_all.add_argument(
|
||||
"configuration", help="Your YAML configuration directory.", nargs="*"
|
||||
"configuration", help="Your YAML file or configuration directory.", nargs="*"
|
||||
)
|
||||
|
||||
parser_dashboard = subparsers.add_parser(
|
||||
|
||||
@@ -87,7 +87,7 @@ void AbsoluteHumidityComponent::loop() {
|
||||
break;
|
||||
default:
|
||||
this->publish_state(NAN);
|
||||
this->status_set_error("Invalid saturation vapor pressure equation selection!");
|
||||
this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
|
||||
|
||||
@@ -83,7 +83,7 @@ void AHT10Component::setup() {
|
||||
void AHT10Component::restart_read_() {
|
||||
if (this->read_count_ == AHT10_ATTEMPTS) {
|
||||
this->read_count_ = 0;
|
||||
this->status_set_error("Reading timed out");
|
||||
this->status_set_error(LOG_STR("Reading timed out"));
|
||||
return;
|
||||
}
|
||||
this->read_count_++;
|
||||
|
||||
@@ -85,6 +85,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||
CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
@@ -537,9 +538,24 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_condition("api.connected", APIConnectedCondition, {})
|
||||
API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(APIServer),
|
||||
cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable(
|
||||
cv.boolean
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
|
||||
)
|
||||
async def api_connected_to_code(config, condition_id, template_arg, args):
|
||||
return cg.new_Pvariable(condition_id, template_arg)
|
||||
var = cg.new_Pvariable(condition_id, template_arg)
|
||||
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
|
||||
cg.add(var.set_state_subscription_only(templ))
|
||||
return var
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
|
||||
@@ -90,8 +90,8 @@ static const int CAMERA_STOP_STREAM = 5000;
|
||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
|
||||
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
|
||||
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
|
||||
auto noise_ctx = parent->get_noise_ctx();
|
||||
if (noise_ctx->has_psk()) {
|
||||
auto &noise_ctx = parent->get_noise_ctx();
|
||||
if (noise_ctx.has_psk()) {
|
||||
this->helper_ =
|
||||
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
|
||||
} else {
|
||||
@@ -1451,8 +1451,11 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
#ifdef USE_AREAS
|
||||
resp.set_suggested_area(StringRef(App.get_area()));
|
||||
#endif
|
||||
// mac_address must store temporary string - will be valid during send_message call
|
||||
std::string mac_address = get_mac_address_pretty();
|
||||
// Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
|
||||
char mac_address[18];
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
format_mac_addr_upper(mac, mac_address);
|
||||
resp.set_mac_address(StringRef(mac_address));
|
||||
|
||||
resp.set_esphome_version(ESPHOME_VERSION_REF);
|
||||
@@ -1493,8 +1496,9 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
|
||||
// bt_mac must store temporary string - will be valid during send_message call
|
||||
std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
|
||||
// Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
|
||||
char bluetooth_mac[18];
|
||||
bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac);
|
||||
resp.set_bluetooth_mac_address(StringRef(bluetooth_mac));
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
|
||||
@@ -84,9 +84,7 @@ class APIFrameHelper {
|
||||
public:
|
||||
APIFrameHelper() = default;
|
||||
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||
: socket_owned_(std::move(socket)), client_info_(client_info) {
|
||||
socket_ = socket_owned_.get();
|
||||
}
|
||||
: socket_(std::move(socket)), client_info_(client_info) {}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
virtual APIError init() = 0;
|
||||
virtual APIError loop();
|
||||
@@ -149,9 +147,8 @@ class APIFrameHelper {
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
||||
const std::string &info, StateEnum &state, StateEnum failed_state);
|
||||
|
||||
// Pointers first (4 bytes each)
|
||||
socket::Socket *socket_{nullptr};
|
||||
std::unique_ptr<socket::Socket> socket_owned_;
|
||||
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
|
||||
// Common state enum for all frame helpers
|
||||
// Note: Not all states are used by all implementations
|
||||
|
||||
@@ -239,12 +239,13 @@ APIError APINoiseFrameHelper::state_action_() {
|
||||
}
|
||||
if (state_ == State::SERVER_HELLO) {
|
||||
// send server hello
|
||||
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
|
||||
const std::string &name = App.get_name();
|
||||
const std::string &mac = get_mac_address();
|
||||
char mac[mac_len];
|
||||
get_mac_address_into_buffer(mac);
|
||||
|
||||
// Calculate positions and sizes
|
||||
size_t name_len = name.size() + 1; // including null terminator
|
||||
size_t mac_len = mac.size() + 1; // including null terminator
|
||||
size_t name_offset = 1;
|
||||
size_t mac_offset = name_offset + name_len;
|
||||
size_t total_size = 1 + name_len + mac_len;
|
||||
@@ -257,7 +258,7 @@ APIError APINoiseFrameHelper::state_action_() {
|
||||
// node name, terminated by null byte
|
||||
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
|
||||
std::memcpy(msg.get() + mac_offset, mac, mac_len);
|
||||
|
||||
aerr = write_frame_(msg.get(), total_size);
|
||||
if (aerr != APIError::OK)
|
||||
@@ -527,7 +528,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
const auto &psk = ctx_->get_psk();
|
||||
const auto &psk = this->ctx_.get_psk();
|
||||
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
|
||||
APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
|
||||
@@ -9,9 +9,8 @@ namespace esphome::api {
|
||||
|
||||
class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
|
||||
const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) {
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
@@ -41,8 +40,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
NoiseCipherState *send_cipher_{nullptr};
|
||||
NoiseCipherState *recv_cipher_{nullptr};
|
||||
|
||||
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
|
||||
std::shared_ptr<APINoiseContext> ctx_;
|
||||
// Reference to noise context (4 bytes on 32-bit)
|
||||
APINoiseContext &ctx_;
|
||||
|
||||
// Vector (12 bytes on 32-bit)
|
||||
std::vector<uint8_t> prologue_;
|
||||
|
||||
@@ -227,8 +227,8 @@ void APIServer::dump_config() {
|
||||
" Max connections: %u",
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
|
||||
if (!this->noise_ctx_->has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
|
||||
if (!this->noise_ctx_.has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Supports encryption: YES");
|
||||
}
|
||||
#else
|
||||
@@ -493,7 +493,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
ESP_LOGW(TAG, "Key set in YAML");
|
||||
return false;
|
||||
#else
|
||||
auto &old_psk = this->noise_ctx_->get_psk();
|
||||
auto &old_psk = this->noise_ctx_.get_psk();
|
||||
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
|
||||
ESP_LOGW(TAG, "New PSK matches old");
|
||||
return true;
|
||||
@@ -528,7 +528,18 @@ void APIServer::request_time() {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool APIServer::is_connected() const { return !this->clients_.empty(); }
|
||||
bool APIServer::is_connected(bool state_subscription_only) const {
|
||||
if (!state_subscription_only) {
|
||||
return !this->clients_.empty();
|
||||
}
|
||||
|
||||
for (const auto &client : this->clients_) {
|
||||
if (client->flags_.state_subscription) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void APIServer::on_shutdown() {
|
||||
this->shutting_down_ = true;
|
||||
|
||||
@@ -54,8 +54,8 @@ class APIServer : public Component, public Controller {
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
bool clear_noise_psk(bool make_active = true);
|
||||
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
|
||||
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
|
||||
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
@@ -150,7 +150,7 @@ class APIServer : public Component, public Controller {
|
||||
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
|
||||
#endif
|
||||
|
||||
bool is_connected() const;
|
||||
bool is_connected(bool state_subscription_only = false) const;
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
struct HomeAssistantStateSubscription {
|
||||
@@ -228,7 +228,7 @@ class APIServer : public Component, public Controller {
|
||||
// 7 bytes used, 1 byte padding
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
|
||||
APINoiseContext noise_ctx_;
|
||||
ESPPreferenceObject noise_pref_;
|
||||
#endif // USE_API_NOISE
|
||||
};
|
||||
@@ -236,8 +236,11 @@ class APIServer : public Component, public Controller {
|
||||
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
|
||||
TEMPLATABLE_VALUE(bool, state_subscription_only)
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_api_server->is_connected(); }
|
||||
bool check(const Ts &...x) override {
|
||||
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() {
|
||||
i2c::ErrorCode result_code =
|
||||
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
|
||||
if (result_code != i2c::ERROR_OK) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,11 +130,13 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
|
||||
return flags;
|
||||
}
|
||||
|
||||
std::string get_bluetooth_mac_address_pretty() {
|
||||
void get_bluetooth_mac_address_pretty(std::span<char, 18> output) {
|
||||
const uint8_t *mac = esp_bt_dev_get_address();
|
||||
char buf[18];
|
||||
format_mac_addr_upper(mac, buf);
|
||||
return std::string(buf);
|
||||
if (mac != nullptr) {
|
||||
format_mac_addr_upper(mac, output.data());
|
||||
} else {
|
||||
output[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -100,18 +100,18 @@ void BME280Component::setup() {
|
||||
|
||||
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x60) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed(BME280_ERROR_WRONG_CHIP_ID);
|
||||
this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a soft reset.
|
||||
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
|
||||
this->mark_failed("Reset failed");
|
||||
this->mark_failed(LOG_STR("Reset failed"));
|
||||
return;
|
||||
}
|
||||
// Wait until the NVM data has finished loading.
|
||||
@@ -120,12 +120,12 @@ void BME280Component::setup() {
|
||||
do { // NOLINT
|
||||
delay(2);
|
||||
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
|
||||
this->mark_failed("Error reading status register");
|
||||
this->mark_failed(LOG_STR("Error reading status register"));
|
||||
return;
|
||||
}
|
||||
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
|
||||
if (status & BME280_STATUS_IM_UPDATE) {
|
||||
this->mark_failed("Timeout loading NVM");
|
||||
this->mark_failed(LOG_STR("Timeout loading NVM"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,26 +153,26 @@ void BME280Component::setup() {
|
||||
|
||||
uint8_t humid_control_val = 0;
|
||||
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
|
||||
this->mark_failed("Read humidity control");
|
||||
this->mark_failed(LOG_STR("Read humidity control"));
|
||||
return;
|
||||
}
|
||||
humid_control_val &= ~0b00000111;
|
||||
humid_control_val |= this->humidity_oversampling_ & 0b111;
|
||||
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
|
||||
this->mark_failed("Write humidity control");
|
||||
this->mark_failed(LOG_STR("Write humidity control"));
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t config_register = 0;
|
||||
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed("Read config");
|
||||
this->mark_failed(LOG_STR("Read config"));
|
||||
return;
|
||||
}
|
||||
config_register &= ~0b11111100;
|
||||
config_register |= 0b101 << 5; // 1000 ms standby time
|
||||
config_register |= (this->iir_filter_ & 0b111) << 2;
|
||||
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed("Write config");
|
||||
this->mark_failed(LOG_STR("Write config"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,23 +65,23 @@ void BMP280Component::setup() {
|
||||
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x58) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID);
|
||||
this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a soft reset.
|
||||
if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
|
||||
this->mark_failed("Reset failed");
|
||||
this->mark_failed(LOG_STR("Reset failed"));
|
||||
return;
|
||||
}
|
||||
// Wait until the NVM data has finished loading.
|
||||
@@ -90,12 +90,12 @@ void BMP280Component::setup() {
|
||||
do {
|
||||
delay(2);
|
||||
if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) {
|
||||
this->mark_failed("Error reading status register");
|
||||
this->mark_failed(LOG_STR("Error reading status register"));
|
||||
return;
|
||||
}
|
||||
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
|
||||
if (status & BMP280_STATUS_IM_UPDATE) {
|
||||
this->mark_failed("Timeout loading NVM");
|
||||
this->mark_failed(LOG_STR("Timeout loading NVM"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,14 +116,14 @@ void BMP280Component::setup() {
|
||||
|
||||
uint8_t config_register = 0;
|
||||
if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed("Read config");
|
||||
this->mark_failed(LOG_STR("Read config"));
|
||||
return;
|
||||
}
|
||||
config_register &= ~0b11111100;
|
||||
config_register |= 0b000 << 5; // 0.5 ms standby time
|
||||
config_register |= (this->iir_filter_ & 0b111) << 2;
|
||||
if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed("Write config");
|
||||
this->mark_failed(LOG_STR("Write config"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ Camera *Camera::global_camera = nullptr;
|
||||
|
||||
Camera::Camera() {
|
||||
if (global_camera != nullptr) {
|
||||
this->status_set_error("Multiple cameras are configured, but only one is supported.");
|
||||
this->status_set_error(LOG_STR("Multiple cameras are configured, but only one is supported."));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@ void CST816Touchscreen::continue_setup_() {
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unknown chip ID: 0x%02X", this->chip_id_);
|
||||
this->status_set_error("Unknown chip ID");
|
||||
this->status_set_error(LOG_STR("Unknown chip ID"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->write_byte(REG_IRQ_CTL, IRQ_EN_MOTION);
|
||||
} else if (!this->skip_probe_) {
|
||||
this->status_set_error("Failed to read chip id");
|
||||
this->status_set_error(LOG_STR("Failed to read chip id"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const char *EPaperBase::epaper_state_to_string_() {
|
||||
|
||||
void EPaperBase::setup() {
|
||||
if (!this->init_buffer_(this->buffer_length_)) {
|
||||
this->mark_failed("Failed to initialise buffer");
|
||||
this->mark_failed(LOG_STR("Failed to initialise buffer"));
|
||||
return;
|
||||
}
|
||||
this->setup_pins_();
|
||||
@@ -246,7 +246,7 @@ void EPaperBase::initialise_() {
|
||||
auto length = this->init_sequence_length_;
|
||||
while (index != length) {
|
||||
if (length - index < 2) {
|
||||
this->mark_failed("Malformed init sequence");
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
const uint8_t cmd = sequence[index++];
|
||||
|
||||
@@ -88,7 +88,7 @@ void Esp32HostedUpdate::perform(bool force) {
|
||||
hasher.add(this->firmware_data_, this->firmware_size_);
|
||||
hasher.calculate();
|
||||
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
|
||||
this->status_set_error("SHA256 verification failed");
|
||||
this->status_set_error(LOG_STR("SHA256 verification failed"));
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ void Esp32HostedUpdate::perform(bool force) {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error("Failed to begin OTA");
|
||||
this->status_set_error(LOG_STR("Failed to begin OTA"));
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
@@ -121,7 +121,7 @@ void Esp32HostedUpdate::perform(bool force) {
|
||||
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
|
||||
esp_hosted_slave_ota_end(); // NOLINT
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error("Failed to write OTA data");
|
||||
this->status_set_error(LOG_STR("Failed to write OTA data"));
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
@@ -134,7 +134,7 @@ void Esp32HostedUpdate::perform(bool force) {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error("Failed to end OTA");
|
||||
this->status_set_error(LOG_STR("Failed to end OTA"));
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
@@ -144,7 +144,7 @@ void Esp32HostedUpdate::perform(bool force) {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
|
||||
this->state_ = prev_state;
|
||||
this->status_set_error("Failed to activate OTA");
|
||||
this->status_set_error(LOG_STR("Failed to activate OTA"));
|
||||
this->publish_state();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ void EspLdo::setup() {
|
||||
config.flags.adjustable = this->adjustable_;
|
||||
auto err = esp_ldo_acquire_channel(&config, &this->handle_);
|
||||
if (err != ESP_OK) {
|
||||
auto msg = str_sprintf("Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_);
|
||||
this->mark_failed(msg.c_str());
|
||||
ESP_LOGE(TAG, "Failed to acquire LDO channel %d with voltage %fV", this->channel_, this->voltage_);
|
||||
this->mark_failed(LOG_STR("Failed to acquire LDO channel"));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Acquired LDO channel %d with voltage %fV", this->channel_, this->voltage_);
|
||||
}
|
||||
|
||||
@@ -383,6 +383,7 @@ async def to_code(config):
|
||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||
|
||||
if CONF_MANUAL_IP in config:
|
||||
cg.add_define("USE_ETHERNET_MANUAL_IP")
|
||||
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
|
||||
|
||||
# Add compile-time define for PHY types with specific code
|
||||
|
||||
@@ -553,11 +553,14 @@ void EthernetComponent::start_connect_() {
|
||||
}
|
||||
|
||||
esp_netif_ip_info_t info;
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
info.ip = this->manual_ip_->static_ip;
|
||||
info.gw = this->manual_ip_->gateway;
|
||||
info.netmask = this->manual_ip_->subnet;
|
||||
} else {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
info.ip.addr = 0;
|
||||
info.gw.addr = 0;
|
||||
info.netmask.addr = 0;
|
||||
@@ -578,6 +581,7 @@ void EthernetComponent::start_connect_() {
|
||||
err = esp_netif_set_ip_info(this->eth_netif_, &info);
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
if (this->manual_ip_.has_value()) {
|
||||
LwIPLock lock;
|
||||
if (this->manual_ip_->dns1.is_set()) {
|
||||
@@ -590,7 +594,9 @@ void EthernetComponent::start_connect_() {
|
||||
d = this->manual_ip_->dns2;
|
||||
dns_setserver(1, &d);
|
||||
}
|
||||
} else {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
err = esp_netif_dhcpc_start(this->eth_netif_);
|
||||
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
|
||||
ESPHL_ERROR_CHECK(err, "DHCPC start error");
|
||||
@@ -688,7 +694,9 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl
|
||||
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
||||
#endif
|
||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
|
||||
@@ -82,7 +82,9 @@ class EthernetComponent : public Component {
|
||||
void add_phy_register(PHYRegister register_value);
|
||||
#endif
|
||||
void set_type(EthernetType type);
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
#endif
|
||||
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
|
||||
|
||||
network::IPAddresses get_ip_addresses();
|
||||
@@ -137,7 +139,9 @@ class EthernetComponent : public Component {
|
||||
uint8_t mdc_pin_{23};
|
||||
uint8_t mdio_pin_{18};
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
optional<ManualIP> manual_ip_{};
|
||||
#endif
|
||||
uint32_t connect_begin_;
|
||||
|
||||
// Group all uint8_t types together (enums and bools)
|
||||
|
||||
@@ -36,20 +36,20 @@ void GDK101Component::setup() {
|
||||
uint8_t data[2];
|
||||
// first, reset the sensor
|
||||
if (!this->reset_sensor_(data)) {
|
||||
this->status_set_error("Reset failed!");
|
||||
this->status_set_error(LOG_STR("Reset failed!"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// sensor should acknowledge success of the reset procedure
|
||||
if (data[0] != 1) {
|
||||
this->status_set_error("Reset not acknowledged!");
|
||||
this->status_set_error(LOG_STR("Reset not acknowledged!"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(10);
|
||||
// read firmware version
|
||||
if (!this->read_fw_version_(data)) {
|
||||
this->status_set_error("Failed to read firmware version");
|
||||
this->status_set_error(LOG_STR("Failed to read firmware version"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,13 +79,13 @@ void GT911Touchscreen::setup_internal_() {
|
||||
}
|
||||
}
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->mark_failed("Calibration error");
|
||||
this->mark_failed(LOG_STR("Calibration error"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
this->setup_done_ = true;
|
||||
|
||||
@@ -29,7 +29,7 @@ void HttpRequestUpdate::setup() {
|
||||
this->publish_state();
|
||||
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
|
||||
this->state_ = update::UPDATE_STATE_AVAILABLE;
|
||||
this->status_set_error("Failed to install firmware");
|
||||
this->status_set_error(LOG_STR("Failed to install firmware"));
|
||||
this->publish_state();
|
||||
}
|
||||
});
|
||||
@@ -51,7 +51,7 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
|
||||
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error("Failed to fetch manifest"); });
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); });
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,8 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
if (data == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error("Failed to allocate memory for manifest"); });
|
||||
this_update->defer(
|
||||
[this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); });
|
||||
container->end();
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
@@ -123,7 +124,7 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
// Defer to main loop to avoid race condition on component_state_ read-modify-write
|
||||
this_update->defer([this_update]() { this_update->status_set_error("Failed to parse manifest JSON"); });
|
||||
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); });
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,18 +47,20 @@ MULTI_CONF = True
|
||||
|
||||
|
||||
def _bus_declare_type(value):
|
||||
if CORE.is_esp32:
|
||||
return cv.declare_id(IDFI2CBus)(value)
|
||||
if CORE.using_arduino:
|
||||
return cv.declare_id(ArduinoI2CBus)(value)
|
||||
if CORE.using_esp_idf:
|
||||
return cv.declare_id(IDFI2CBus)(value)
|
||||
if CORE.using_zephyr:
|
||||
return cv.declare_id(ZephyrI2CBus)(value)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
if CORE.using_esp_idf:
|
||||
return cv.require_framework_version(esp_idf=cv.Version(5, 4, 2))(config)
|
||||
if CORE.is_esp32:
|
||||
return cv.require_framework_version(
|
||||
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
|
||||
)(config)
|
||||
return config
|
||||
|
||||
|
||||
@@ -67,12 +69,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): _bus_declare_type,
|
||||
cv.Optional(CONF_SDA, default="SDA"): pins.internal_gpio_pin_number,
|
||||
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32_idf=True): cv.All(
|
||||
cv.only_with_esp_idf, cv.boolean
|
||||
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All(
|
||||
cv.only_on_esp32, cv.boolean
|
||||
),
|
||||
cv.Optional(CONF_SCL, default="SCL"): pins.internal_gpio_pin_number,
|
||||
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All(
|
||||
cv.only_with_esp_idf, cv.boolean
|
||||
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All(
|
||||
cv.only_on_esp32, cv.boolean
|
||||
),
|
||||
cv.SplitDefault(
|
||||
CONF_FREQUENCY,
|
||||
@@ -151,7 +153,7 @@ async def to_code(config):
|
||||
cg.add(var.set_scan(config[CONF_SCAN]))
|
||||
if CONF_TIMEOUT in config:
|
||||
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
|
||||
if CORE.using_arduino:
|
||||
if CORE.using_arduino and not CORE.is_esp32:
|
||||
cg.add_library("Wire", None)
|
||||
|
||||
|
||||
@@ -248,14 +250,16 @@ def final_validate_device_schema(
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
{
|
||||
"i2c_bus_arduino.cpp": {
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP8266_ARDUINO,
|
||||
PlatformFramework.RP2040_ARDUINO,
|
||||
PlatformFramework.BK72XX_ARDUINO,
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
"i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
|
||||
"i2c_bus_esp_idf.cpp": {
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
"i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
|
||||
#include "i2c_bus_arduino.h"
|
||||
#include <Arduino.h>
|
||||
@@ -15,16 +15,7 @@ static const char *const TAG = "i2c.arduino";
|
||||
void ArduinoI2CBus::setup() {
|
||||
recover_();
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
static uint8_t next_bus_num = 0;
|
||||
if (next_bus_num == 0) {
|
||||
wire_ = &Wire;
|
||||
} else {
|
||||
wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
this->port_ = next_bus_num;
|
||||
next_bus_num++;
|
||||
#elif defined(USE_ESP8266)
|
||||
#if defined(USE_ESP8266)
|
||||
wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
#elif defined(USE_RP2040)
|
||||
static bool first = true;
|
||||
@@ -54,10 +45,7 @@ void ArduinoI2CBus::set_pins_and_clock_() {
|
||||
wire_->begin(static_cast<int>(sda_pin_), static_cast<int>(scl_pin_));
|
||||
#endif
|
||||
if (timeout_ > 0) { // if timeout specified in yaml
|
||||
#if defined(USE_ESP32)
|
||||
// https://github.com/espressif/arduino-esp32/blob/master/libraries/Wire/src/Wire.cpp
|
||||
wire_->setTimeOut(timeout_ / 1000); // unit: ms
|
||||
#elif defined(USE_ESP8266)
|
||||
#if defined(USE_ESP8266)
|
||||
// https://github.com/esp8266/Arduino/blob/master/libraries/Wire/Wire.h
|
||||
wire_->setClockStretchLimit(timeout_); // unit: us
|
||||
#elif defined(USE_RP2040)
|
||||
@@ -76,9 +64,7 @@ void ArduinoI2CBus::dump_config() {
|
||||
" Frequency: %u Hz",
|
||||
this->sda_pin_, this->scl_pin_, this->frequency_);
|
||||
if (timeout_ > 0) {
|
||||
#if defined(USE_ESP32)
|
||||
ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000);
|
||||
#elif defined(USE_ESP8266)
|
||||
#if defined(USE_ESP8266)
|
||||
ESP_LOGCONFIG(TAG, " Timeout: %u us", this->timeout_);
|
||||
#elif defined(USE_RP2040)
|
||||
ESP_LOGCONFIG(TAG, " Timeout: %u ms", this->timeout_ / 1000);
|
||||
@@ -275,4 +261,4 @@ void ArduinoI2CBus::recover_() {
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
#endif // defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
|
||||
#include <Wire.h>
|
||||
#include "esphome/core/component.h"
|
||||
@@ -29,7 +29,7 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
|
||||
void set_frequency(uint32_t frequency) { frequency_ = frequency; }
|
||||
void set_timeout(uint32_t timeout) { timeout_ = timeout; }
|
||||
|
||||
int get_port() const override { return this->port_; }
|
||||
int get_port() const override { return 0; }
|
||||
|
||||
private:
|
||||
void recover_();
|
||||
@@ -37,7 +37,6 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
|
||||
RecoveryCode recovery_result_;
|
||||
|
||||
protected:
|
||||
int8_t port_{-1};
|
||||
TwoWire *wire_;
|
||||
uint8_t sda_pin_;
|
||||
uint8_t scl_pin_;
|
||||
@@ -49,4 +48,4 @@ class ArduinoI2CBus : public InternalI2CBus, public Component {
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
||||
#endif // defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "i2c_bus_esp_idf.h"
|
||||
|
||||
@@ -299,4 +299,4 @@ void IDFI2CBus::recover_() {
|
||||
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
#endif // USE_ESP_IDF
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "i2c_bus.h"
|
||||
@@ -53,4 +53,4 @@ class IDFI2CBus : public InternalI2CBus, public Component {
|
||||
} // namespace i2c
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -23,6 +23,9 @@ void LightState::setup() {
|
||||
effect->init_internal(this);
|
||||
}
|
||||
|
||||
// Start with loop disabled if idle - respects any effects/transitions set up during initialization
|
||||
this->disable_loop_if_idle_();
|
||||
|
||||
// When supported color temperature range is known, initialize color temperature setting within bounds.
|
||||
auto traits = this->get_traits();
|
||||
float min_mireds = traits.get_min_mireds();
|
||||
@@ -125,6 +128,9 @@ void LightState::loop() {
|
||||
this->is_transformer_active_ = false;
|
||||
this->transformer_ = nullptr;
|
||||
this->target_state_reached_callback_.call();
|
||||
|
||||
// Disable loop if idle (no transformer and no effect)
|
||||
this->disable_loop_if_idle_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +138,8 @@ void LightState::loop() {
|
||||
if (this->next_write_) {
|
||||
this->next_write_ = false;
|
||||
this->output_->write_state(this);
|
||||
// Disable loop if idle (no transformer and no effect)
|
||||
this->disable_loop_if_idle_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +235,8 @@ void LightState::start_effect_(uint32_t effect_index) {
|
||||
this->active_effect_index_ = effect_index;
|
||||
auto *effect = this->get_active_effect_();
|
||||
effect->start_internal();
|
||||
// Enable loop while effect is active
|
||||
this->enable_loop();
|
||||
}
|
||||
LightEffect *LightState::get_active_effect_() {
|
||||
if (this->active_effect_index_ == 0) {
|
||||
@@ -241,6 +251,8 @@ void LightState::stop_effect_() {
|
||||
effect->stop();
|
||||
}
|
||||
this->active_effect_index_ = 0;
|
||||
// Disable loop if idle (no effect and no transformer)
|
||||
this->disable_loop_if_idle_();
|
||||
}
|
||||
|
||||
void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
|
||||
@@ -250,6 +262,8 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng
|
||||
if (set_remote_values) {
|
||||
this->remote_values = target;
|
||||
}
|
||||
// Enable loop while transition is active
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
|
||||
@@ -265,6 +279,8 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b
|
||||
if (set_remote_values) {
|
||||
this->remote_values = target;
|
||||
};
|
||||
// Enable loop while flash is active
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
|
||||
@@ -276,6 +292,14 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
|
||||
}
|
||||
this->output_->update_state(this);
|
||||
this->next_write_ = true;
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void LightState::disable_loop_if_idle_() {
|
||||
// Only disable loop if both transformer and effect are inactive, and no pending writes
|
||||
if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void LightState::save_remote_values_() {
|
||||
|
||||
@@ -255,6 +255,9 @@ class LightState : public EntityBase, public Component {
|
||||
/// Internal method to save the current remote_values to the preferences
|
||||
void save_remote_values_();
|
||||
|
||||
/// Disable loop if neither transformer nor effect is active
|
||||
void disable_loop_if_idle_();
|
||||
|
||||
/// Store the output to allow effects to have more access.
|
||||
LightOutput *output_;
|
||||
/// The currently active transformer for this light (transition/flash).
|
||||
|
||||
@@ -365,8 +365,10 @@ async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
|
||||
try:
|
||||
uart_selection(USB_SERIAL_JTAG)
|
||||
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")
|
||||
|
||||
@@ -65,7 +65,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
uint16_t buffer_at = 0; // Initialize buffer position
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at,
|
||||
MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
this->write_msg_(console_buffer);
|
||||
// Add newline if platform needs it (ESP32 doesn't add via write_msg_)
|
||||
this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE);
|
||||
this->write_msg_(console_buffer, buffer_at);
|
||||
}
|
||||
|
||||
// Reset the recursion guard for this task
|
||||
@@ -131,18 +133,19 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
|
||||
// Save the offset before calling format_log_to_buffer_with_terminator_
|
||||
// since it will increment tx_buffer_at_ to the end of the formatted string
|
||||
uint32_t msg_start = this->tx_buffer_at_;
|
||||
uint16_t msg_start = this->tx_buffer_at_;
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
|
||||
&this->tx_buffer_at_, this->tx_buffer_size_);
|
||||
|
||||
// Write to console and send callback starting at the msg_start
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_ + msg_start);
|
||||
}
|
||||
size_t msg_length =
|
||||
uint16_t msg_length =
|
||||
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
|
||||
|
||||
// Callbacks get message first (before console write)
|
||||
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
|
||||
|
||||
// Write to console starting at the msg_start
|
||||
this->write_tx_buffer_to_console_(msg_start, &msg_length);
|
||||
|
||||
global_recursion_guard_ = false;
|
||||
}
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
@@ -209,9 +212,7 @@ void Logger::process_messages_() {
|
||||
// This ensures all log messages appear on the console in a clean, serialized manner
|
||||
// Note: Messages may appear slightly out of order due to async processing, but
|
||||
// this is preferred over corrupted/interleaved console output
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_);
|
||||
}
|
||||
this->write_tx_buffer_to_console_();
|
||||
}
|
||||
} else {
|
||||
// No messages to process, disable loop if appropriate
|
||||
|
||||
@@ -71,6 +71,17 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
// Platform-specific: does write_msg_ add its own newline?
|
||||
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny)
|
||||
// Allows single write call with newline included for efficiency
|
||||
// true: write_msg_ adds newline itself via puts()/println() (other platforms)
|
||||
// Newline should NOT be added to buffer
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY)
|
||||
static constexpr bool WRITE_MSG_ADDS_NEWLINE = false;
|
||||
#else
|
||||
static constexpr bool WRITE_MSG_ADDS_NEWLINE = true;
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
/** Enum for logging UART selection
|
||||
*
|
||||
@@ -173,7 +184,7 @@ class Logger : public Component {
|
||||
|
||||
protected:
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg);
|
||||
void write_msg_(const char *msg, size_t len);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// It's the caller's responsibility to initialize buffer_at (typically to 0)
|
||||
@@ -200,6 +211,35 @@ class Logger : public Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to add newline to buffer for platforms that need it
|
||||
// Modifies buffer_at to include the newline
|
||||
inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
|
||||
if constexpr (!WRITE_MSG_ADDS_NEWLINE) {
|
||||
// Add newline - don't need to maintain null termination
|
||||
// write_msg_ now always receives explicit length, so we can safely overwrite the null terminator
|
||||
// This is safe because:
|
||||
// 1. Callbacks already received the message (before we add newline)
|
||||
// 2. write_msg_ receives the length explicitly (doesn't need null terminator)
|
||||
if (*buffer_at < buffer_size) {
|
||||
buffer[(*buffer_at)++] = '\n';
|
||||
} else if (buffer_size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
buffer[buffer_size - 1] = '\n';
|
||||
*buffer_at = buffer_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to write tx_buffer_ to console if logging is enabled
|
||||
// INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null
|
||||
inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) {
|
||||
if (this->baud_rate_ > 0) {
|
||||
uint16_t *len_ptr = length ? length : &this->tx_buffer_at_;
|
||||
this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset);
|
||||
this->write_msg_(this->tx_buffer_ + offset, *len_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to format and send a log message to both console and callbacks
|
||||
inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args) {
|
||||
@@ -208,10 +248,11 @@ class Logger : public Component {
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_,
|
||||
this->tx_buffer_size_);
|
||||
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
|
||||
}
|
||||
// Callbacks get message WITHOUT newline (for API/MQTT/syslog)
|
||||
this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
|
||||
|
||||
// Console gets message WITH newline (if platform needs it)
|
||||
this->write_tx_buffer_to_console_();
|
||||
}
|
||||
|
||||
// Write the body of the log message to the buffer
|
||||
@@ -425,7 +466,9 @@ class Logger : public Component {
|
||||
}
|
||||
|
||||
// Update buffer_at with the formatted length (handle truncation)
|
||||
uint16_t formatted_len = (ret >= remaining) ? remaining : ret;
|
||||
// When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
|
||||
// When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
|
||||
uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret;
|
||||
*buffer_at += formatted_len;
|
||||
|
||||
// Remove all trailing newlines right after formatting
|
||||
|
||||
@@ -121,25 +121,23 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
if (
|
||||
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
|
||||
this->uart_ == UART_SELECTION_USB_CDC
|
||||
#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
|
||||
this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
|
||||
this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) {
|
||||
// Length is now always passed explicitly - no strlen() fallback needed
|
||||
|
||||
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
|
||||
// USB CDC/JTAG - single write including newline (already in buffer)
|
||||
// Use fwrite to stdout which goes through VFS to USB console
|
||||
//
|
||||
// Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG).
|
||||
// They are ONLY defined when the user explicitly selects USB as the logger output in their config.
|
||||
// This is compile-time selection, not runtime detection - if USB is configured, it's always used.
|
||||
// There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility
|
||||
// to configure correctly for their hardware. This approach eliminates runtime overhead.
|
||||
fwrite(msg, 1, len, stdout);
|
||||
#else
|
||||
/* DISABLES CODE */ (false) // NOLINT
|
||||
// Regular UART - single write including newline (already in buffer)
|
||||
uart_write_bytes(this->uart_num_, msg, len);
|
||||
#endif
|
||||
) {
|
||||
puts(msg);
|
||||
} else {
|
||||
// Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
|
||||
size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
|
||||
uart_write_bytes(this->uart_num_, msg, len);
|
||||
uart_write_bytes(this->uart_num_, "\n", 1);
|
||||
}
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
|
||||
@@ -33,7 +33,10 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) {
|
||||
// Single write with newline already in buffer (added by caller)
|
||||
this->hw_serial_->write(msg, len);
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) {
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
@@ -49,7 +49,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -27,7 +27,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
|
||||
@@ -62,7 +62,7 @@ void Logger::pre_setup() {
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
void HOT Logger::write_msg_(const char *msg, size_t) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("%s\n", msg);
|
||||
#endif
|
||||
|
||||
@@ -466,7 +466,7 @@ void LvglComponent::setup() {
|
||||
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
|
||||
}
|
||||
if (buffer == nullptr) {
|
||||
this->status_set_error("Memory allocation failure");
|
||||
this->status_set_error(LOG_STR("Memory allocation failure"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -479,7 +479,7 @@ void LvglComponent::setup() {
|
||||
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
|
||||
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT
|
||||
if (this->rotate_buf_ == nullptr) {
|
||||
this->status_set_error("Memory allocation failure");
|
||||
this->status_set_error(LOG_STR("Memory allocation failure"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,14 +57,14 @@ void MAX17043Component::setup() {
|
||||
|
||||
if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) {
|
||||
ESP_LOGE(TAG, "Device does not appear to be a MAX17043");
|
||||
this->status_set_error("unrecognised");
|
||||
this->status_set_error(LOG_STR("unrecognised"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// need to write back to config register to reset the sleep bit
|
||||
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) {
|
||||
this->status_set_error("sleep reset failed");
|
||||
this->status_set_error(LOG_STR("sleep reset failed"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
|
||||
MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
|
||||
MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
|
||||
bool has_psk = api::global_api_server->get_noise_ctx()->has_psk();
|
||||
bool has_psk = api::global_api_server->get_noise_ctx().has_psk();
|
||||
const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
|
||||
txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,12 @@ static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel
|
||||
xSemaphoreGiveFromISR(sem, &need_yield);
|
||||
return (need_yield == pdTRUE);
|
||||
}
|
||||
|
||||
void MIPI_DSI::smark_failed(const LogString *message, esp_err_t err) {
|
||||
ESP_LOGE(TAG, "%s: %s", LOG_STR_ARG(message), esp_err_to_name(err));
|
||||
this->mark_failed(message);
|
||||
}
|
||||
|
||||
void MIPI_DSI::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Running Setup");
|
||||
|
||||
@@ -31,7 +37,7 @@ void MIPI_DSI::setup() {
|
||||
};
|
||||
auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed("lcd_new_dsi_bus failed", err);
|
||||
this->smark_failed(LOG_STR("lcd_new_dsi_bus failed"), err);
|
||||
return;
|
||||
}
|
||||
esp_lcd_dbi_io_config_t dbi_config = {
|
||||
@@ -41,7 +47,7 @@ void MIPI_DSI::setup() {
|
||||
};
|
||||
err = esp_lcd_new_panel_io_dbi(this->bus_handle_, &dbi_config, &this->io_handle_);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed("new_panel_io_dbi failed", err);
|
||||
this->smark_failed(LOG_STR("new_panel_io_dbi failed"), err);
|
||||
return;
|
||||
}
|
||||
auto pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565;
|
||||
@@ -69,7 +75,7 @@ void MIPI_DSI::setup() {
|
||||
}};
|
||||
err = esp_lcd_new_panel_dpi(this->bus_handle_, &dpi_config, &this->handle_);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed("esp_lcd_new_panel_dpi failed", err);
|
||||
this->smark_failed(LOG_STR("esp_lcd_new_panel_dpi failed"), err);
|
||||
return;
|
||||
}
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
@@ -86,14 +92,14 @@ void MIPI_DSI::setup() {
|
||||
auto when = millis() + 120;
|
||||
err = esp_lcd_panel_init(this->handle_);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed("esp_lcd_init failed", err);
|
||||
this->smark_failed(LOG_STR("esp_lcd_init failed"), err);
|
||||
return;
|
||||
}
|
||||
size_t index = 0;
|
||||
auto &vec = this->init_sequence_;
|
||||
while (index != vec.size()) {
|
||||
if (vec.size() - index < 2) {
|
||||
this->mark_failed("Malformed init sequence");
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = vec[index++];
|
||||
@@ -104,7 +110,7 @@ void MIPI_DSI::setup() {
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (vec.size() - index < num_args) {
|
||||
this->mark_failed("Malformed init sequence");
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
if (cmd == SLEEP_OUT) {
|
||||
@@ -119,7 +125,7 @@ void MIPI_DSI::setup() {
|
||||
format_hex_pretty(ptr, num_args, '.', false).c_str());
|
||||
err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed("lcd_panel_io_tx_param failed", err);
|
||||
this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err);
|
||||
return;
|
||||
}
|
||||
index += num_args;
|
||||
@@ -134,7 +140,7 @@ void MIPI_DSI::setup() {
|
||||
|
||||
err = (esp_lcd_dpi_panel_register_event_callbacks(this->handle_, &cbs, this->io_lock_));
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed("Failed to register callbacks", err);
|
||||
this->smark_failed(LOG_STR("Failed to register callbacks"), err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -216,7 +222,7 @@ bool MIPI_DSI::check_buffer_() {
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
this->buffer_ = allocator.allocate(this->height_ * this->width_ * bytes_per_pixel);
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed("Could not allocate buffer for display!");
|
||||
this->mark_failed(LOG_STR("Could not allocate buffer for display!"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -62,10 +62,7 @@ class MIPI_DSI : public display::Display {
|
||||
void set_lanes(uint8_t lanes) { this->lanes_ = lanes; }
|
||||
void set_madctl(uint8_t madctl) { this->madctl_ = madctl; }
|
||||
|
||||
void smark_failed(const char *message, esp_err_t err) {
|
||||
auto str = str_sprintf("Setup failed: %s: %s", message, esp_err_to_name(err));
|
||||
this->mark_failed(str.c_str());
|
||||
}
|
||||
void smark_failed(const LogString *message, esp_err_t err);
|
||||
|
||||
void update() override;
|
||||
|
||||
|
||||
@@ -35,3 +35,70 @@ DriverChip(
|
||||
(0x10, 0x0C), (0x11, 0x0C), (0x12, 0x0C), (0x13, 0x0C), (0x30, 0x00),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# JC4880P443 Driver Configuration (ST7701)
|
||||
# Using parameters from esp_lcd_st7701.h and the working full init sequence
|
||||
# ----------------------------------------------------------------------------------------------------------------------
|
||||
# * Resolution: 480x800
|
||||
# * PCLK Frequency: 34 MHz
|
||||
# * DSI Lane Bit Rate: 500 Mbps (using 2-Lane DSI configuration)
|
||||
# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42)
|
||||
# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166)
|
||||
# ----------------------------------------------------------------------------------------------------------------------
|
||||
DriverChip(
|
||||
"JC4880P443",
|
||||
width=480,
|
||||
height=800,
|
||||
hsync_back_porch=42,
|
||||
hsync_pulse_width=12,
|
||||
hsync_front_porch=42,
|
||||
vsync_back_porch=8,
|
||||
vsync_pulse_width=2,
|
||||
vsync_front_porch=166,
|
||||
pclk_frequency="34MHz",
|
||||
lane_bit_rate="500Mbps",
|
||||
swap_xy=cv.UNDEFINED,
|
||||
color_order="RGB",
|
||||
reset_pin=5,
|
||||
initsequence=[
|
||||
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x13),
|
||||
(0xEF, 0x08),
|
||||
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10),
|
||||
(0xC0, 0x63, 0x00),
|
||||
(0xC1, 0x0D, 0x02),
|
||||
(0xC2, 0x10, 0x08),
|
||||
(0xCC, 0x10),
|
||||
(0xB0, 0x80, 0x09, 0x53, 0x0C, 0xD0, 0x07, 0x0C, 0x09, 0x09, 0x28, 0x06, 0xD4, 0x13, 0x69, 0x2B, 0x71),
|
||||
(0xB1, 0x80, 0x94, 0x5A, 0x10, 0xD3, 0x06, 0x0A, 0x08, 0x08, 0x25, 0x03, 0xD3, 0x12, 0x66, 0x6A, 0x0D),
|
||||
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x11),
|
||||
(0xB0, 0x5D),
|
||||
(0xB1, 0x58),
|
||||
(0xB2, 0x87),
|
||||
(0xB3, 0x80),
|
||||
(0xB5, 0x4E),
|
||||
(0xB7, 0x85),
|
||||
(0xB8, 0x21),
|
||||
(0xB9, 0x10, 0x1F),
|
||||
(0xBB, 0x03),
|
||||
(0xBC, 0x00),
|
||||
(0xC1, 0x78),
|
||||
(0xC2, 0x78),
|
||||
(0xD0, 0x88),
|
||||
(0xE0, 0x00, 0x3A, 0x02),
|
||||
(0xE1, 0x04, 0xA0, 0x00, 0xA0, 0x05, 0xA0, 0x00, 0xA0, 0x00, 0x40, 0x40),
|
||||
(0xE2, 0x30, 0x00, 0x40, 0x40, 0x32, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0xA0, 0x00),
|
||||
(0xE3, 0x00, 0x00, 0x33, 0x33),
|
||||
(0xE4, 0x44, 0x44),
|
||||
(0xE5, 0x09, 0x2E, 0xA0, 0xA0, 0x0B, 0x30, 0xA0, 0xA0, 0x05, 0x2A, 0xA0, 0xA0, 0x07, 0x2C, 0xA0, 0xA0),
|
||||
(0xE6, 0x00, 0x00, 0x33, 0x33),
|
||||
(0xE7, 0x44, 0x44),
|
||||
(0xE8, 0x08, 0x2D, 0xA0, 0xA0, 0x0A, 0x2F, 0xA0, 0xA0, 0x04, 0x29, 0xA0, 0xA0, 0x06, 0x2B, 0xA0, 0xA0),
|
||||
(0xEB, 0x00, 0x00, 0x4E, 0x4E, 0x00, 0x00, 0x00),
|
||||
(0xEC, 0x08, 0x01),
|
||||
(0xED, 0xB0, 0x2B, 0x98, 0xA4, 0x56, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0x65, 0x4A, 0x89, 0xB2, 0x0B),
|
||||
(0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54),
|
||||
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x00),
|
||||
]
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
@@ -73,7 +73,7 @@ void MipiRgbSpi::write_init_sequence_() {
|
||||
auto &vec = this->init_sequence_;
|
||||
while (index != vec.size()) {
|
||||
if (vec.size() - index < 2) {
|
||||
this->mark_failed("Malformed init sequence");
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = vec[index++];
|
||||
@@ -84,7 +84,7 @@ void MipiRgbSpi::write_init_sequence_() {
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (vec.size() - index < num_args) {
|
||||
this->mark_failed("Malformed init sequence");
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
if (cmd == SLEEP_OUT) {
|
||||
@@ -164,8 +164,8 @@ void MipiRgb::common_setup_() {
|
||||
if (err == ESP_OK)
|
||||
err = esp_lcd_panel_init(this->handle_);
|
||||
if (err != ESP_OK) {
|
||||
auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed(msg.c_str());
|
||||
ESP_LOGE(TAG, "lcd setup failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed(LOG_STR("lcd setup failed"));
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "MipiRgb setup complete");
|
||||
}
|
||||
@@ -249,7 +249,7 @@ bool MipiRgb::check_buffer_() {
|
||||
RAMAllocator<uint16_t> allocator;
|
||||
this->buffer_ = allocator.allocate(this->height_ * this->width_);
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed("Could not allocate buffer for display!");
|
||||
this->mark_failed(LOG_STR("Could not allocate buffer for display!"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -478,7 +478,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
RAMAllocator<BUFFERTYPE> allocator{};
|
||||
this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION);
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed("Buffer allocation failed");
|
||||
this->mark_failed(LOG_STR("Buffer allocation failed"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,19 +78,20 @@ void SourceSpeaker::loop() {
|
||||
} else {
|
||||
switch (err) {
|
||||
case ESP_ERR_NO_MEM:
|
||||
this->status_set_error("Failed to start mixer: not enough memory");
|
||||
this->status_set_error(LOG_STR("Failed to start mixer: not enough memory"));
|
||||
break;
|
||||
case ESP_ERR_NOT_SUPPORTED:
|
||||
this->status_set_error("Failed to start mixer: unsupported bits per sample");
|
||||
this->status_set_error(LOG_STR("Failed to start mixer: unsupported bits per sample"));
|
||||
break;
|
||||
case ESP_ERR_INVALID_ARG:
|
||||
this->status_set_error("Failed to start mixer: audio stream isn't compatible with the other audio stream.");
|
||||
this->status_set_error(
|
||||
LOG_STR("Failed to start mixer: audio stream isn't compatible with the other audio stream."));
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
this->status_set_error("Failed to start mixer: mixer task failed to start");
|
||||
this->status_set_error(LOG_STR("Failed to start mixer: mixer task failed to start"));
|
||||
break;
|
||||
default:
|
||||
this->status_set_error("Failed to start mixer");
|
||||
this->status_set_error(LOG_STR("Failed to start mixer"));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -317,7 +318,7 @@ void MixerSpeaker::loop() {
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING);
|
||||
}
|
||||
if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) {
|
||||
this->status_set_error("Failed to allocate the mixer's internal buffer");
|
||||
this->status_set_error(LOG_STR("Failed to allocate the mixer's internal buffer"));
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
}
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) {
|
||||
|
||||
@@ -140,7 +140,7 @@ void MQTTClientComponent::send_device_info_() {
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
root[api::global_api_server->get_noise_ctx()->has_psk() ? "api_encryption" : "api_encryption_supported"] =
|
||||
root[api::global_api_server->get_noise_ctx().has_psk() ? "api_encryption" : "api_encryption_supported"] =
|
||||
"Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
#endif
|
||||
},
|
||||
|
||||
@@ -278,7 +278,7 @@ void NAU7802Sensor::loop() {
|
||||
this->set_calibration_failure_(true);
|
||||
this->state_ = CalibrationState::INACTIVE;
|
||||
ESP_LOGE(TAG, "Failed to calibrate sensor");
|
||||
this->status_set_error("Calibration Failed");
|
||||
this->status_set_error(LOG_STR("Calibration Failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -38,6 +39,14 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui
|
||||
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
|
||||
Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||
decoder->draw(x, y, w, h, color);
|
||||
|
||||
// Feed watchdog periodically to avoid triggering during long decode operations.
|
||||
// Feed every 1024 pixels to balance efficiency and responsiveness.
|
||||
uint32_t pixels = w * h;
|
||||
decoder->increment_pixels_decoded(pixels);
|
||||
if ((decoder->get_pixels_decoded() % 1024) < pixels) {
|
||||
App.feed_wdt();
|
||||
}
|
||||
}
|
||||
|
||||
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {
|
||||
|
||||
@@ -25,9 +25,13 @@ class PngDecoder : public ImageDecoder {
|
||||
int prepare(size_t download_size) override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
|
||||
void increment_pixels_decoded(uint32_t count) { this->pixels_decoded_ += count; }
|
||||
uint32_t get_pixels_decoded() const { return this->pixels_decoded_; }
|
||||
|
||||
protected:
|
||||
RAMAllocator<pngle_t> allocator_;
|
||||
pngle_t *pngle_;
|
||||
uint32_t pixels_decoded_{0};
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
|
||||
@@ -195,7 +195,7 @@ static void add(std::vector<uint8_t> &vec, const char *str) {
|
||||
void PacketTransport::setup() {
|
||||
this->name_ = App.get_name().c_str();
|
||||
if (strlen(this->name_) > 255) {
|
||||
this->status_set_error("Device name exceeds 255 chars");
|
||||
this->status_set_error(LOG_STR("Device name exceeds 255 chars"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,18 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
|
||||
this->lock_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
this->event_type_(stream);
|
||||
for (auto *obj : App.get_events())
|
||||
this->event_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
this->text_type_(stream);
|
||||
for (auto *obj : App.get_texts())
|
||||
this->text_row_(stream, obj, area, node, friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
this->text_sensor_type_(stream);
|
||||
for (auto *obj : App.get_text_sensors())
|
||||
@@ -547,6 +559,100 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso
|
||||
}
|
||||
#endif
|
||||
|
||||
// Type-specific implementation
|
||||
#ifdef USE_TEXT
|
||||
void PrometheusHandler::text_type_(AsyncResponseStream *stream) {
|
||||
stream->print(ESPHOME_F("#TYPE esphome_text_value gauge\n"));
|
||||
stream->print(ESPHOME_F("#TYPE esphome_text_failed gauge\n"));
|
||||
}
|
||||
void PrometheusHandler::text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name) {
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
if (obj->has_state()) {
|
||||
// We have a valid value, output this value
|
||||
stream->print(ESPHOME_F("esphome_text_failed{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("\"} 0\n"));
|
||||
// Data itself
|
||||
stream->print(ESPHOME_F("esphome_text_value{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("\",value=\""));
|
||||
stream->print(obj->state.c_str());
|
||||
stream->print(ESPHOME_F("\"} "));
|
||||
stream->print(ESPHOME_F("1.0"));
|
||||
stream->print(ESPHOME_F("\n"));
|
||||
} else {
|
||||
// Invalid state
|
||||
stream->print(ESPHOME_F("esphome_text_failed{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("\"} 1\n"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Type-specific implementation
|
||||
#ifdef USE_EVENT
|
||||
void PrometheusHandler::event_type_(AsyncResponseStream *stream) {
|
||||
stream->print(ESPHOME_F("#TYPE esphome_event_value gauge\n"));
|
||||
stream->print(ESPHOME_F("#TYPE esphome_event_failed gauge\n"));
|
||||
}
|
||||
void PrometheusHandler::event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name) {
|
||||
if (obj->is_internal() && !this->include_internal_)
|
||||
return;
|
||||
if (obj->get_last_event_type() != nullptr) {
|
||||
// We have a valid event type, output this value
|
||||
stream->print(ESPHOME_F("esphome_event_failed{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("\"} 0\n"));
|
||||
// Data itself
|
||||
stream->print(ESPHOME_F("esphome_event_value{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("\",last_event_type=\""));
|
||||
stream->print(obj->get_last_event_type());
|
||||
stream->print(ESPHOME_F("\"} "));
|
||||
stream->print(ESPHOME_F("1.0"));
|
||||
stream->print(ESPHOME_F("\n"));
|
||||
} else {
|
||||
// No event triggered yet
|
||||
stream->print(ESPHOME_F("esphome_event_failed{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("\"} 1\n"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Type-specific implementation
|
||||
#ifdef USE_NUMBER
|
||||
void PrometheusHandler::number_type_(AsyncResponseStream *stream) {
|
||||
@@ -620,7 +726,7 @@ void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select
|
||||
stream->print(ESPHOME_F("\",name=\""));
|
||||
stream->print(relabel_name_(obj).c_str());
|
||||
stream->print(ESPHOME_F("\",value=\""));
|
||||
stream->print(obj->state.c_str());
|
||||
stream->print(obj->current_option());
|
||||
stream->print(ESPHOME_F("\"} "));
|
||||
stream->print(ESPHOME_F("1.0"));
|
||||
stream->print(ESPHOME_F("\n"));
|
||||
|
||||
@@ -123,6 +123,22 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
/// Return the type for prometheus
|
||||
void event_type_(AsyncResponseStream *stream);
|
||||
/// Return the event values state as prometheus data point
|
||||
void event_row_(AsyncResponseStream *stream, event::Event *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
/// Return the type for prometheus
|
||||
void text_type_(AsyncResponseStream *stream);
|
||||
/// Return the text values state as prometheus data point
|
||||
void text_row_(AsyncResponseStream *stream, text::Text *obj, std::string &area, std::string &node,
|
||||
std::string &friendly_name);
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
/// Return the type for prometheus
|
||||
void text_sensor_type_(AsyncResponseStream *stream);
|
||||
|
||||
@@ -310,7 +310,7 @@ void QMP6988Component::calculate_pressure_() {
|
||||
|
||||
void QMP6988Component::setup() {
|
||||
if (!this->device_check_()) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,17 +66,17 @@ void ResamplerSpeaker::loop() {
|
||||
}
|
||||
|
||||
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) {
|
||||
this->status_set_error("Resampler task failed to allocate the internal buffers");
|
||||
this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers"));
|
||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) {
|
||||
this->status_set_error("Cannot resample due to an unsupported audio stream");
|
||||
this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream"));
|
||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) {
|
||||
this->status_set_error("Resampler task failed");
|
||||
this->status_set_error(LOG_STR("Resampler task failed"));
|
||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
@@ -106,12 +106,12 @@ void ResamplerSpeaker::loop() {
|
||||
} else {
|
||||
switch (err) {
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
this->status_set_error("Failed to start resampler: resampler task failed to start");
|
||||
this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start"));
|
||||
break;
|
||||
case ESP_ERR_NO_MEM:
|
||||
this->status_set_error("Failed to start resampler: not enough memory for task stack");
|
||||
this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack"));
|
||||
default:
|
||||
this->status_set_error("Failed to start resampler");
|
||||
this->status_set_error(LOG_STR("Failed to start resampler"));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <forward_list>
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -290,10 +290,10 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
||||
}
|
||||
|
||||
// Store parameters for later execution
|
||||
this->param_queue_.emplace_front(x...);
|
||||
// Enable loop now that we have work to do
|
||||
this->param_queue_.emplace_back(x...);
|
||||
// Enable loop now that we have work to do - don't call loop() synchronously!
|
||||
// Let the event loop call it to avoid reentrancy issues
|
||||
this->enable_loop();
|
||||
this->loop();
|
||||
}
|
||||
|
||||
void loop() override {
|
||||
@@ -303,13 +303,17 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
||||
if (this->script_->is_running())
|
||||
return;
|
||||
|
||||
while (!this->param_queue_.empty()) {
|
||||
// Only process ONE queued item per loop iteration
|
||||
// Processing all items in a while loop causes infinite loops because
|
||||
// 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->param_queue_.pop_front();
|
||||
} else {
|
||||
// Queue is now empty - disable loop until next play_complex
|
||||
this->disable_loop();
|
||||
}
|
||||
// Queue is now empty - disable loop until next play_complex
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override { /* ignore - see play_complex */
|
||||
@@ -326,7 +330,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
|
||||
}
|
||||
|
||||
C *script_;
|
||||
std::forward_list<std::tuple<Ts...>> param_queue_;
|
||||
std::list<std::tuple<Ts...>> param_queue_;
|
||||
};
|
||||
|
||||
} // namespace script
|
||||
|
||||
@@ -13,7 +13,7 @@ void SHT4XComponent::start_heater_() {
|
||||
|
||||
ESP_LOGD(TAG, "Heater turning on");
|
||||
if (this->write(cmd, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_error("Failed to turn on heater");
|
||||
this->status_set_error(LOG_STR("Failed to turn on heater"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ static const float SENSOR_SCALE = 0.01f; // Sensor resolution in degrees Celsiu
|
||||
void STTS22HComponent::setup() {
|
||||
// Check if device is a STTS22H
|
||||
if (!this->is_stts22h_sensor_()) {
|
||||
this->mark_failed("Device is not a STTS22H sensor");
|
||||
this->mark_failed(LOG_STR("Device is not a STTS22H sensor"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,12 +61,12 @@ float STTS22HComponent::read_temperature_() {
|
||||
bool STTS22HComponent::is_stts22h_sensor_() {
|
||||
uint8_t whoami_value;
|
||||
if (this->read_register(WHOAMI_REG, &whoami_value, 1) != i2c::NO_ERROR) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (whoami_value != WHOAMI_STTS22H_IDENTIFICATION) {
|
||||
this->mark_failed("Unexpected WHOAMI identifier. Sensor is not a STTS22H");
|
||||
this->mark_failed(LOG_STR("Unexpected WHOAMI identifier. Sensor is not a STTS22H"));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ void STTS22HComponent::initialize_sensor_() {
|
||||
// Read current CTRL_REG configuration
|
||||
uint8_t ctrl_value;
|
||||
if (this->read_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -86,14 +86,14 @@ void STTS22HComponent::initialize_sensor_() {
|
||||
// FREERUN bit must be cleared (see sensor documentation)
|
||||
ctrl_value &= ~FREERUN_CTRL_ENABLE_FLAG; // Clear FREERUN bit
|
||||
if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable LOW ODR mode and ADD_INC
|
||||
ctrl_value |= LOW_ODR_CTRL_ENABLE_FLAG | ADD_INC_ENABLE_FLAG; // Set LOW ODR bit and ADD_INC bit
|
||||
if (this->write_register(CTRL_REG, &ctrl_value, 1) != i2c::NO_ERROR) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble_tracker, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_EXTERNAL_TEMPERATURE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_MAC_ADDRESS,
|
||||
CONF_SIGNAL_STRENGTH,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_DECIBEL_MILLIWATT,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@sittner"]
|
||||
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
|
||||
thermopro_ble_ns = cg.esphome_ns.namespace("thermopro_ble")
|
||||
ThermoProBLE = thermopro_ble_ns.class_(
|
||||
"ThermoProBLE", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ThermoProBLE),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await esp32_ble_tracker.register_ble_device(var, config)
|
||||
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature(sens))
|
||||
if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(external_temperature_config)
|
||||
cg.add(var.set_external_temperature(sens))
|
||||
if humidity_config := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humidity_config)
|
||||
cg.add(var.set_humidity(sens))
|
||||
if battery_level_config := config.get(CONF_BATTERY_LEVEL):
|
||||
sens = await sensor.new_sensor(battery_level_config)
|
||||
cg.add(var.set_battery_level(sens))
|
||||
if signal_strength_config := config.get(CONF_SIGNAL_STRENGTH):
|
||||
sens = await sensor.new_sensor(signal_strength_config)
|
||||
cg.add(var.set_signal_strength(sens))
|
||||
@@ -0,0 +1,204 @@
|
||||
#include "thermopro_ble.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome::thermopro_ble {
|
||||
|
||||
// this size must be large enough to hold the largest data frame
|
||||
// of all supported devices
|
||||
static constexpr std::size_t MAX_DATA_SIZE = 24;
|
||||
|
||||
struct DeviceParserMapping {
|
||||
const char *prefix;
|
||||
DeviceParser parser;
|
||||
};
|
||||
|
||||
static float tp96_battery(uint16_t voltage);
|
||||
|
||||
static optional<ParseResult> parse_tp972(const uint8_t *data, std::size_t data_size);
|
||||
static optional<ParseResult> parse_tp96(const uint8_t *data, std::size_t data_size);
|
||||
static optional<ParseResult> parse_tp3(const uint8_t *data, std::size_t data_size);
|
||||
|
||||
static const char *const TAG = "thermopro_ble";
|
||||
|
||||
static const struct DeviceParserMapping DEVICE_PARSER_MAP[] = {
|
||||
{"TP972", parse_tp972}, {"TP970", parse_tp96}, {"TP96", parse_tp96}, {"TP3", parse_tp3}};
|
||||
|
||||
void ThermoProBLE::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ThermoPro BLE");
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "External temperature", this->external_temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
}
|
||||
|
||||
bool ThermoProBLE::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
// check for matching mac address
|
||||
if (device.address_uint64() != this->address_) {
|
||||
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for valid device type
|
||||
update_device_type_(device.get_name());
|
||||
if (this->device_parser_ == nullptr) {
|
||||
ESP_LOGVV(TAG, "parse_device(): invalid device type.");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
|
||||
|
||||
// publish signal strength
|
||||
float signal_strength = float(device.get_rssi());
|
||||
if (this->signal_strength_ != nullptr)
|
||||
this->signal_strength_->publish_state(signal_strength);
|
||||
|
||||
bool success = false;
|
||||
for (auto &service_data : device.get_manufacturer_datas()) {
|
||||
// check maximum data size
|
||||
std::size_t data_size = service_data.data.size() + 2;
|
||||
if (data_size > MAX_DATA_SIZE) {
|
||||
ESP_LOGVV(TAG, "parse_device(): maximum data size exceeded!");
|
||||
continue;
|
||||
}
|
||||
|
||||
// reconstruct whole record from 2 byte uuid and data
|
||||
esp_bt_uuid_t uuid = service_data.uuid.get_uuid();
|
||||
uint8_t data[MAX_DATA_SIZE] = {static_cast<uint8_t>(uuid.uuid.uuid16), static_cast<uint8_t>(uuid.uuid.uuid16 >> 8)};
|
||||
std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2);
|
||||
|
||||
// dispatch data to parser
|
||||
optional<ParseResult> result = this->device_parser_(data, data_size);
|
||||
if (!result.has_value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// publish sensor values
|
||||
if (result->temperature.has_value() && this->temperature_ != nullptr)
|
||||
this->temperature_->publish_state(*result->temperature);
|
||||
if (result->external_temperature.has_value() && this->external_temperature_ != nullptr)
|
||||
this->external_temperature_->publish_state(*result->external_temperature);
|
||||
if (result->humidity.has_value() && this->humidity_ != nullptr)
|
||||
this->humidity_->publish_state(*result->humidity);
|
||||
if (result->battery_level.has_value() && this->battery_level_ != nullptr)
|
||||
this->battery_level_->publish_state(*result->battery_level);
|
||||
|
||||
success = true;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
void ThermoProBLE::update_device_type_(const std::string &device_name) {
|
||||
// check for changed device name (should only happen on initial call)
|
||||
if (this->device_name_ == device_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remember device name
|
||||
this->device_name_ = device_name;
|
||||
|
||||
// try to find device parser
|
||||
for (const auto &mapping : DEVICE_PARSER_MAP) {
|
||||
if (device_name.starts_with(mapping.prefix)) {
|
||||
this->device_parser_ = mapping.parser;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// device type unknown
|
||||
this->device_parser_ = nullptr;
|
||||
ESP_LOGVV(TAG, "update_device_type_(): unknown device type %s.", device_name.c_str());
|
||||
}
|
||||
|
||||
static inline uint16_t read_uint16(const uint8_t *data, std::size_t offset) {
|
||||
return static_cast<uint16_t>(data[offset + 0]) | (static_cast<uint16_t>(data[offset + 1]) << 8);
|
||||
}
|
||||
|
||||
static inline int16_t read_int16(const uint8_t *data, std::size_t offset) {
|
||||
return static_cast<int16_t>(read_uint16(data, offset));
|
||||
}
|
||||
|
||||
static inline uint32_t read_uint32(const uint8_t *data, std::size_t offset) {
|
||||
return static_cast<uint32_t>(data[offset + 0]) | (static_cast<uint32_t>(data[offset + 1]) << 8) |
|
||||
(static_cast<uint32_t>(data[offset + 2]) << 16) | (static_cast<uint32_t>(data[offset + 3]) << 24);
|
||||
}
|
||||
|
||||
// Battery calculation used with permission from:
|
||||
// https://github.com/Bluetooth-Devices/thermopro-ble/blob/main/src/thermopro_ble/parser.py
|
||||
//
|
||||
// TP96x battery values appear to be a voltage reading, probably in millivolts.
|
||||
// This means that calculating battery life from it is a non-linear function.
|
||||
// Examining the curve, it looked fairly close to a curve from the tanh function.
|
||||
// So, I created a script to use Tensorflow to optimize an equation in the format
|
||||
// A*tanh(B*x+C)+D
|
||||
// Where A,B,C,D are the variables to optimize for. This yielded the below function
|
||||
static float tp96_battery(uint16_t voltage) {
|
||||
float level = 52.317286f * tanh(static_cast<float>(voltage) / 273.624277936f - 8.76485439394f) + 51.06925f;
|
||||
return std::max(0.0f, std::min(level, 100.0f));
|
||||
}
|
||||
|
||||
static optional<ParseResult> parse_tp972(const uint8_t *data, std::size_t data_size) {
|
||||
if (data_size != 23) {
|
||||
ESP_LOGVV(TAG, "parse_tp972(): payload has wrong size of %d (!= 23)!", data_size);
|
||||
return {};
|
||||
}
|
||||
|
||||
ParseResult result;
|
||||
|
||||
// ambient temperature, 2 bytes, 16-bit unsigned integer, -54 °C offset
|
||||
result.external_temperature = static_cast<float>(read_uint16(data, 1)) - 54.0f;
|
||||
|
||||
// battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage)
|
||||
result.battery_level = tp96_battery(read_uint16(data, 3));
|
||||
|
||||
// internal temperature, 4 bytes, float, -54 °C offset
|
||||
result.temperature = static_cast<float>(read_uint32(data, 9)) - 54.0f;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static optional<ParseResult> parse_tp96(const uint8_t *data, std::size_t data_size) {
|
||||
if (data_size != 7) {
|
||||
ESP_LOGVV(TAG, "parse_tp96(): payload has wrong size of %d (!= 7)!", data_size);
|
||||
return {};
|
||||
}
|
||||
|
||||
ParseResult result;
|
||||
|
||||
// internal temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset
|
||||
result.temperature = static_cast<float>(read_uint16(data, 1)) - 30.0f;
|
||||
|
||||
// battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage)
|
||||
result.battery_level = tp96_battery(read_uint16(data, 3));
|
||||
|
||||
// ambient temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset
|
||||
result.external_temperature = static_cast<float>(read_uint16(data, 5)) - 30.0f;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static optional<ParseResult> parse_tp3(const uint8_t *data, std::size_t data_size) {
|
||||
if (data_size < 6) {
|
||||
ESP_LOGVV(TAG, "parse_tp3(): payload has wrong size of %d (< 6)!", data_size);
|
||||
return {};
|
||||
}
|
||||
|
||||
ParseResult result;
|
||||
|
||||
// temperature, 2 bytes, 16-bit signed integer, 0.1 °C
|
||||
result.temperature = static_cast<float>(read_int16(data, 1)) * 0.1f;
|
||||
|
||||
// humidity, 1 byte, 8-bit unsigned integer, 1.0 %
|
||||
result.humidity = static_cast<float>(data[3]);
|
||||
|
||||
// battery level, 2 bits (0-2)
|
||||
result.battery_level = static_cast<float>(data[4] & 0x3) * 50.0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace esphome::thermopro_ble
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome::thermopro_ble {
|
||||
|
||||
struct ParseResult {
|
||||
optional<float> temperature;
|
||||
optional<float> external_temperature;
|
||||
optional<float> humidity;
|
||||
optional<float> battery_level;
|
||||
};
|
||||
|
||||
using DeviceParser = optional<ParseResult> (*)(const uint8_t *data, std::size_t data_size);
|
||||
|
||||
class ThermoProBLE : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
|
||||
public:
|
||||
void set_address(uint64_t address) { this->address_ = address; };
|
||||
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
||||
void dump_config() override;
|
||||
void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; }
|
||||
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
||||
void set_external_temperature(sensor::Sensor *external_temperature) {
|
||||
this->external_temperature_ = external_temperature;
|
||||
}
|
||||
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
||||
void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; }
|
||||
|
||||
protected:
|
||||
uint64_t address_;
|
||||
std::string device_name_;
|
||||
DeviceParser device_parser_{nullptr};
|
||||
sensor::Sensor *signal_strength_{nullptr};
|
||||
sensor::Sensor *temperature_{nullptr};
|
||||
sensor::Sensor *external_temperature_{nullptr};
|
||||
sensor::Sensor *humidity_{nullptr};
|
||||
sensor::Sensor *battery_level_{nullptr};
|
||||
|
||||
void update_device_type_(const std::string &device_name);
|
||||
};
|
||||
|
||||
} // namespace esphome::thermopro_ble
|
||||
|
||||
#endif
|
||||
@@ -21,7 +21,7 @@ void UDPComponent::setup() {
|
||||
if (this->should_broadcast_) {
|
||||
this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
if (this->broadcast_socket_ == nullptr) {
|
||||
this->status_set_error("Could not create socket");
|
||||
this->status_set_error(LOG_STR("Could not create socket"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -41,14 +41,14 @@ void UDPComponent::setup() {
|
||||
if (this->should_listen_) {
|
||||
this->listen_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
if (this->listen_socket_ == nullptr) {
|
||||
this->status_set_error("Could not create socket");
|
||||
this->status_set_error(LOG_STR("Could not create socket"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
auto err = this->listen_socket_->setblocking(false);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Unable to set nonblocking: errno %d", errno);
|
||||
this->status_set_error("Unable to set nonblocking");
|
||||
this->status_set_error(LOG_STR("Unable to set nonblocking"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ void UDPComponent::setup() {
|
||||
err = this->listen_socket_->setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, &imreq, sizeof(imreq));
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Failed to set IP_ADD_MEMBERSHIP. Error %d", errno);
|
||||
this->status_set_error("Failed to set IP_ADD_MEMBERSHIP");
|
||||
this->status_set_error(LOG_STR("Failed to set IP_ADD_MEMBERSHIP"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
@@ -82,7 +82,7 @@ void UDPComponent::setup() {
|
||||
err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server));
|
||||
if (err != 0) {
|
||||
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
|
||||
this->status_set_error("Unable to bind socket");
|
||||
this->status_set_error(LOG_STR("Unable to bind socket"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ void USBClient::setup() {
|
||||
auto err = usb_host_client_register(&config, &this->handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err));
|
||||
this->status_set_error("Client register failed");
|
||||
this->status_set_error(LOG_STR("Client register failed"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ void USBHost::setup() {
|
||||
usb_host_config_t config{};
|
||||
|
||||
if (usb_host_install(&config) != ESP_OK) {
|
||||
this->status_set_error("usb_host_install failed");
|
||||
this->status_set_error(LOG_STR("usb_host_install failed"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ static void fix_mps(const usb_ep_desc_t *ep) {
|
||||
void USBUartTypeCdcAcm::on_connected() {
|
||||
auto cdc_devs = this->parse_descriptors(this->device_handle_);
|
||||
if (cdc_devs.empty()) {
|
||||
this->status_set_error("No CDC-ACM device found");
|
||||
this->status_set_error(LOG_STR("No CDC-ACM device found"));
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
@@ -341,7 +341,7 @@ void USBUartTypeCdcAcm::on_connected() {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_,
|
||||
channel->cdc_dev_.bulk_interface_number);
|
||||
this->status_set_error("usb_host_interface_claim failed");
|
||||
this->status_set_error(LOG_STR("usb_host_interface_claim failed"));
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ void VoiceAssistant::loop() {
|
||||
case State::START_MICROPHONE: {
|
||||
ESP_LOGD(TAG, "Starting Microphone");
|
||||
if (!this->allocate_buffers_()) {
|
||||
this->status_set_error("Failed to allocate buffers");
|
||||
this->status_set_error(LOG_STR("Failed to allocate buffers"));
|
||||
return;
|
||||
}
|
||||
if (this->status_has_error()) {
|
||||
|
||||
@@ -67,7 +67,7 @@ void WakeOnLanButton::setup() {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
if (this->broadcast_socket_ == nullptr) {
|
||||
this->status_set_error("Could not create socket");
|
||||
this->status_set_error(LOG_STR("Could not create socket"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ class WebServerBase : public Component {
|
||||
this->initialized_++;
|
||||
return;
|
||||
}
|
||||
this->server_ = std::make_shared<AsyncWebServer>(this->port_);
|
||||
this->server_ = std::make_unique<AsyncWebServer>(this->port_);
|
||||
// All content is controlled and created by user - so allowing all origins is fine here.
|
||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
|
||||
this->server_->begin();
|
||||
@@ -127,7 +127,7 @@ class WebServerBase : public Component {
|
||||
this->server_ = nullptr;
|
||||
}
|
||||
}
|
||||
std::shared_ptr<AsyncWebServer> get_server() const { return server_; }
|
||||
AsyncWebServer *get_server() const { return this->server_.get(); }
|
||||
float get_setup_priority() const override;
|
||||
|
||||
#ifdef USE_WEBSERVER_AUTH
|
||||
@@ -143,7 +143,7 @@ class WebServerBase : public Component {
|
||||
protected:
|
||||
int initialized_{0};
|
||||
uint16_t port_{80};
|
||||
std::shared_ptr<AsyncWebServer> server_{nullptr};
|
||||
std::unique_ptr<AsyncWebServer> server_{nullptr};
|
||||
std::vector<AsyncWebHandler *> handlers_;
|
||||
#ifdef USE_WEBSERVER_AUTH
|
||||
internal::Credentials credentials_;
|
||||
|
||||
@@ -97,6 +97,7 @@ WIFI_MIN_AUTH_MODES = {
|
||||
VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True)
|
||||
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
|
||||
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
|
||||
WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition)
|
||||
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
|
||||
WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action)
|
||||
WiFiConfigureAction = wifi_ns.class_(
|
||||
@@ -590,6 +591,11 @@ async def wifi_enabled_to_code(config, condition_id, template_arg, args):
|
||||
return cg.new_Pvariable(condition_id, template_arg)
|
||||
|
||||
|
||||
@automation.register_condition("wifi.ap_active", WiFiAPActiveCondition, cv.Schema({}))
|
||||
async def wifi_ap_active_to_code(config, condition_id, template_arg, args):
|
||||
return cg.new_Pvariable(condition_id, template_arg)
|
||||
|
||||
|
||||
@automation.register_action("wifi.enable", WiFiEnableAction, cv.Schema({}))
|
||||
async def wifi_enable_to_code(config, action_id, template_arg, args):
|
||||
return cg.new_Pvariable(action_id, template_arg)
|
||||
@@ -601,6 +607,7 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
|
||||
RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save"
|
||||
|
||||
|
||||
def request_wifi_scan_results():
|
||||
@@ -613,13 +620,28 @@ def request_wifi_scan_results():
|
||||
CORE.data[KEEP_SCAN_RESULTS_KEY] = True
|
||||
|
||||
|
||||
def enable_runtime_power_save_control():
|
||||
"""Enable runtime WiFi power save control.
|
||||
|
||||
Components that need to dynamically switch WiFi power saving on/off for latency
|
||||
performance (e.g., audio streaming, large data transfers) should call this
|
||||
function during their code generation. This enables the request_high_performance()
|
||||
and release_high_performance() APIs.
|
||||
|
||||
Only supported on ESP32.
|
||||
"""
|
||||
CORE.data[RUNTIME_POWER_SAVE_KEY] = True
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def final_step():
|
||||
"""Final code generation step to configure scan result retention."""
|
||||
"""Final code generation step to configure optional WiFi features."""
|
||||
if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False):
|
||||
cg.add(
|
||||
cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)")
|
||||
)
|
||||
if CORE.data.get(RUNTIME_POWER_SAVE_KEY, False):
|
||||
cg.add_define("USE_WIFI_RUNTIME_POWER_SAVE")
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -16,6 +16,11 @@ template<typename... Ts> class WiFiEnabledCondition : public Condition<Ts...> {
|
||||
bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class WiFiAPActiveCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_wifi_component->is_ap_active(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class WiFiEnableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_wifi_component->enable(); }
|
||||
|
||||
@@ -329,6 +329,19 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
|
||||
|
||||
void WiFiComponent::setup() {
|
||||
this->wifi_pre_setup_();
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
// Create semaphore for high-performance mode requests
|
||||
// Start at 0, increment on request, decrement on release
|
||||
this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0);
|
||||
if (this->high_performance_semaphore_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed semaphore");
|
||||
}
|
||||
|
||||
// Store the configured power save mode as baseline
|
||||
this->configured_power_save_ = this->power_save_;
|
||||
#endif
|
||||
|
||||
if (this->enable_on_boot_) {
|
||||
this->start();
|
||||
} else {
|
||||
@@ -370,6 +383,19 @@ void WiFiComponent::start() {
|
||||
ESP_LOGV(TAG, "Setting Output Power Option failed");
|
||||
}
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
// Synchronize power_save_ with semaphore state before applying
|
||||
if (this->high_performance_semaphore_ != nullptr) {
|
||||
UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
|
||||
if (semaphore_count > 0) {
|
||||
this->power_save_ = WIFI_POWER_SAVE_NONE;
|
||||
this->is_high_performance_mode_ = true;
|
||||
} else {
|
||||
this->power_save_ = this->configured_power_save_;
|
||||
this->is_high_performance_mode_ = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!this->wifi_apply_power_save_()) {
|
||||
ESP_LOGV(TAG, "Setting Power Save Option failed");
|
||||
}
|
||||
@@ -524,11 +550,37 @@ void WiFiComponent::loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
// Check if power save mode needs to be updated based on high-performance requests
|
||||
if (this->high_performance_semaphore_ != nullptr) {
|
||||
// Semaphore count directly represents active requests (starts at 0, increments on request)
|
||||
UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
|
||||
|
||||
if (semaphore_count > 0 && !this->is_high_performance_mode_) {
|
||||
// Transition to high-performance mode (no power save)
|
||||
ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count,
|
||||
semaphore_count == 1 ? "request" : "requests");
|
||||
this->power_save_ = WIFI_POWER_SAVE_NONE;
|
||||
if (this->wifi_apply_power_save_()) {
|
||||
this->is_high_performance_mode_ = true;
|
||||
}
|
||||
} else if (semaphore_count == 0 && this->is_high_performance_mode_) {
|
||||
// Restore to configured power save mode
|
||||
ESP_LOGV(TAG, "Restoring power save mode to configured setting");
|
||||
this->power_save_ = this->configured_power_save_;
|
||||
if (this->wifi_apply_power_save_()) {
|
||||
this->is_high_performance_mode_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
WiFiComponent::WiFiComponent() { global_wifi_component = this; }
|
||||
|
||||
bool WiFiComponent::has_ap() const { return this->has_ap_; }
|
||||
bool WiFiComponent::is_ap_active() const { return this->state_ == WIFI_COMPONENT_STATE_AP; }
|
||||
bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
|
||||
#ifdef USE_WIFI_11KV_SUPPORT
|
||||
void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
|
||||
@@ -1565,7 +1617,12 @@ bool WiFiComponent::is_connected() {
|
||||
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
|
||||
this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_;
|
||||
}
|
||||
void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; }
|
||||
void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) {
|
||||
this->power_save_ = power_save;
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
this->configured_power_save_ = power_save;
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
|
||||
|
||||
@@ -1584,6 +1641,38 @@ bool WiFiComponent::is_esp32_improv_active_() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
bool WiFiComponent::request_high_performance() {
|
||||
// Already configured for high performance - request satisfied
|
||||
if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Semaphore initialization failed
|
||||
if (this->high_performance_semaphore_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Give the semaphore (non-blocking). This increments the count.
|
||||
return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE;
|
||||
}
|
||||
|
||||
bool WiFiComponent::release_high_performance() {
|
||||
// Already configured for high performance - nothing to release
|
||||
if (this->configured_power_save_ == WIFI_POWER_SAVE_NONE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Semaphore initialization failed
|
||||
if (this->high_performance_semaphore_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Take the semaphore (non-blocking). This decrements the count.
|
||||
return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE;
|
||||
}
|
||||
#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE
|
||||
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) {
|
||||
SavedWifiFastConnectSettings fast_connect_save{};
|
||||
|
||||
@@ -49,6 +49,11 @@ extern "C" {
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::wifi {
|
||||
|
||||
/// Sentinel value for RSSI when WiFi is not connected
|
||||
@@ -307,6 +312,7 @@ class WiFiComponent : public Component {
|
||||
|
||||
bool has_sta() const;
|
||||
bool has_ap() const;
|
||||
bool is_ap_active() const;
|
||||
|
||||
#ifdef USE_WIFI_11KV_SUPPORT
|
||||
void set_btm(bool btm);
|
||||
@@ -382,6 +388,37 @@ class WiFiComponent : public Component {
|
||||
this->wifi_connect_state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_RUNTIME_POWER_SAVE
|
||||
/** Request high-performance mode (no power saving) for improved WiFi latency.
|
||||
*
|
||||
* Components that need maximum WiFi performance (e.g., audio streaming, large data transfers)
|
||||
* can call this method to temporarily disable WiFi power saving. Multiple components can
|
||||
* request high performance simultaneously using a counting semaphore.
|
||||
*
|
||||
* Power saving will be restored to the YAML-configured mode when all components have
|
||||
* called release_high_performance().
|
||||
*
|
||||
* Note: Only supported on ESP32.
|
||||
*
|
||||
* @return true if request was satisfied (high-performance mode active or already configured),
|
||||
* false if operation failed (semaphore error)
|
||||
*/
|
||||
bool request_high_performance();
|
||||
|
||||
/** Release a high-performance mode request.
|
||||
*
|
||||
* Should be called when a component no longer needs maximum WiFi latency.
|
||||
* When all requests are released (semaphore count reaches zero), WiFi power saving
|
||||
* is restored to the YAML-configured mode.
|
||||
*
|
||||
* Note: Only supported on ESP32.
|
||||
*
|
||||
* @return true if release was successful (or already in high-performance config),
|
||||
* false if operation failed (semaphore error)
|
||||
*/
|
||||
bool release_high_performance();
|
||||
#endif // USE_WIFI_RUNTIME_POWER_SAVE
|
||||
|
||||
protected:
|
||||
#ifdef USE_WIFI_AP
|
||||
void setup_ap_config_();
|
||||
@@ -555,6 +592,12 @@ class WiFiComponent : public Component {
|
||||
bool keep_scan_results_{false};
|
||||
bool did_scan_this_cycle_{false};
|
||||
bool skip_cooldown_next_cycle_{false};
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
|
||||
WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE};
|
||||
bool is_high_performance_mode_{false};
|
||||
|
||||
SemaphoreHandle_t high_performance_semaphore_{nullptr};
|
||||
#endif
|
||||
|
||||
// Pointers at the end (naturally aligned)
|
||||
Trigger<> *connect_trigger_{new Trigger<>()};
|
||||
|
||||
@@ -602,10 +602,6 @@ const char *get_auth_mode_str(uint8_t mode) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string format_ip4_addr(const esp_ip4_addr_t &ip) { return str_snprintf(IPSTR, 15, IP2STR(&ip)); }
|
||||
#if LWIP_IPV6
|
||||
std::string format_ip6_addr(const esp_ip6_addr_t &ip) { return str_snprintf(IPV6STR, 39, IPV62STR(ip)); }
|
||||
#endif /* LWIP_IPV6 */
|
||||
const char *get_disconnect_reason_str(uint8_t reason) {
|
||||
switch (reason) {
|
||||
case WIFI_REASON_AUTH_EXPIRE:
|
||||
@@ -762,15 +758,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
#if USE_NETWORK_IPV6
|
||||
esp_netif_create_ip6_linklocal(s_sta_netif);
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(),
|
||||
format_ip4_addr(it.ip_info.gw).c_str());
|
||||
ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
|
||||
this->got_ipv4_address_ = true;
|
||||
this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) {
|
||||
const auto &it = data->data.ip_got_ip6;
|
||||
ESP_LOGV(TAG, "IPv6 address=%s", format_ip6_addr(it.ip6_info.ip).c_str());
|
||||
ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip));
|
||||
this->num_ipv6_addresses_++;
|
||||
this->ip_state_callback_.call(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
@@ -836,7 +831,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) {
|
||||
const auto &it = data->data.ip_ap_staipassigned;
|
||||
ESP_LOGV(TAG, "AP client assigned IP %s", format_ip4_addr(it.ip).c_str());
|
||||
ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <list>
|
||||
#include <vector>
|
||||
#include <forward_list>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -445,9 +445,10 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
|
||||
// Store for later processing
|
||||
auto now = millis();
|
||||
auto timeout = this->timeout_value_.optional_value(x...);
|
||||
this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...));
|
||||
this->var_queue_.emplace_back(now, timeout, std::make_tuple(x...));
|
||||
|
||||
// Do immediate check with fresh timestamp
|
||||
// Do immediate check with fresh timestamp - don't call loop() synchronously!
|
||||
// Let the event loop call it to avoid reentrancy issues
|
||||
if (this->process_queue_(now)) {
|
||||
// Only enable loop if we still have pending items
|
||||
this->enable_loop();
|
||||
@@ -499,7 +500,7 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
|
||||
}
|
||||
|
||||
Condition<Ts...> *condition_;
|
||||
std::forward_list<std::tuple<uint32_t, optional<uint32_t>, std::tuple<Ts...>>> var_queue_{};
|
||||
std::list<std::tuple<uint32_t, optional<uint32_t>, std::tuple<Ts...>>> var_queue_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class UpdateComponentAction : public Action<Ts...> {
|
||||
|
||||
+41
-14
@@ -36,6 +36,9 @@ namespace {
|
||||
struct ComponentErrorMessage {
|
||||
const Component *component;
|
||||
const char *message;
|
||||
// Track if message is flash pointer (needs LOG_STR_ARG) or RAM pointer
|
||||
// Remove before 2026.6.0 when deprecated const char* API is removed
|
||||
bool is_flash_ptr;
|
||||
};
|
||||
|
||||
struct ComponentPriorityOverride {
|
||||
@@ -49,6 +52,25 @@ std::unique_ptr<std::vector<ComponentErrorMessage>> component_error_messages;
|
||||
// Setup priority overrides - freed after setup completes
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::unique_ptr<std::vector<ComponentPriorityOverride>> setup_priority_overrides;
|
||||
|
||||
// Helper to store error messages - reduces duplication between deprecated and new API
|
||||
// Remove before 2026.6.0 when deprecated const char* API is removed
|
||||
void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) {
|
||||
// Lazy allocate the error messages vector if needed
|
||||
if (!component_error_messages) {
|
||||
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
|
||||
}
|
||||
// Check if this component already has an error message
|
||||
for (auto &entry : *component_error_messages) {
|
||||
if (entry.component == component) {
|
||||
entry.message = message;
|
||||
entry.is_flash_ptr = is_flash_ptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Add new error message
|
||||
component_error_messages->emplace_back(ComponentErrorMessage{component, message, is_flash_ptr});
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace setup_priority {
|
||||
@@ -143,16 +165,20 @@ void Component::call_dump_config() {
|
||||
if (this->is_failed()) {
|
||||
// Look up error message from global vector
|
||||
const char *error_msg = nullptr;
|
||||
bool is_flash_ptr = false;
|
||||
if (component_error_messages) {
|
||||
for (const auto &entry : *component_error_messages) {
|
||||
if (entry.component == this) {
|
||||
error_msg = entry.message;
|
||||
is_flash_ptr = entry.is_flash_ptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Log with appropriate format based on pointer type
|
||||
ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()),
|
||||
error_msg ? error_msg : LOG_STR_LITERAL("unspecified"));
|
||||
error_msg ? (is_flash_ptr ? LOG_STR_ARG((const LogString *) error_msg) : error_msg)
|
||||
: LOG_STR_LITERAL("unspecified"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +333,7 @@ void Component::status_set_warning(const LogString *message) {
|
||||
ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()),
|
||||
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
|
||||
}
|
||||
void Component::status_set_error() { this->status_set_error((const LogString *) nullptr); }
|
||||
void Component::status_set_error(const char *message) {
|
||||
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
|
||||
return;
|
||||
@@ -315,19 +342,19 @@ void Component::status_set_error(const char *message) {
|
||||
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
|
||||
message ? message : LOG_STR_LITERAL("unspecified"));
|
||||
if (message != nullptr) {
|
||||
// Lazy allocate the error messages vector if needed
|
||||
if (!component_error_messages) {
|
||||
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
|
||||
}
|
||||
// Check if this component already has an error message
|
||||
for (auto &entry : *component_error_messages) {
|
||||
if (entry.component == this) {
|
||||
entry.message = message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Add new error message
|
||||
component_error_messages->emplace_back(ComponentErrorMessage{this, message});
|
||||
store_component_error_message(this, message, false);
|
||||
}
|
||||
}
|
||||
void Component::status_set_error(const LogString *message) {
|
||||
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
|
||||
return;
|
||||
this->component_state_ |= STATUS_LED_ERROR;
|
||||
App.app_state_ |= STATUS_LED_ERROR;
|
||||
ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()),
|
||||
message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified"));
|
||||
if (message != nullptr) {
|
||||
// Store the LogString pointer directly (safe because LogString is always in flash/static memory)
|
||||
store_component_error_message(this, LOG_STR_ARG(message), true);
|
||||
}
|
||||
}
|
||||
void Component::status_clear_warning() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
@@ -157,7 +158,19 @@ class Component {
|
||||
*/
|
||||
virtual void mark_failed();
|
||||
|
||||
// Remove before 2026.6.0
|
||||
ESPDEPRECATED("Use mark_failed(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary "
|
||||
"strings. Will stop working in 2026.6.0",
|
||||
"2025.12.0")
|
||||
void mark_failed(const char *message) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
this->status_set_error(message);
|
||||
#pragma GCC diagnostic pop
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
void mark_failed(const LogString *message) {
|
||||
this->status_set_error(message);
|
||||
this->mark_failed();
|
||||
}
|
||||
@@ -216,7 +229,13 @@ class Component {
|
||||
void status_set_warning(const char *message = nullptr);
|
||||
void status_set_warning(const LogString *message);
|
||||
|
||||
void status_set_error(const char *message = nullptr);
|
||||
void status_set_error(); // Set error flag without message
|
||||
// Remove before 2026.6.0
|
||||
ESPDEPRECATED("Use status_set_error(LOG_STR(\"static string literal\")) instead. Do NOT use .c_str() from temporary "
|
||||
"strings. Will stop working in 2026.6.0",
|
||||
"2025.12.0")
|
||||
void status_set_error(const char *message);
|
||||
void status_set_error(const LogString *message);
|
||||
|
||||
void status_clear_warning();
|
||||
|
||||
|
||||
@@ -210,12 +210,14 @@
|
||||
#define USE_WEBSERVER_SORTING
|
||||
#define USE_WIFI_11KV_SUPPORT
|
||||
#define USE_WIFI_FAST_CONNECT
|
||||
#define USE_WIFI_RUNTIME_POWER_SAVE
|
||||
#define USB_HOST_MAX_REQUESTS 16
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
|
||||
#define USE_ETHERNET
|
||||
#define USE_ETHERNET_KSZ8081
|
||||
#define USE_ETHERNET_MANUAL_IP
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
+7
-1
@@ -343,7 +343,13 @@ def clean_build(clear_pio_cache: bool = True):
|
||||
def clean_all(configuration: list[str]):
|
||||
import shutil
|
||||
|
||||
data_dirs = [Path(dir) / ".esphome" for dir in configuration]
|
||||
data_dirs = []
|
||||
for config in configuration:
|
||||
item = Path(config)
|
||||
if item.is_file() and item.suffix in (".yaml", ".yml"):
|
||||
data_dirs.append(item.parent / ".esphome")
|
||||
else:
|
||||
data_dirs.append(item / ".esphome")
|
||||
if is_ha_addon():
|
||||
data_dirs.append(Path("/data"))
|
||||
if "ESPHOME_DATA_DIR" in os.environ:
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ pillow==11.3.0
|
||||
cairosvg==2.8.2
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==1.1.1
|
||||
bleak==2.0.0
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.0
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- wait_until:
|
||||
condition:
|
||||
api.connected:
|
||||
state_subscription_only: true
|
||||
- homeassistant.event:
|
||||
event: esphome.button_pressed
|
||||
data:
|
||||
|
||||
@@ -39,6 +39,15 @@ sensor:
|
||||
return 0.0;
|
||||
update_interval: 60s
|
||||
|
||||
text:
|
||||
- platform: template
|
||||
name: "Template text"
|
||||
optimistic: true
|
||||
min_length: 0
|
||||
max_length: 100
|
||||
mode: text
|
||||
initial_value: "Hello World"
|
||||
|
||||
text_sensor:
|
||||
- platform: version
|
||||
name: "ESPHome Version"
|
||||
@@ -52,6 +61,25 @@ text_sensor:
|
||||
return {"Goodbye (cruel) World"};
|
||||
update_interval: 60s
|
||||
|
||||
event:
|
||||
- platform: template
|
||||
name: "Template Event"
|
||||
id: template_event1
|
||||
event_types:
|
||||
- "custom_event_1"
|
||||
- "custom_event_2"
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Template Event Button"
|
||||
on_press:
|
||||
- logger.log: "Template Event Button pressed"
|
||||
- lambda: |-
|
||||
ESP_LOGD("template_event_button", "Template Event Button pressed");
|
||||
- event.trigger:
|
||||
id: template_event1
|
||||
event_type: custom_event_1
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
id: template_binary_sensor1
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
esp32_ble_tracker:
|
||||
|
||||
sensor:
|
||||
- platform: thermopro_ble
|
||||
mac_address: FE:74:B8:6A:97:B7
|
||||
temperature:
|
||||
name: "ThermoPro Temperature"
|
||||
humidity:
|
||||
name: "ThermoPro Humidity"
|
||||
battery_level:
|
||||
name: "ThermoPro Battery Level"
|
||||
signal_strength:
|
||||
name: "ThermoPro Signal Strength"
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -10,6 +10,10 @@ esphome:
|
||||
- logger.log: "Connected to WiFi!"
|
||||
on_error:
|
||||
- logger.log: "Failed to connect to WiFi!"
|
||||
- if:
|
||||
condition: wifi.ap_active
|
||||
then:
|
||||
- logger.log: "WiFi AP is active!"
|
||||
|
||||
wifi:
|
||||
networks:
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
psram:
|
||||
|
||||
# Tests the high performance request and release; requires the USE_WIFI_RUNTIME_POWER_SAVE define
|
||||
esphome:
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- "-DUSE_WIFI_RUNTIME_POWER_SAVE"
|
||||
on_boot:
|
||||
- then:
|
||||
- lambda: |-
|
||||
esphome::wifi::global_wifi_component->request_high_performance();
|
||||
esphome::wifi::global_wifi_component->release_high_performance();
|
||||
|
||||
wifi:
|
||||
use_psram: true
|
||||
min_auth_mode: WPA
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
esphome:
|
||||
name: test-script-delay-params
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
actions:
|
||||
# Test case from issue #12044: parent script with repeat calling child with delay
|
||||
- action: test_repeat_with_delay
|
||||
then:
|
||||
- logger.log: "=== TEST: Repeat loop calling script with delay and parameters ==="
|
||||
- script.execute: father_script
|
||||
|
||||
# Test case from issue #12043: script.wait with delayed child script
|
||||
- action: test_script_wait
|
||||
then:
|
||||
- logger.log: "=== TEST: script.wait with delayed child script ==="
|
||||
- script.execute: show_start_page
|
||||
- script.wait: show_start_page
|
||||
- logger.log: "After wait: script completed successfully"
|
||||
|
||||
# Test: Delay with different parameter types
|
||||
- action: test_delay_param_types
|
||||
then:
|
||||
- logger.log: "=== TEST: Delay with various parameter types ==="
|
||||
- script.execute:
|
||||
id: delay_with_int
|
||||
val: 42
|
||||
- delay: 50ms
|
||||
- script.execute:
|
||||
id: delay_with_string
|
||||
msg: "test message"
|
||||
- delay: 50ms
|
||||
- script.execute:
|
||||
id: delay_with_float
|
||||
num: 3.14
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
script:
|
||||
# Reproduces issue #12044: child script with conditional delay
|
||||
- id: son_script
|
||||
mode: single
|
||||
parameters:
|
||||
iteration: int
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Son script started with iteration %d"
|
||||
args: ['iteration']
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return iteration >= 5;'
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Son script delaying for iteration %d"
|
||||
args: ['iteration']
|
||||
- delay: 100ms
|
||||
- logger.log:
|
||||
format: "Son script finished with iteration %d"
|
||||
args: ['iteration']
|
||||
|
||||
# Reproduces issue #12044: parent script with repeat loop
|
||||
- id: father_script
|
||||
mode: single
|
||||
then:
|
||||
- repeat:
|
||||
count: 10
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Father iteration %d: calling son"
|
||||
args: ['iteration']
|
||||
- script.execute:
|
||||
id: son_script
|
||||
iteration: !lambda 'return iteration;'
|
||||
- script.wait: son_script
|
||||
- logger.log:
|
||||
format: "Father iteration %d: son finished, wait returned"
|
||||
args: ['iteration']
|
||||
|
||||
# Reproduces issue #12043: script.wait hangs
|
||||
- id: show_start_page
|
||||
mode: single
|
||||
then:
|
||||
- logger.log: "Start page: beginning"
|
||||
- delay: 100ms
|
||||
- logger.log: "Start page: after delay"
|
||||
- delay: 100ms
|
||||
- logger.log: "Start page: completed"
|
||||
|
||||
# Test delay with int parameter
|
||||
- id: delay_with_int
|
||||
mode: single
|
||||
parameters:
|
||||
val: int
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Int test: before delay, val=%d"
|
||||
args: ['val']
|
||||
- delay: 50ms
|
||||
- logger.log:
|
||||
format: "Int test: after delay, val=%d"
|
||||
args: ['val']
|
||||
|
||||
# Test delay with string parameter
|
||||
- id: delay_with_string
|
||||
mode: single
|
||||
parameters:
|
||||
msg: string
|
||||
then:
|
||||
- logger.log:
|
||||
format: "String test: before delay, msg=%s"
|
||||
args: ['msg.c_str()']
|
||||
- delay: 50ms
|
||||
- logger.log:
|
||||
format: "String test: after delay, msg=%s"
|
||||
args: ['msg.c_str()']
|
||||
|
||||
# Test delay with float parameter
|
||||
- id: delay_with_float
|
||||
mode: single
|
||||
parameters:
|
||||
num: float
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Float test: before delay, num=%.2f"
|
||||
args: ['num']
|
||||
- delay: 50ms
|
||||
- logger.log:
|
||||
format: "Float test: after delay, num=%.2f"
|
||||
args: ['num']
|
||||
@@ -0,0 +1,82 @@
|
||||
esphome:
|
||||
name: test-wait-until-ordering
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
actions:
|
||||
- action: test_wait_until_fifo
|
||||
then:
|
||||
- logger.log: "=== TEST: wait_until should execute in FIFO order ==="
|
||||
- globals.set:
|
||||
id: gate_open
|
||||
value: 'false'
|
||||
- delay: 100ms
|
||||
# Start multiple parallel executions of coordinator script
|
||||
# Each will call the shared waiter script, queueing in same wait_until
|
||||
- script.execute: coordinator_0
|
||||
- script.execute: coordinator_1
|
||||
- script.execute: coordinator_2
|
||||
- script.execute: coordinator_3
|
||||
- script.execute: coordinator_4
|
||||
# Give scripts time to reach wait_until and queue
|
||||
- delay: 200ms
|
||||
- logger.log: "Opening gate - all wait_until should complete now"
|
||||
- globals.set:
|
||||
id: gate_open
|
||||
value: 'true'
|
||||
- delay: 500ms
|
||||
- logger.log: "Test complete"
|
||||
|
||||
globals:
|
||||
- id: gate_open
|
||||
type: bool
|
||||
initial_value: 'false'
|
||||
|
||||
script:
|
||||
# Shared waiter with single wait_until action (all coordinators call this)
|
||||
- id: waiter
|
||||
mode: parallel
|
||||
parameters:
|
||||
iter: int
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("main", "Queueing iteration %d", iter);'
|
||||
- wait_until:
|
||||
condition:
|
||||
lambda: 'return id(gate_open);'
|
||||
timeout: 5s
|
||||
- lambda: 'ESP_LOGD("main", "Completed iteration %d", iter);'
|
||||
|
||||
# Coordinator scripts - each calls shared waiter with different iteration number
|
||||
- id: coordinator_0
|
||||
then:
|
||||
- script.execute:
|
||||
id: waiter
|
||||
iter: 0
|
||||
|
||||
- id: coordinator_1
|
||||
then:
|
||||
- script.execute:
|
||||
id: waiter
|
||||
iter: 1
|
||||
|
||||
- id: coordinator_2
|
||||
then:
|
||||
- script.execute:
|
||||
id: waiter
|
||||
iter: 2
|
||||
|
||||
- id: coordinator_3
|
||||
then:
|
||||
- script.execute:
|
||||
id: waiter
|
||||
iter: 3
|
||||
|
||||
- id: coordinator_4
|
||||
then:
|
||||
- script.execute:
|
||||
id: waiter
|
||||
iter: 4
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Integration test for script.wait FIFO ordering (issues #12043, #12044).
|
||||
|
||||
This test verifies that ScriptWaitAction processes queued items in FIFO order.
|
||||
|
||||
PR #7972 introduced bugs in ScriptWaitAction:
|
||||
- Used emplace_front() causing LIFO ordering instead of FIFO
|
||||
- Called loop() synchronously causing reentrancy issues
|
||||
- Used while loop processing entire queue causing infinite loops
|
||||
|
||||
These bugs manifested as:
|
||||
- Scripts becoming "zombies" (stuck in running state)
|
||||
- script.wait hanging forever
|
||||
- Incorrect execution order
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_script_delay_with_params(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that script.wait processes queued items in FIFO order.
|
||||
|
||||
This reproduces issues #12043 and #12044 where scripts would hang or become
|
||||
zombies due to LIFO ordering bugs in ScriptWaitAction from PR #7972.
|
||||
"""
|
||||
test_complete = asyncio.Event()
|
||||
|
||||
# Patterns to match in logs
|
||||
father_calling_pattern = re.compile(r"Father iteration (\d+): calling son")
|
||||
son_started_pattern = re.compile(r"Son script started with iteration (\d+)")
|
||||
son_delaying_pattern = re.compile(r"Son script delaying for iteration (\d+)")
|
||||
son_finished_pattern = re.compile(r"Son script finished with iteration (\d+)")
|
||||
father_wait_returned_pattern = re.compile(
|
||||
r"Father iteration (\d+): son finished, wait returned"
|
||||
)
|
||||
|
||||
# Track which iterations completed
|
||||
father_calling = set()
|
||||
son_started = set()
|
||||
son_delaying = set()
|
||||
son_finished = set()
|
||||
wait_returned = set()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
if test_complete.is_set():
|
||||
return
|
||||
|
||||
if mo := father_calling_pattern.search(line):
|
||||
father_calling.add(int(mo.group(1)))
|
||||
elif mo := son_started_pattern.search(line):
|
||||
son_started.add(int(mo.group(1)))
|
||||
elif mo := son_delaying_pattern.search(line):
|
||||
son_delaying.add(int(mo.group(1)))
|
||||
elif mo := son_finished_pattern.search(line):
|
||||
son_finished.add(int(mo.group(1)))
|
||||
elif mo := father_wait_returned_pattern.search(line):
|
||||
iteration = int(mo.group(1))
|
||||
wait_returned.add(iteration)
|
||||
# Test completes when iteration 9 finishes
|
||||
if iteration == 9:
|
||||
test_complete.set()
|
||||
|
||||
# Run with log monitoring
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "test-script-delay-params"
|
||||
|
||||
# Get services
|
||||
_, services = await client.list_entities_services()
|
||||
test_service = next(
|
||||
(s for s in services if s.name == "test_repeat_with_delay"), None
|
||||
)
|
||||
assert test_service is not None, "test_repeat_with_delay service not found"
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(test_service, {})
|
||||
|
||||
# Wait for test to complete (10 iterations * ~100ms each + margin)
|
||||
try:
|
||||
await asyncio.wait_for(test_complete.wait(), timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test timed out. Completed iterations: {sorted(wait_returned)}. "
|
||||
f"This likely indicates the script became a zombie (issue #12044)."
|
||||
)
|
||||
|
||||
# Verify all 10 iterations completed successfully
|
||||
expected_iterations = set(range(10))
|
||||
assert father_calling == expected_iterations, "Not all iterations started"
|
||||
assert son_started == expected_iterations, (
|
||||
"Son script not started for all iterations"
|
||||
)
|
||||
assert son_finished == expected_iterations, (
|
||||
"Son script not finished for all iterations"
|
||||
)
|
||||
assert wait_returned == expected_iterations, (
|
||||
"script.wait did not return for all iterations"
|
||||
)
|
||||
|
||||
# Verify delays were triggered for iterations >= 5
|
||||
expected_delays = set(range(5, 10))
|
||||
assert son_delaying == expected_delays, (
|
||||
"Delays not triggered for iterations >= 5"
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Integration test for wait_until FIFO ordering.
|
||||
|
||||
This test verifies that when multiple wait_until actions are queued,
|
||||
they execute in FIFO (First In First Out) order, not LIFO.
|
||||
|
||||
PR #7972 introduced a bug where emplace_front() was used, causing
|
||||
LIFO ordering which is incorrect.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wait_until_fifo_ordering(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that wait_until executes queued items in FIFO order.
|
||||
|
||||
With the bug (using emplace_front), the order would be 4,3,2,1,0 (LIFO).
|
||||
With the fix (using emplace_back), the order should be 0,1,2,3,4 (FIFO).
|
||||
"""
|
||||
test_complete = asyncio.Event()
|
||||
|
||||
# Track completion order
|
||||
completed_order = []
|
||||
|
||||
# Patterns to match
|
||||
queuing_pattern = re.compile(r"Queueing iteration (\d+)")
|
||||
completed_pattern = re.compile(r"Completed iteration (\d+)")
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for completion order."""
|
||||
if test_complete.is_set():
|
||||
return
|
||||
|
||||
if mo := queuing_pattern.search(line):
|
||||
iteration = int(mo.group(1))
|
||||
|
||||
elif mo := completed_pattern.search(line):
|
||||
iteration = int(mo.group(1))
|
||||
completed_order.append(iteration)
|
||||
|
||||
# Test completes when all 5 have completed
|
||||
if len(completed_order) == 5:
|
||||
test_complete.set()
|
||||
|
||||
# Run with log monitoring
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "test-wait-until-ordering"
|
||||
|
||||
# Get services
|
||||
_, services = await client.list_entities_services()
|
||||
test_service = next(
|
||||
(s for s in services if s.name == "test_wait_until_fifo"), None
|
||||
)
|
||||
assert test_service is not None, "test_wait_until_fifo service not found"
|
||||
|
||||
# Execute the test
|
||||
client.execute_service(test_service, {})
|
||||
|
||||
# Wait for test to complete
|
||||
try:
|
||||
await asyncio.wait_for(test_complete.wait(), timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test timed out. Completed order: {completed_order}. "
|
||||
f"Expected 5 completions but got {len(completed_order)}."
|
||||
)
|
||||
|
||||
# Verify FIFO order
|
||||
expected_order = [0, 1, 2, 3, 4]
|
||||
assert completed_order == expected_order, (
|
||||
f"Unexpected order: {completed_order}. "
|
||||
f"Expected FIFO order: {expected_order}"
|
||||
)
|
||||
@@ -737,6 +737,37 @@ def test_write_cpp_with_duplicate_markers(
|
||||
write_cpp("// New code")
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
def test_clean_all_with_yaml_file(
|
||||
mock_core: MagicMock,
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test clean_all with a .yaml file uses parent directory."""
|
||||
# Create config directory with yaml file
|
||||
config_dir = tmp_path / "config"
|
||||
config_dir.mkdir()
|
||||
yaml_file = config_dir / "test.yaml"
|
||||
yaml_file.write_text("esphome:\n name: test\n")
|
||||
|
||||
build_dir = config_dir / ".esphome"
|
||||
build_dir.mkdir()
|
||||
(build_dir / "dummy.txt").write_text("x")
|
||||
|
||||
from esphome.writer import clean_all
|
||||
|
||||
with caplog.at_level("INFO"):
|
||||
clean_all([str(yaml_file)])
|
||||
|
||||
# Verify .esphome directory still exists but contents cleaned
|
||||
assert build_dir.exists()
|
||||
assert not (build_dir / "dummy.txt").exists()
|
||||
|
||||
# Verify logging mentions the build dir
|
||||
assert "Cleaning" in caplog.text
|
||||
assert str(build_dir) in caplog.text
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
def test_clean_all(
|
||||
mock_core: MagicMock,
|
||||
|
||||
Reference in New Issue
Block a user