1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-17 07:15:48 +00:00

Compare commits

...

25 Commits

Author SHA1 Message Date
J. Nick Koston
c18a0f538f preen 2025-10-25 15:05:13 -07:00
J. Nick Koston
7e31149584 readable 2025-10-25 15:02:56 -07:00
J. Nick Koston
2c6b9d3826 no race window 2025-10-25 14:56:59 -07:00
J. Nick Koston
527039211e fix off by one 2025-10-25 14:53:48 -07:00
J. Nick Koston
1ea17607f3 fix race. 2025-10-25 14:44:36 -07:00
J. Nick Koston
6cfca87ca7 safer 2025-10-25 14:39:28 -07:00
J. Nick Koston
8bd640875f touch ups 2025-10-25 14:20:57 -07:00
J. Nick Koston
1e17ed8c1e narrow scope 2025-10-25 13:51:29 -07:00
J. Nick Koston
d3b4b11302 narrow scope 2025-10-25 13:50:16 -07:00
J. Nick Koston
c5ff19d3ab [usb_host] Fix atomic memory ordering in transfer slot allocation 2025-10-25 13:43:53 -07:00
J. Nick Koston
e212ed024d [sntp] Replace std::vector<std::string> with std::array<const char*> to save heap memory (#11525) 2025-10-25 10:00:43 -07:00
Jonathan Swoboda
5fdd90c71a [esp32] Add IDF 5.4.3 to platform list and switch to tar.xz (#11528) 2025-10-25 00:27:39 -07:00
Jonathan Swoboda
6929bdb415 [remote_transmitter] Remove delays and use RMT instead (#11505) 2025-10-24 15:01:30 -04:00
J. Nick Koston
2c85ba037e [http_request] Pass collect_headers by const reference instead of by value (#11494) 2025-10-23 20:01:48 -07:00
J. Nick Koston
2440bbdceb [core][sensor] Eliminate redundant default value setters in generated code (#11495) 2025-10-23 20:01:23 -07:00
Jesse Hills
3ac8eb7696 Merge branch 'release' into dev 2025-10-24 14:08:56 +13:00
Jesse Hills
6a478b9070 Merge pull request #11506 from esphome/bump-2025.10.3
2025.10.3
2025-10-24 14:08:12 +13:00
Jesse Hills
a32a1d11fb Bump version to 2025.10.3 2025-10-24 07:51:38 +13:00
Markus
daeb8ef88c [core] handle mixed IP and DNS addresses correctly in resolve_ip_address (#11503)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-24 07:51:38 +13:00
Anton Sergunov
febee437d6 [uart] Make rx pin respect pullup and pulldown settings (#9248) 2025-10-24 07:51:38 +13:00
Peter Zich
de2f475dbd [hdc1080] Make HDC1080_CMD_CONFIGURATION failure a warning (and log it) (#11355)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-24 07:51:38 +13:00
Markus
fa3ec6f732 [core] handle mixed IP and DNS addresses correctly in resolve_ip_address (#11503)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-23 11:32:07 -07:00
dependabot[bot]
e490aec6b4 Bump ruamel-yaml from 0.18.15 to 0.18.16 (#11482)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-23 10:25:36 -07:00
J. Nick Koston
8da8095a6a [tests] Isolate gps component to prevent TinyGPSPlus millis() conflicts (#11499) 2025-10-23 10:11:13 -07:00
Patrick
ab14c0cd72 [pipsolar] improve sensor readout in HA, set unknown state on timeout / error (#10292)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-23 11:32:02 -04:00
27 changed files with 484 additions and 161 deletions

View File

@@ -304,9 +304,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
# a PIO platformio/framework-espidf value
if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
ext = "tar.xz"
else:
ext = "zip"
if release:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}"
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}"
def _is_framework_url(source: str) -> str:
@@ -355,6 +359,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),

View File

@@ -169,7 +169,7 @@ class HttpRequestComponent : public Component {
protected:
virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
const std::string &body, const std::list<Header> &request_headers,
std::set<std::string> collect_headers) = 0;
const std::set<std::string> &collect_headers) = 0;
const char *useragent_{nullptr};
bool follow_redirects_{};
uint16_t redirect_limit_{};

View File

@@ -17,7 +17,7 @@ static const char *const TAG = "http_request.arduino";
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> collect_headers) {
const std::set<std::string> &collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");

View File

@@ -33,7 +33,7 @@ class HttpRequestArduino : public HttpRequestComponent {
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> collect_headers) override;
const std::set<std::string> &collect_headers) override;
};
} // namespace http_request

View File

@@ -20,7 +20,7 @@ static const char *const TAG = "http_request.host";
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> response_headers) {
const std::set<std::string> &response_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");

View File

@@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> response_headers) override;
const std::set<std::string> &response_headers) override;
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
protected:

View File

@@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method,
const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> collect_headers) {
const std::set<std::string> &collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");

View File

@@ -39,7 +39,7 @@ class HttpRequestIDF : public HttpRequestComponent {
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> collect_headers) override;
const std::set<std::string> &collect_headers) override;
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{};

View File

@@ -38,7 +38,6 @@ void Pipsolar::loop() {
}
if (this->state_ == STATE_COMMAND_COMPLETE) {
if (this->check_incoming_length_(4)) {
ESP_LOGD(TAG, "response length for command OK");
if (this->check_incoming_crc_()) {
// crc ok
if (this->read_buffer_[1] == 'A' && this->read_buffer_[2] == 'C' && this->read_buffer_[3] == 'K') {
@@ -49,15 +48,15 @@ void Pipsolar::loop() {
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
} else {
// crc failed
// no log message necessary, check_incoming_crc_() logs
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
}
} else {
ESP_LOGD(TAG, "response length for command %s not OK: with length %zu",
ESP_LOGD(TAG, "command %s response length not OK: with length %zu",
this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_);
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
@@ -66,46 +65,10 @@ void Pipsolar::loop() {
}
if (this->state_ == STATE_POLL_CHECKED) {
switch (this->enabled_polling_commands_[this->last_polling_command_].identifier) {
case POLLING_QPIRI:
ESP_LOGD(TAG, "Decode QPIRI");
handle_qpiri_((const char *) this->read_buffer_);
this->state_ = STATE_IDLE;
break;
case POLLING_QPIGS:
ESP_LOGD(TAG, "Decode QPIGS");
handle_qpigs_((const char *) this->read_buffer_);
this->state_ = STATE_IDLE;
break;
case POLLING_QMOD:
ESP_LOGD(TAG, "Decode QMOD");
handle_qmod_((const char *) this->read_buffer_);
this->state_ = STATE_IDLE;
break;
case POLLING_QFLAG:
ESP_LOGD(TAG, "Decode QFLAG");
handle_qflag_((const char *) this->read_buffer_);
this->state_ = STATE_IDLE;
break;
case POLLING_QPIWS:
ESP_LOGD(TAG, "Decode QPIWS");
handle_qpiws_((const char *) this->read_buffer_);
this->state_ = STATE_IDLE;
break;
case POLLING_QT:
ESP_LOGD(TAG, "Decode QT");
handle_qt_((const char *) this->read_buffer_);
this->state_ = STATE_IDLE;
break;
case POLLING_QMN:
ESP_LOGD(TAG, "Decode QMN");
handle_qmn_((const char *) this->read_buffer_);
this->state_ = STATE_IDLE;
break;
default:
this->state_ = STATE_IDLE;
break;
}
ESP_LOGD(TAG, "poll %s decode", this->enabled_polling_commands_[this->last_polling_command_].command);
this->handle_poll_response_(this->enabled_polling_commands_[this->last_polling_command_].identifier,
(const char *) this->read_buffer_);
this->state_ = STATE_IDLE;
return;
}
@@ -113,6 +76,8 @@ void Pipsolar::loop() {
if (this->check_incoming_crc_()) {
if (this->read_buffer_[0] == '(' && this->read_buffer_[1] == 'N' && this->read_buffer_[2] == 'A' &&
this->read_buffer_[3] == 'K') {
ESP_LOGD(TAG, "poll %s NACK", this->enabled_polling_commands_[this->last_polling_command_].command);
this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier);
this->state_ = STATE_IDLE;
return;
}
@@ -121,6 +86,9 @@ void Pipsolar::loop() {
this->state_ = STATE_POLL_CHECKED;
return;
} else {
// crc failed
// no log message necessary, check_incoming_crc_() logs
this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier);
this->state_ = STATE_IDLE;
}
}
@@ -158,21 +126,19 @@ void Pipsolar::loop() {
// command timeout
const char *command = this->command_queue_[this->command_queue_position_].c_str();
this->command_start_millis_ = millis();
ESP_LOGD(TAG, "timeout command from queue: %s", command);
ESP_LOGD(TAG, "command %s timeout", command);
this->command_queue_[this->command_queue_position_] = std::string("");
this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH;
this->state_ = STATE_IDLE;
return;
} else {
}
}
if (this->state_ == STATE_POLL) {
if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) {
// command timeout
ESP_LOGD(TAG, "timeout command to poll: %s",
this->enabled_polling_commands_[this->last_polling_command_].command);
ESP_LOGD(TAG, "poll %s timeout", this->enabled_polling_commands_[this->last_polling_command_].command);
this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier);
this->state_ = STATE_IDLE;
} else {
}
}
}
@@ -187,7 +153,6 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) {
uint8_t Pipsolar::check_incoming_crc_() {
uint16_t crc16;
crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3);
ESP_LOGD(TAG, "checking crc on incoming message");
if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] &&
((uint8_t) ((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) {
ESP_LOGD(TAG, "CRC OK");
@@ -253,7 +218,7 @@ bool Pipsolar::send_next_poll_() {
this->write(((uint8_t) ((crc16) &0xff))); // lowbyte
// end Byte
this->write(0x0D);
ESP_LOGD(TAG, "Sending polling command : %s with length %d",
ESP_LOGD(TAG, "Sending polling command: %s with length %d",
this->enabled_polling_commands_[this->last_polling_command_].command,
this->enabled_polling_commands_[this->last_polling_command_].length);
return true;
@@ -274,6 +239,38 @@ void Pipsolar::queue_command(const std::string &command) {
ESP_LOGD(TAG, "Command queue full dropping command: %s", command.c_str());
}
void Pipsolar::handle_poll_response_(ENUMPollingCommand polling_command, const char *message) {
switch (polling_command) {
case POLLING_QPIRI:
handle_qpiri_(message);
break;
case POLLING_QPIGS:
handle_qpigs_(message);
break;
case POLLING_QMOD:
handle_qmod_(message);
break;
case POLLING_QFLAG:
handle_qflag_(message);
break;
case POLLING_QPIWS:
handle_qpiws_(message);
break;
case POLLING_QT:
handle_qt_(message);
break;
case POLLING_QMN:
handle_qmn_(message);
break;
default:
break;
}
}
void Pipsolar::handle_poll_error_(ENUMPollingCommand polling_command) {
// handlers are designed in a way that an empty message sets all sensors to unknown
this->handle_poll_response_(polling_command, "");
}
void Pipsolar::handle_qpiri_(const char *message) {
if (this->last_qpiri_) {
this->last_qpiri_->publish_state(message);

View File

@@ -204,6 +204,9 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent {
bool send_next_command_();
bool send_next_poll_();
void handle_poll_response_(ENUMPollingCommand polling_command, const char *message);
void handle_poll_error_(ENUMPollingCommand polling_command);
// these handlers are designed in a way that an empty message sets all sensors to unknown
void handle_qpiri_(const char *message);
void handle_qpigs_(const char *message);
void handle_qmod_(const char *message);

View File

@@ -4,11 +4,18 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_BATTERY_VOLTAGE,
CONF_BUS_VOLTAGE,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE,
ICON_BATTERY,
ICON_CURRENT_AC,
ICON_FLASH,
ICON_GAUGE,
STATE_CLASS_MEASUREMENT,
UNIT_AMPERE,
UNIT_CELSIUS,
UNIT_HERTZ,
@@ -22,6 +29,10 @@ from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA
DEPENDENCIES = ["uart"]
ICON_SOLAR_POWER = "mdi:solar-power"
ICON_SOLAR_PANEL = "mdi:solar-panel"
ICON_CURRENT_DC = "mdi:current-dc"
# QPIRI sensors
CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage"
CONF_GRID_RATING_CURRENT = "grid_rating_current"
@@ -75,16 +86,19 @@ TYPES = {
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_GRID_RATING_CURRENT: sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=1,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
@@ -98,11 +112,12 @@ TYPES = {
),
CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=1,
accuracy_decimals=0,
device_class=DEVICE_CLASS_APPARENT_POWER,
),
CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
),
CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema(
@@ -131,124 +146,151 @@ TYPES = {
device_class=DEVICE_CLASS_VOLTAGE,
),
CONF_BATTERY_TYPE: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=1,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
),
CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=1,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
),
CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_PARALLEL_MAX_NUM: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_MACHINE_TYPE: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_TOPOLOGY: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_OUTPUT_MODE: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema(
accuracy_decimals=1,
),
CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_PV_POWER_BALANCE: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_GRID_VOLTAGE: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_GRID_FREQUENCY: sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
icon=ICON_CURRENT_AC,
accuracy_decimals=1,
device_class=DEVICE_CLASS_FREQUENCY,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema(
unit_of_measurement=UNIT_HERTZ,
icon=ICON_CURRENT_AC,
accuracy_decimals=1,
device_class=DEVICE_CLASS_FREQUENCY,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=1,
accuracy_decimals=0,
device_class=DEVICE_CLASS_APPARENT_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
icon=ICON_GAUGE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_BUS_VOLTAGE: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
icon=ICON_FLASH,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_BATTERY_VOLTAGE: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
icon=ICON_BATTERY,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=1,
icon=ICON_CURRENT_DC,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
accuracy_decimals=0,
device_class=DEVICE_CLASS_BATTERY,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
icon=ICON_SOLAR_PANEL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
icon=ICON_SOLAR_PANEL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=1,
icon=ICON_CURRENT_DC,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
@@ -256,12 +298,14 @@ TYPES = {
device_class=DEVICE_CLASS_VOLTAGE,
),
CONF_EEPROM_VERSION: sensor.sensor_schema(
accuracy_decimals=1,
accuracy_decimals=0,
),
CONF_PV_CHARGING_POWER: sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
icon=ICON_SOLAR_POWER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
}

View File

@@ -12,6 +12,25 @@
namespace esphome {
namespace remote_transmitter {
#ifdef USE_ESP32
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
// IDF version 5.5.1 and above is required because of a bug in
// the RMT encoder: https://github.com/espressif/esp-idf/issues/17244
typedef union { // NOLINT(modernize-use-using)
struct {
uint16_t duration : 15;
uint16_t level : 1;
};
uint16_t val;
} rmt_symbol_half_t;
struct RemoteTransmitterComponentStore {
uint32_t times{0};
uint32_t index{0};
};
#endif
#endif
class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
public Component
#ifdef USE_ESP32
@@ -56,9 +75,14 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
#ifdef USE_ESP32
void configure_rmt_();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
RemoteTransmitterComponentStore store_{};
std::vector<rmt_symbol_half_t> rmt_temp_;
#else
std::vector<rmt_symbol_word_t> rmt_temp_;
#endif
uint32_t current_carrier_frequency_{38000};
bool initialized_{false};
std::vector<rmt_symbol_word_t> rmt_temp_;
bool with_dma_{false};
bool eot_level_{false};
rmt_channel_handle_t channel_{NULL};

View File

@@ -10,6 +10,46 @@ namespace remote_transmitter {
static const char *const TAG = "remote_transmitter";
// Maximum RMT symbol duration (15-bit field)
static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free,
rmt_symbol_word_t *symbols, bool *done, void *arg) {
auto *store = static_cast<RemoteTransmitterComponentStore *>(arg);
const auto *encoded = static_cast<const rmt_symbol_half_t *>(data);
size_t length = size / sizeof(rmt_symbol_half_t);
size_t count = 0;
// copy symbols
for (size_t i = 0; i < free; i++) {
uint16_t sym_0 = encoded[store->index++].val;
if (store->index >= length) {
store->index = 0;
store->times--;
if (store->times == 0) {
*done = true;
symbols[count++].val = sym_0;
return count;
}
}
uint16_t sym_1 = encoded[store->index++].val;
if (store->index >= length) {
store->index = 0;
store->times--;
if (store->times == 0) {
*done = true;
symbols[count++].val = sym_0 | (sym_1 << 16);
return count;
}
}
symbols[count++].val = sym_0 | (sym_1 << 16);
}
*done = false;
return count;
}
#endif
void RemoteTransmitterComponent::setup() {
this->inverted_ = this->pin_->is_inverted();
this->configure_rmt_();
@@ -34,6 +74,17 @@ void RemoteTransmitterComponent::dump_config() {
}
void RemoteTransmitterComponent::digital_write(bool value) {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
rmt_symbol_half_t symbol = {
.duration = 1,
.level = value,
};
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.flags.eot_level = value;
this->store_.times = 1;
this->store_.index = 0;
#else
rmt_symbol_word_t symbol = {
.duration0 = 1,
.level0 = value,
@@ -42,8 +93,8 @@ void RemoteTransmitterComponent::digital_write(bool value) {
};
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.loop_count = 0;
config.flags.eot_level = value;
#endif
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
@@ -90,6 +141,20 @@ void RemoteTransmitterComponent::configure_rmt_() {
gpio_pullup_dis(gpio_num_t(this->pin_->get_pin()));
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
rmt_simple_encoder_config_t encoder;
memset(&encoder, 0, sizeof(encoder));
encoder.callback = encoder_callback;
encoder.arg = &this->store_;
encoder.min_chunk_size = 1;
error = rmt_new_simple_encoder(&encoder, &this->encoder_);
if (error != ESP_OK) {
this->error_code_ = error;
this->error_string_ = "in rmt_new_simple_encoder";
this->mark_failed();
return;
}
#else
rmt_copy_encoder_config_t encoder;
memset(&encoder, 0, sizeof(encoder));
error = rmt_new_copy_encoder(&encoder, &this->encoder_);
@@ -99,6 +164,7 @@ void RemoteTransmitterComponent::configure_rmt_() {
this->mark_failed();
return;
}
#endif
error = rmt_enable(this->channel_);
if (error != ESP_OK) {
@@ -130,6 +196,79 @@ void RemoteTransmitterComponent::configure_rmt_() {
}
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->is_failed()) {
return;
}
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
this->configure_rmt_();
}
this->rmt_temp_.clear();
this->rmt_temp_.reserve(this->temp_.get_data().size() + 1);
// encode any delay at the start of the buffer to simplify the encoder callback
// this will be skipped the first time around
send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait));
while (send_wait > 0) {
int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX));
this->rmt_temp_.push_back({
.duration = static_cast<uint16_t>(duration),
.level = static_cast<uint16_t>(this->eot_level_),
});
send_wait -= duration;
}
// encode data
size_t offset = this->rmt_temp_.size();
for (int32_t value : this->temp_.get_data()) {
bool level = value >= 0;
if (!level) {
value = -value;
}
value = this->from_microseconds_(static_cast<uint32_t>(value));
while (value > 0) {
int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX));
this->rmt_temp_.push_back({
.duration = static_cast<uint16_t>(duration),
.level = static_cast<uint16_t>(level ^ this->inverted_),
});
value -= duration;
}
}
if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) {
ESP_LOGE(TAG, "Empty data");
return;
}
this->transmit_trigger_->trigger();
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.flags.eot_level = this->eot_level_;
this->store_.times = send_times;
this->store_.index = offset;
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(),
this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error));
this->status_set_warning();
} else {
this->status_clear_warning();
}
error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
this->complete_trigger_->trigger();
}
#else
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
if (this->is_failed())
return;
@@ -151,7 +290,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
val = this->from_microseconds_(static_cast<uint32_t>(val));
do {
int32_t item = std::min(val, int32_t(32767));
int32_t item = std::min(val, int32_t(RMT_SYMBOL_DURATION_MAX));
val -= item;
if (rmt_i % 2 == 0) {
@@ -180,7 +319,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
for (uint32_t i = 0; i < send_times; i++) {
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.loop_count = 0;
config.flags.eot_level = this->eot_level_;
esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(),
this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config);
@@ -200,6 +338,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
}
this->complete_trigger_->trigger();
}
#endif
} // namespace remote_transmitter
} // namespace esphome

View File

@@ -878,7 +878,9 @@ async def setup_sensor_core_(var, config):
cg.add(var.set_unit_of_measurement(unit_of_measurement))
if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None:
cg.add(var.set_accuracy_decimals(accuracy_decimals))
cg.add(var.set_force_update(config[CONF_FORCE_UPDATE]))
# Only set force_update if True (default is False)
if config[CONF_FORCE_UPDATE]:
cg.add(var.set_force_update(True))
if config.get(CONF_FILTERS): # must exist and not be empty
filters = await build_filters(config[CONF_FILTERS])
cg.add(var.set_filters(filters))

View File

@@ -27,7 +27,7 @@ void SNTPComponent::setup() {
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
size_t i = 0;
for (auto &server : this->servers_) {
esp_sntp_setservername(i++, server.c_str());
esp_sntp_setservername(i++, server);
}
esp_sntp_set_sync_interval(this->get_update_interval());
esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) {
@@ -42,7 +42,7 @@ void SNTPComponent::setup() {
size_t i = 0;
for (auto &server : this->servers_) {
sntp_setservername(i++, server.c_str());
sntp_setservername(i++, server);
}
#if defined(USE_ESP8266)
@@ -59,7 +59,7 @@ void SNTPComponent::dump_config() {
ESP_LOGCONFIG(TAG, "SNTP Time:");
size_t i = 0;
for (auto &server : this->servers_) {
ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server.c_str());
ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server);
}
}
void SNTPComponent::update() {

View File

@@ -2,10 +2,14 @@
#include "esphome/core/component.h"
#include "esphome/components/time/real_time_clock.h"
#include <array>
namespace esphome {
namespace sntp {
// Server count is calculated at compile time by Python codegen
// SNTP_SERVER_COUNT will always be defined
/// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol.
///
/// \note
@@ -14,10 +18,7 @@ namespace sntp {
/// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
class SNTPComponent : public time::RealTimeClock {
public:
SNTPComponent(const std::vector<std::string> &servers) : servers_(servers) {}
// Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would
// have had no effect anyway, and making the strings immutable avoids the need to strdup their contents.
SNTPComponent(const std::array<const char *, SNTP_SERVER_COUNT> &servers) : servers_(servers) {}
void setup() override;
void dump_config() override;
@@ -29,7 +30,10 @@ class SNTPComponent : public time::RealTimeClock {
void time_synced();
protected:
std::vector<std::string> servers_;
// Store const char pointers to string literals
// ESP8266: strings in rodata (RAM), but avoids std::string overhead (~24 bytes each)
// Other platforms: strings in flash
std::array<const char *, SNTP_SERVER_COUNT> servers_;
bool has_time_{false};
#if defined(USE_ESP32)

View File

@@ -43,6 +43,11 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
servers = config[CONF_SERVERS]
# Define server count at compile time
cg.add_define("SNTP_SERVER_COUNT", len(servers))
# Pass string literals to constructor - stored in flash/rodata by compiler
var = cg.new_Pvariable(config[CONF_ID], servers)
await cg.register_component(var, config)

View File

@@ -82,6 +82,12 @@ struct TransferStatus {
using transfer_cb_t = std::function<void(const TransferStatus &)>;
enum TransferResult : uint8_t {
TRANSFER_OK = 0,
TRANSFER_ERROR_NO_SLOTS,
TRANSFER_ERROR_SUBMIT_FAILED,
};
class USBClient;
// struct used to capture all data needed for a transfer
@@ -134,7 +140,7 @@ class USBClient : public Component {
void on_opened(uint8_t addr);
void on_removed(usb_device_handle_t handle);
void control_transfer_callback(const usb_transfer_t *xfer) const;
void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
TransferResult transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
void dump_config() override;
void release_trq(TransferRequest *trq);

View File

@@ -334,7 +334,7 @@ static void control_callback(const usb_transfer_t *xfer) {
// This multi-threaded access is intentional for performance - USB task can
// immediately restart transfers without waiting for main loop scheduling.
TransferRequest *USBClient::get_trq_() {
trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire);
// Find first available slot (bit = 0) and try to claim it atomically
// We use a while loop to allow retrying the same slot after CAS failure
@@ -443,14 +443,15 @@ static void transfer_callback(usb_transfer_t *xfer) {
* @param ep_address The endpoint address.
* @param callback The callback function to be called when the transfer is complete.
* @param length The length of the data to be transferred.
* @return TransferResult indicating success or specific failure reason
*
* @throws None.
*/
void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
auto *trq = this->get_trq_();
if (trq == nullptr) {
ESP_LOGE(TAG, "Too many requests queued");
return;
return TRANSFER_ERROR_NO_SLOTS;
}
trq->callback = callback;
trq->transfer->callback = transfer_callback;
@@ -460,7 +461,9 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
this->release_trq(trq);
return TRANSFER_ERROR_SUBMIT_FAILED;
}
return TRANSFER_OK;
}
/**

View File

@@ -169,6 +169,98 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) {
this->parent_->start_input(this);
return status;
}
void USBUartComponent::reset_input_state_(USBUartChannel *channel) {
channel->input_retry_count_.store(0);
channel->input_started_.store(false);
}
void USBUartComponent::restart_input_(USBUartChannel *channel) {
// Atomically verify it's still started (true) and keep it started
// This prevents the race window of toggling true->false->true
bool expected = true;
if (channel->input_started_.compare_exchange_strong(expected, true)) {
// Still started - do the actual restart work without toggling the flag
this->do_start_input_(channel);
}
}
void USBUartComponent::input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status) {
// CALLBACK CONTEXT: This function is executed in USB task via transfer_callback
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
// Transfer failed, slot already released
// Reset state so normal operations can restart later
this->reset_input_state_(channel);
return;
}
if (!channel->dummy_receiver_ && status.data_len > 0) {
// Allocate a chunk from the pool
UsbDataChunk *chunk = this->chunk_pool_.allocate();
if (chunk == nullptr) {
// No chunks available - queue is full, data dropped, slot already released
this->usb_data_queue_.increment_dropped_count();
// Reset state so normal operations can restart later
this->reset_input_state_(channel);
return;
}
// Copy data to chunk (this is fast, happens in USB task)
memcpy(chunk->data, status.data, status.data_len);
chunk->length = status.data_len;
chunk->channel = channel;
// Push to lock-free queue for main loop processing
// Push always succeeds because pool size == queue size
this->usb_data_queue_.push(chunk);
}
// On success, reset retry count and restart input immediately from USB task for performance
// The lock-free queue will handle backpressure
channel->input_retry_count_.store(0);
channel->input_started_.store(false);
this->start_input(channel);
}
void USBUartComponent::do_start_input_(USBUartChannel *channel) {
// This function does the actual work of starting input
// Caller must ensure input_started_ is already set to true
const auto *ep = channel->cdc_dev_.in_ep;
// input_started_ already set to true by caller
auto result = this->transfer_in(
ep->bEndpointAddress,
[this, channel](const usb_host::TransferStatus &status) { this->input_transfer_callback_(channel, status); },
ep->wMaxPacketSize);
if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) {
// No slots available - defer retry to main loop
this->defer_input_retry_(channel);
} else if (result != usb_host::TRANSFER_OK) {
// Other error (submit failed) - don't retry, just reset state
// Error already logged by transfer_in()
this->reset_input_state_(channel);
}
}
void USBUartComponent::defer_input_retry_(USBUartChannel *channel) {
static constexpr uint8_t MAX_INPUT_RETRIES = 10;
// Atomically increment and get the NEW value (previous + 1)
uint8_t new_retry_count = channel->input_retry_count_.fetch_add(1) + 1;
if (new_retry_count > MAX_INPUT_RETRIES) {
ESP_LOGE(TAG, "Input retry limit reached for channel %d, stopping retries", channel->index_);
this->reset_input_state_(channel);
return;
}
// Keep input_started_ as true during defer to prevent multiple retries from queueing
// The deferred lambda will atomically restart
this->defer([this, channel] { this->restart_input_(channel); });
}
void USBUartComponent::setup() { USBClient::setup(); }
void USBUartComponent::loop() {
USBClient::loop();
@@ -214,8 +306,14 @@ void USBUartComponent::dump_config() {
}
}
void USBUartComponent::start_input(USBUartChannel *channel) {
if (!channel->initialised_.load() || channel->input_started_.load())
if (!channel->initialised_.load())
return;
// Atomically check if not started and set to started in one operation
bool expected = false;
if (!channel->input_started_.compare_exchange_strong(expected, true))
return; // Already started - prevents duplicate transfers from concurrent threads
// THREAD CONTEXT: Called from both USB task and main loop threads
// - USB task: Immediate restart after successful transfer for continuous data flow
// - Main loop: Controlled restart after consuming data (backpressure mechanism)
@@ -226,45 +324,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
//
// The underlying transfer_in() uses lock-free atomic allocation from the
// TransferRequest pool, making this multi-threaded access safe
const auto *ep = channel->cdc_dev_.in_ep;
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
auto callback = [this, channel](const usb_host::TransferStatus &status) {
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
// On failure, don't restart - let next read_array() trigger it
channel->input_started_.store(false);
return;
}
if (!channel->dummy_receiver_ && status.data_len > 0) {
// Allocate a chunk from the pool
UsbDataChunk *chunk = this->chunk_pool_.allocate();
if (chunk == nullptr) {
// No chunks available - queue is full or we're out of memory
this->usb_data_queue_.increment_dropped_count();
// Mark input as not started so we can retry
channel->input_started_.store(false);
return;
}
// Copy data to chunk (this is fast, happens in USB task)
memcpy(chunk->data, status.data, status.data_len);
chunk->length = status.data_len;
chunk->channel = channel;
// Push to lock-free queue for main loop processing
// Push always succeeds because pool size == queue size
this->usb_data_queue_.push(chunk);
}
// On success, restart input immediately from USB task for performance
// The lock-free queue will handle backpressure
channel->input_started_.store(false);
this->start_input(channel);
};
channel->input_started_.store(true);
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
// Do the actual work (input_started_ already set to true by CAS above)
this->do_start_input_(channel);
}
void USBUartComponent::start_output(USBUartChannel *channel) {
@@ -370,7 +432,7 @@ void USBUartTypeCdcAcm::enable_channels() {
for (auto *channel : this->channels_) {
if (!channel->initialised_.load())
continue;
channel->input_started_.store(false);
this->reset_input_state_(channel);
channel->output_started_.store(false);
this->start_input(channel);
}

View File

@@ -111,10 +111,11 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
CdcEps cdc_dev_{};
// Enum (likely 4 bytes)
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
// Group atomics together (each 1 byte)
// Group atomics together
std::atomic<bool> input_started_{true};
std::atomic<bool> output_started_{true};
std::atomic<bool> initialised_{false};
std::atomic<uint8_t> input_retry_count_{0};
// Group regular bytes together to minimize padding
const uint8_t index_;
bool debug_{};
@@ -140,6 +141,11 @@ class USBUartComponent : public usb_host::USBClient {
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_;
protected:
void defer_input_retry_(USBUartChannel *channel);
void reset_input_state_(USBUartChannel *channel);
void restart_input_(USBUartChannel *channel);
void do_start_input_(USBUartChannel *channel);
void input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status);
std::vector<USBUartChannel *> channels_{};
};

View File

@@ -87,6 +87,7 @@
#define USE_MDNS_STORE_SERVICES
#define MDNS_SERVICE_COUNT 3
#define MDNS_DYNAMIC_TXT_COUNT 3
#define SNTP_SERVER_COUNT 3
#define USE_MEDIA_PLAYER
#define USE_NEXTION_TFT_UPLOAD
#define USE_NUMBER

View File

@@ -105,7 +105,9 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
config[CONF_NAME],
platform,
)
add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT]))
# Only set disabled_by_default if True (default is False)
if config[CONF_DISABLED_BY_DEFAULT]:
add(var.set_disabled_by_default(True))
if CONF_INTERNAL in config:
add(var.set_internal(config[CONF_INTERNAL]))
if CONF_ICON in config:

View File

@@ -224,36 +224,37 @@ def resolve_ip_address(
return res
# Process hosts
cached_addresses: list[str] = []
uncached_hosts: list[str] = []
has_cache = address_cache is not None
for h in hosts:
if is_ip_address(h):
if has_cache:
# If we have a cache, treat IPs as cached
cached_addresses.append(h)
else:
# If no cache, pass IPs through to resolver with hostnames
uncached_hosts.append(h)
_add_ip_addresses_to_addrinfo([h], port, res)
elif address_cache and (cached := address_cache.get_addresses(h)):
# Found in cache
cached_addresses.extend(cached)
_add_ip_addresses_to_addrinfo(cached, port, res)
else:
# Not cached, need to resolve
if address_cache and address_cache.has_cache():
_LOGGER.info("Host %s not in cache, will need to resolve", h)
uncached_hosts.append(h)
# Process cached addresses (includes direct IPs and cached lookups)
_add_ip_addresses_to_addrinfo(cached_addresses, port, res)
# If we have uncached hosts (only non-IP hostnames), resolve them
if uncached_hosts:
from aioesphomeapi.host_resolver import AddrInfo as AioAddrInfo
from esphome.core import EsphomeError
from esphome.resolver import AsyncResolver
resolver = AsyncResolver(uncached_hosts, port)
addr_infos = resolver.resolve()
addr_infos: list[AioAddrInfo] = []
try:
addr_infos = resolver.resolve()
except EsphomeError as err:
if not res:
# No pre-resolved addresses available, DNS resolution is fatal
raise
_LOGGER.info("%s (using %d already resolved IP addresses)", err, len(res))
# Convert aioesphomeapi AddrInfo to our format
for addr_info in addr_infos:
sockaddr = addr_info.sockaddr

View File

@@ -15,7 +15,7 @@ esphome-dashboard==20251013.0
aioesphomeapi==42.3.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import
ruamel.yaml==0.18.16 # dashboard_import
ruamel.yaml.clib==0.2.14 # dashboard_import
esphome-glyphsets==0.2.0
pillow==11.3.0

View File

@@ -77,6 +77,7 @@ ISOLATED_COMPONENTS = {
"esphome": "Defines devices/areas in esphome: section that are referenced in other sections - breaks when merged",
"ethernet": "Defines ethernet: which conflicts with wifi: used by most components",
"ethernet_info": "Related to ethernet component which conflicts with wifi",
"gps": "TinyGPSPlus library declares millis() function that creates ambiguity with ESPHome millis() macro when merged with components using millis() in lambdas",
"lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs",
"mapping": "Uses dict format for image/display sections incompatible with standard list format - ESPHome merge_config cannot handle",
"openthread": "Conflicts with wifi: used by most components",

View File

@@ -454,9 +454,27 @@ def test_resolve_ip_address_mixed_list() -> None:
# Mix of IP and hostname - should use async resolver
result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053)
assert len(result) == 2
assert result[0][4][0] == "192.168.1.100"
assert result[1][4][0] == "192.168.1.200"
MockResolver.assert_called_once_with(["test.local"], 6053)
mock_resolver.resolve.assert_called_once()
def test_resolve_ip_address_mixed_list_fail() -> None:
"""Test resolving a mix of IPs and hostnames with resolve failed."""
with patch("esphome.resolver.AsyncResolver") as MockResolver:
mock_resolver = MockResolver.return_value
mock_resolver.resolve.side_effect = EsphomeError(
"Error resolving IP address: [test.local]"
)
# Mix of IP and hostname - should use async resolver
result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053)
assert len(result) == 1
assert result[0][4][0] == "192.168.1.200"
MockResolver.assert_called_once_with(["192.168.1.100", "test.local"], 6053)
assert result[0][4][0] == "192.168.1.100"
MockResolver.assert_called_once_with(["test.local"], 6053)
mock_resolver.resolve.assert_called_once()