Compare commits

..

128 Commits

Author SHA1 Message Date
J. Nick Koston
3f34de5d6e Merge branch 'min_filter_ring_buffer' into ci_impact_analysis_sensor_full 2025-10-17 21:13:37 -10:00
J. Nick Koston
629bded07c Merge branch 'ci_impact_analysis_sensor_base' into ci_impact_analysis_sensor_full 2025-10-17 21:13:31 -10:00
J. Nick Koston
8b26ed1eda Merge branch 'ci_impact_analysis' into ci_impact_analysis_sensor_base 2025-10-17 21:13:18 -10:00
J. Nick Koston
fd32f6930e Merge remote-tracking branch 'upstream/dev' into min_filter_ring_buffer 2025-10-17 21:04:24 -10:00
J. Nick Koston
b4ae85cf0f cleanup sorting 2025-10-17 21:03:51 -10:00
J. Nick Koston
e70cb098ae whitespace 2025-10-17 18:50:07 -10:00
J. Nick Koston
7f2d8a2c11 whitespace 2025-10-17 18:46:41 -10:00
J. Nick Koston
4f4da1de22 preen 2025-10-17 18:41:12 -10:00
J. Nick Koston
f9807db08a preen 2025-10-17 18:37:24 -10:00
J. Nick Koston
541fb8b27c update test 2025-10-17 18:32:22 -10:00
J. Nick Koston
85e0a4fbf9 update test 2025-10-17 18:29:36 -10:00
J. Nick Koston
7e54803ede update test 2025-10-17 18:25:41 -10:00
J. Nick Koston
a078486a87 update test 2025-10-17 18:21:28 -10:00
J. Nick Koston
ba18bb6a4f template all the things 2025-10-17 18:18:15 -10:00
J. Nick Koston
07ad32968e template all the things 2025-10-17 18:15:46 -10:00
J. Nick Koston
0b077bdfc6 preen 2025-10-17 18:08:52 -10:00
J. Nick Koston
1f00617738 Merge remote-tracking branch 'upstream/ci_impact_analysis' into ci_impact_analysis 2025-10-17 18:06:44 -10:00
J. Nick Koston
9cf1fd24fd preen 2025-10-17 18:06:13 -10:00
pre-commit-ci-lite[bot]
bbd636a8cc [pre-commit.ci lite] apply automatic fixes 2025-10-18 03:59:23 +00:00
J. Nick Koston
322dc530a9 Merge remote-tracking branch 'origin/ci_impact_analysis' into ci_impact_analysis 2025-10-17 17:58:05 -10:00
J. Nick Koston
0b09e50685 preen 2025-10-17 17:57:42 -10:00
J. Nick Koston
a96cc5e6f2 Update esphome/analyze_memory/__init__.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 17:57:33 -10:00
J. Nick Koston
9a4288d81a Update script/determine-jobs.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 17:56:41 -10:00
J. Nick Koston
b95999aca7 Update esphome/analyze_memory/__init__.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 17:55:37 -10:00
J. Nick Koston
c70937ed01 dry 2025-10-17 17:55:05 -10:00
J. Nick Koston
3151606d50 Merge branch 'dev' into ci_impact_analysis 2025-10-17 17:47:36 -10:00
J. Nick Koston
5080698c3a no memory when tatget branch does not have 2025-10-17 17:34:16 -10:00
J. Nick Koston
931e3f80f0 no memory when tatget branch does not have 2025-10-17 17:25:14 -10:00
J. Nick Koston
b5190e5e87 no memory when tatget branch does not have 2025-10-17 17:25:03 -10:00
J. Nick Koston
66103b71ba Merge branch 'ci_impact_analysis_sensor_base' into ci_impact_analysis_sensor_full 2025-10-17 17:16:33 -10:00
J. Nick Koston
050d9575f2 Merge branch 'ci_impact_analysis' into ci_impact_analysis_sensor_base 2025-10-17 17:16:19 -10:00
J. Nick Koston
cd93f7f55a tweak 2025-10-17 17:13:24 -10:00
J. Nick Koston
d98b00f56d tweak 2025-10-17 17:10:28 -10:00
J. Nick Koston
8fd43f1d96 tweak 2025-10-17 17:09:05 -10:00
J. Nick Koston
0475ec5533 preen 2025-10-17 17:01:20 -10:00
J. Nick Koston
5ccdc48ce2 Merge branch 'min_filter_ring_buffer' into ci_impact_analysis_sensor_full 2025-10-17 16:46:37 -10:00
J. Nick Koston
db20f90aa0 add tests 2025-10-17 16:46:07 -10:00
J. Nick Koston
6fe5a0c736 preen 2025-10-17 16:44:38 -10:00
J. Nick Koston
1ec9383abe preen 2025-10-17 16:39:10 -10:00
J. Nick Koston
5e1ee92754 add tests 2025-10-17 16:34:25 -10:00
J. Nick Koston
558d4eb9dd preen 2025-10-17 16:19:50 -10:00
J. Nick Koston
c6ecfd0c55 esp32 only platforms 2025-10-17 16:15:46 -10:00
J. Nick Koston
3b8b2c0754 esp32 only platforms 2025-10-17 16:13:30 -10:00
J. Nick Koston
f5d69a2539 esp32 only platforms 2025-10-17 16:11:28 -10:00
J. Nick Koston
29b9073d62 esp32 only platforms 2025-10-17 16:08:16 -10:00
J. Nick Koston
a45e94cd06 preen 2025-10-17 16:02:08 -10:00
J. Nick Koston
71f2fb8353 preen 2025-10-17 15:56:13 -10:00
J. Nick Koston
0fcae15c25 preen 2025-10-17 15:53:03 -10:00
J. Nick Koston
a1d6bac21a preen 2025-10-17 15:44:36 -10:00
J. Nick Koston
db69ce24ae fix 2025-10-17 15:41:20 -10:00
J. Nick Koston
293400ee14 fix 2025-10-17 15:35:51 -10:00
J. Nick Koston
57bf3f968f fix 2025-10-17 15:34:17 -10:00
J. Nick Koston
922c2bcd5a fix 2025-10-17 15:26:55 -10:00
J. Nick Koston
5e9b972831 fix 2025-10-17 15:24:49 -10:00
J. Nick Koston
3bc0041b94 fix 2025-10-17 15:22:06 -10:00
J. Nick Koston
daa03e5b3c fix 2025-10-17 15:17:28 -10:00
J. Nick Koston
62ce39e430 fix 2025-10-17 15:17:15 -10:00
J. Nick Koston
a9e5e4d6d2 tweak 2025-10-17 15:14:00 -10:00
J. Nick Koston
95a0c9594f tweak 2025-10-17 15:12:36 -10:00
J. Nick Koston
8762d7cf0e Merge remote-tracking branch 'upstream/dev' into ci_impact_analysis 2025-10-17 15:06:15 -10:00
J. Nick Koston
84316d62f9 tweak 2025-10-17 15:04:19 -10:00
J. Nick Koston
e1e047c53f tweak 2025-10-17 15:02:09 -10:00
J. Nick Koston
b0ada914bc tweak 2025-10-17 14:57:45 -10:00
J. Nick Koston
e2101f5a20 tweak 2025-10-17 14:52:07 -10:00
J. Nick Koston
f87c969b43 tweak 2025-10-17 14:40:45 -10:00
J. Nick Koston
f011c44130 merge 2025-10-17 14:26:44 -10:00
J. Nick Koston
843f590db4 fix 2025-10-17 14:13:25 -10:00
J. Nick Koston
2c86ebaf7f merge 2025-10-17 14:10:23 -10:00
J. Nick Koston
25fe4a1476 merge 2025-10-17 14:09:08 -10:00
J. Nick Koston
86c12079b4 merge 2025-10-17 14:05:24 -10:00
J. Nick Koston
79aafe2cd5 merge 2025-10-17 14:01:21 -10:00
J. Nick Koston
a5d6e39b2f merge 2025-10-17 14:01:07 -10:00
J. Nick Koston
a78a7dfa4e merge 2025-10-17 13:58:59 -10:00
J. Nick Koston
7879df4dd1 merge 2025-10-17 13:57:57 -10:00
J. Nick Koston
43c62297e8 merge 2025-10-17 13:56:31 -10:00
J. Nick Koston
5049c7227d reduce 2025-10-17 13:50:15 -10:00
J. Nick Koston
256d3b119b relo 2025-10-17 13:44:30 -10:00
J. Nick Koston
6d2c700c43 relo 2025-10-17 13:43:05 -10:00
J. Nick Koston
9d081795e8 relo 2025-10-17 13:41:55 -10:00
J. Nick Koston
59848a2c8a tweak 2025-10-17 13:31:04 -10:00
J. Nick Koston
c7c408e667 tweak 2025-10-17 13:28:13 -10:00
J. Nick Koston
acfa325f23 merge 2025-10-17 13:22:01 -10:00
J. Nick Koston
cb97271704 Merge remote-tracking branch 'upstream/dev' into ci_impact_analysis 2025-10-17 13:19:47 -10:00
J. Nick Koston
1118ef32c3 preen 2025-10-16 06:16:37 -10:00
J. Nick Koston
0cff6acdf4 fix flakey 2025-10-16 06:09:44 -10:00
J. Nick Koston
7be04916ac fix flakey 2025-10-16 06:09:38 -10:00
J. Nick Koston
b5c4dc13e0 fix flakey 2025-10-16 06:07:41 -10:00
J. Nick Koston
0200d7c358 fix flakey 2025-10-16 06:05:39 -10:00
J. Nick Koston
44ad787cb3 fix flakey 2025-10-16 06:04:42 -10:00
J. Nick Koston
699da1adc1 Merge remote-tracking branch 'upstream/min_filter_ring_buffer' into min_filter_ring_buffer 2025-10-16 06:02:03 -10:00
J. Nick Koston
4d7e8ffd0a Merge remote-tracking branch 'upstream/dev' into min_filter_ring_buffer 2025-10-16 06:01:57 -10:00
J. Nick Koston
3ba2212cfc fix flakey 2025-10-16 06:01:32 -10:00
J. Nick Koston
bb2be9869d Merge branch 'dev' into min_filter_ring_buffer 2025-10-15 23:45:55 -10:00
J. Nick Koston
b4ba2aff30 remove dead unreachable code 2025-10-15 23:30:45 -10:00
J. Nick Koston
febe075bb2 helper 2025-10-15 23:17:08 -10:00
J. Nick Koston
baf117b411 fix flakey test 2025-10-15 22:03:22 -10:00
J. Nick Koston
55e03036e2 preen 2025-10-15 21:46:00 -10:00
J. Nick Koston
af45dc206f Merge remote-tracking branch 'origin/datetime_guard_not_needed' into min_filter_ring_buffer 2025-10-15 21:45:13 -10:00
J. Nick Koston
7027ae9833 race 2025-10-15 21:44:38 -10:00
J. Nick Koston
784183ca8d [datetime] Fix DateTimeStateTrigger compilation when time component is not used 2025-10-15 21:38:02 -10:00
J. Nick Koston
855df423ee add 2025-10-15 19:58:18 -10:00
J. Nick Koston
f75f11b550 add 2025-10-15 19:57:29 -10:00
J. Nick Koston
a999349fa5 tweak 2025-10-15 19:29:55 -10:00
J. Nick Koston
92d54ffb09 tweak 2025-10-15 19:28:51 -10:00
J. Nick Koston
589c25e65a tweak 2025-10-15 19:24:44 -10:00
J. Nick Koston
5a8558e1c5 tweak 2025-10-15 19:23:35 -10:00
J. Nick Koston
a72c494b75 tweak 2025-10-15 19:23:01 -10:00
J. Nick Koston
e3089ff0f6 tweak 2025-10-15 19:21:33 -10:00
J. Nick Koston
a4b14902db perf 2025-10-15 18:44:37 -10:00
J. Nick Koston
4ff39ee82c Merge remote-tracking branch 'origin/min_filter_ring_buffer' into min_filter_ring_buffer 2025-10-15 18:27:33 -10:00
J. Nick Koston
447ee3da39 tests 2025-10-15 18:26:23 -10:00
J. Nick Koston
9b6707c1c0 tests 2025-10-15 18:25:42 -10:00
J. Nick Koston
0c18dd872b Merge branch 'dev' into min_filter_ring_buffer 2025-10-15 18:05:02 -10:00
J. Nick Koston
b074ca8a1e fix 2025-10-15 18:00:33 -10:00
J. Nick Koston
4c24545b82 fix 2025-10-15 17:51:08 -10:00
J. Nick Koston
cd252a33f9 fix 2025-10-15 17:51:03 -10:00
J. Nick Koston
36f8511309 fix 2025-10-15 17:50:32 -10:00
J. Nick Koston
12874187dd fix 2025-10-15 17:50:27 -10:00
J. Nick Koston
d7832c44bc [sensor] Fix sliding window filter memory fragmentation with FixedVector ring buffer 2025-10-15 17:45:37 -10:00
J. Nick Koston
8e6ee2bed1 debug 2025-10-14 13:43:58 -10:00
J. Nick Koston
354f46f7c0 debug 2025-10-14 13:38:41 -10:00
J. Nick Koston
7b6acd3c00 tidy 2025-10-14 13:33:31 -10:00
J. Nick Koston
11f5f7683c tidy 2025-10-14 13:32:21 -10:00
J. Nick Koston
5da589abd0 fix 2025-10-14 13:27:13 -10:00
J. Nick Koston
daa39a489d fix tests 2025-10-14 13:20:31 -10:00
J. Nick Koston
3bb95a190d fix 2025-10-14 13:15:44 -10:00
J. Nick Koston
25a6202bb9 [ci] Automatic Flash/RAM impact analysis 2025-10-14 13:09:01 -10:00
J. Nick Koston
c4eeed7f7e [ci] Automatic Flash/RAM impact analysis 2025-10-14 13:05:02 -10:00
64 changed files with 309 additions and 1461 deletions

View File

@@ -70,7 +70,6 @@ esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @dan-s-github @tobias-
esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/ble_nus/* @tomaszduda23
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov

View File

@@ -185,9 +185,7 @@ def choose_upload_log_host(
else:
resolved.append(device)
if not resolved:
raise EsphomeError(
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
)
_LOGGER.error("All specified devices: %s could not be resolved.", defaults)
return resolved
# No devices specified, show interactive chooser

View File

@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
string name = 3;
reserved 4; // Deprecated: was string unique_id
repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
// next four supports_* are for legacy clients, newer clients should use color modes
// Deprecated in API version 1.6
bool legacy_supports_brightness = 5 [deprecated=true];

View File

@@ -453,6 +453,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
bool is_single) {
auto *light = static_cast<light::LightState *>(entity);
LightStateResponse resp;
auto traits = light->get_traits();
auto values = light->remote_values;
auto color_mode = values.get_color_mode();
resp.state = values.is_on();
@@ -476,8 +477,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
auto *light = static_cast<light::LightState *>(entity);
ListEntitiesLightResponse msg;
auto traits = light->get_traits();
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
msg.supported_color_modes = &traits.get_supported_color_modes();
msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
msg.min_mireds = traits.get_min_mireds();
@@ -661,12 +661,11 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
ListEntitiesClimateResponse msg;
auto traits = climate->get_traits();
// Flags set for backward compatibility, deprecated in 2025.11.0
msg.supports_current_temperature = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
msg.supports_current_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
msg.supports_two_point_target_temperature = traits.has_feature_flags(
climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
msg.supports_target_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
msg.supports_current_temperature = traits.get_supports_current_temperature();
msg.supports_current_humidity = traits.get_supports_current_humidity();
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
msg.supports_target_humidity = traits.get_supports_target_humidity();
msg.supports_action = traits.get_supports_action();
// Current feature flags and other supported parameters
msg.feature_flags = traits.get_feature_flags();
msg.supported_modes = &traits.get_supported_modes_for_api_();
@@ -1082,8 +1081,13 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
#ifdef USE_TIME_TIMEZONE
if (value.timezone_len > 0) {
homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
value.timezone_len);
const std::string &current_tz = homeassistant::global_homeassistant_time->get_timezone();
// Compare without allocating a string
if (current_tz.length() != value.timezone_len ||
memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
homeassistant::global_homeassistant_time->set_timezone(
std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
}
}
#endif
}

View File

@@ -70,14 +70,4 @@ extend google.protobuf.FieldOptions {
// init(size) before adding elements. This eliminates std::vector template overhead
// and is ideal when the exact size is known before populating the array.
optional bool fixed_vector = 50013 [default=false];
// container_pointer_no_template: Use a non-template container type for repeated fields
// Similar to container_pointer, but for containers that don't take template parameters.
// The container type is used as-is without appending element type.
// The container must have:
// - begin() and end() methods returning iterators
// - empty() method
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
// generates: const light::ColorModeMask *supported_color_modes{};
optional string container_pointer_no_template = 50014;
}

View File

@@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_light_response"; }
#endif
const light::ColorModeMask *supported_color_modes{};
const std::set<light::ColorMode> *supported_color_modes{};
float min_mireds{0.0f};
float max_mireds{0.0f};
std::vector<std::string> effects{};

View File

@@ -1,29 +0,0 @@
import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE
AUTO_LOAD = ["zephyr_ble_server"]
CODEOWNERS = ["@tomaszduda23"]
ble_nus_ns = cg.esphome_ns.namespace("ble_nus")
BLENUS = ble_nus_ns.class_("BLENUS", cg.Component)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLENUS),
cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
*[CONF_LOGS], lower=True
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_framework("zephyr"),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
zephyr_add_prj_conf("BT_NUS", True)
cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS))
await cg.register_component(var, config)

View File

@@ -1,157 +0,0 @@
#ifdef USE_ZEPHYR
#include "ble_nus.h"
#include <zephyr/kernel.h>
#include <bluetooth/services/nus.h>
#include "esphome/core/log.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#include "esphome/core/application.h"
#endif
#include <zephyr/sys/ring_buffer.h>
namespace esphome::ble_nus {
constexpr size_t BLE_TX_BUF_SIZE = 2048;
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
BLENUS *global_ble_nus;
RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE);
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
static const char *const TAG = "ble_nus";
size_t BLENUS::write_array(const uint8_t *data, size_t len) {
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
return 0;
}
return ring_buf_put(&global_ble_tx_ring_buf, data, len);
}
void BLENUS::connected(bt_conn *conn, uint8_t err) {
if (err == 0) {
global_ble_nus->conn_.store(bt_conn_ref(conn));
}
}
void BLENUS::disconnected(bt_conn *conn, uint8_t reason) {
if (global_ble_nus->conn_) {
bt_conn_unref(global_ble_nus->conn_.load());
// Connection array is global static.
// Reference can be kept even if disconnected.
}
}
void BLENUS::tx_callback(bt_conn *conn) {
atomic_cas(&global_ble_nus->tx_status_, TX_BUSY, TX_ENABLED);
ESP_LOGVV(TAG, "Sent operation completed");
}
void BLENUS::send_enabled_callback(bt_nus_send_status status) {
switch (status) {
case BT_NUS_SEND_STATUS_ENABLED:
atomic_set(&global_ble_nus->tx_status_, TX_ENABLED);
#ifdef USE_LOGGER
if (global_ble_nus->expose_log_) {
App.schedule_dump_config();
}
#endif
ESP_LOGD(TAG, "NUS notification has been enabled");
break;
case BT_NUS_SEND_STATUS_DISABLED:
atomic_set(&global_ble_nus->tx_status_, TX_DISABLED);
ESP_LOGD(TAG, "NUS notification has been disabled");
break;
}
}
void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) {
ESP_LOGD(TAG, "Received %d bytes.", len);
}
void BLENUS::setup() {
bt_nus_cb callbacks = {
.received = rx_callback,
.sent = tx_callback,
.send_enabled = send_enabled_callback,
};
bt_nus_init(&callbacks);
static bt_conn_cb conn_callbacks = {
.connected = BLENUS::connected,
.disconnected = BLENUS::disconnected,
};
bt_conn_cb_register(&conn_callbacks);
global_ble_nus = this;
#ifdef USE_LOGGER
if (logger::global_logger != nullptr && this->expose_log_) {
logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message, size_t message_len) {
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
const char c = '\n';
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
});
}
#endif
}
void BLENUS::dump_config() {
ESP_LOGCONFIG(TAG, "ble nus:");
ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_));
uint32_t mtu = 0;
bt_conn *conn = this->conn_.load();
if (conn) {
mtu = bt_nus_get_mtu(conn);
}
ESP_LOGCONFIG(TAG, " MTU: %u", mtu);
}
void BLENUS::loop() {
if (ring_buf_is_empty(&global_ble_tx_ring_buf)) {
return;
}
if (!atomic_cas(&this->tx_status_, TX_ENABLED, TX_BUSY)) {
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
ring_buf_reset(&global_ble_tx_ring_buf);
}
return;
}
bt_conn *conn = this->conn_.load();
if (conn) {
conn = bt_conn_ref(conn);
}
if (nullptr == conn) {
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
return;
}
uint32_t req_len = bt_nus_get_mtu(conn);
uint8_t *buf;
uint32_t size = ring_buf_get_claim(&global_ble_tx_ring_buf, &buf, req_len);
int err, err2;
err = bt_nus_send(conn, buf, size);
err2 = ring_buf_get_finish(&global_ble_tx_ring_buf, size);
if (err2) {
// It should no happen.
ESP_LOGE(TAG, "Size %u exceeds valid bytes in the ring buffer (%d error)", size, err2);
}
if (err == 0) {
ESP_LOGVV(TAG, "Sent %d bytes", size);
} else {
ESP_LOGE(TAG, "Failed to send %d bytes (%d error)", size, err);
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
}
bt_conn_unref(conn);
}
} // namespace esphome::ble_nus
#endif

View File

@@ -1,37 +0,0 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include <shell/shell_bt_nus.h>
#include <atomic>
namespace esphome::ble_nus {
class BLENUS : public Component {
enum TxStatus {
TX_DISABLED,
TX_ENABLED,
TX_BUSY,
};
public:
void setup() override;
void dump_config() override;
void loop() override;
size_t write_array(const uint8_t *data, size_t len);
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
protected:
static void send_enabled_callback(bt_nus_send_status status);
static void tx_callback(bt_conn *conn);
static void rx_callback(bt_conn *conn, const uint8_t *data, uint16_t len);
static void connected(bt_conn *conn, uint8_t err);
static void disconnected(bt_conn *conn, uint8_t reason);
std::atomic<bt_conn *> conn_ = nullptr;
bool expose_log_ = false;
atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED);
};
} // namespace esphome::ble_nus
#endif

View File

@@ -155,12 +155,16 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
uint64_t conn_addr = connection->get_address();
if (conn_addr == address)
if (connection->get_address() == address)
return connection;
}
if (reserve && conn_addr == 0) {
if (!reserve)
return nullptr;
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() == 0) {
connection->send_service_ = INIT_SENDING_SERVICES;
connection->set_address(address);
// All connections must start at INIT
@@ -171,6 +175,7 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
return connection;
}
}
return nullptr;
}

View File

@@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BME680BSECComponent),
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
IAQ_MODE_OPTIONS, upper=True
),

View File

@@ -139,7 +139,7 @@ CONFIG_SCHEMA_BASE = (
cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
VOLTAGE_OPTIONS, upper=True
),
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
cv.Optional(
CONF_STATE_SAVE_INTERVAL, default="6hours"
): cv.positive_time_period_minutes,

View File

@@ -61,7 +61,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
this->address_str_ = "";
} else {
char buf[18];
format_mac_addr_upper(this->remote_bda_, buf);
uint8_t mac[6] = {
(uint8_t) ((this->address_ >> 40) & 0xff), (uint8_t) ((this->address_ >> 32) & 0xff),
(uint8_t) ((this->address_ >> 24) & 0xff), (uint8_t) ((this->address_ >> 16) & 0xff),
(uint8_t) ((this->address_ >> 8) & 0xff), (uint8_t) ((this->address_ >> 0) & 0xff),
};
format_mac_addr_upper(mac, buf);
this->address_str_ = buf;
}
}

View File

@@ -104,200 +104,5 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) {
return static_cast<ColorMode>(static_cast<uint8_t>(lhs) | static_cast<uint8_t>(rhs));
}
// Type alias for raw color mode bitmask values
using color_mode_bitmask_t = uint16_t;
// Constants for ColorMode count and bit range
static constexpr int COLOR_MODE_COUNT = 10; // UNKNOWN through RGB_COLD_WARM_WHITE
static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8; // Number of bits in bitmask type
// Compile-time array of all ColorMode values in declaration order
// Bit positions (0-9) map directly to enum declaration order
static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = {
ColorMode::UNKNOWN, // bit 0
ColorMode::ON_OFF, // bit 1
ColorMode::BRIGHTNESS, // bit 2
ColorMode::WHITE, // bit 3
ColorMode::COLOR_TEMPERATURE, // bit 4
ColorMode::COLD_WARM_WHITE, // bit 5
ColorMode::RGB, // bit 6
ColorMode::RGB_WHITE, // bit 7
ColorMode::RGB_COLOR_TEMPERATURE, // bit 8
ColorMode::RGB_COLD_WARM_WHITE, // bit 9
};
/// Map ColorMode enum values to bit positions (0-9)
/// Bit positions follow the enum declaration order
static constexpr int mode_to_bit(ColorMode mode) {
// Linear search through COLOR_MODES array
// Compiler optimizes this to efficient code since array is constexpr
for (int i = 0; i < COLOR_MODE_COUNT; ++i) {
if (COLOR_MODES[i] == mode)
return i;
}
return 0;
}
/// Map bit positions (0-9) to ColorMode enum values
/// Bit positions follow the enum declaration order
static constexpr ColorMode bit_to_mode(int bit) {
// Direct lookup in COLOR_MODES array
return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN;
}
/// Helper to compute capability bitmask at compile time
static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) {
color_mode_bitmask_t mask = 0;
uint8_t cap_bit = static_cast<uint8_t>(capability);
// Check each ColorMode to see if it has this capability
for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) {
uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit));
if ((mode_val & cap_bit) != 0) {
mask |= (1 << bit);
}
}
return mask;
}
// Number of ColorCapability enum values
static constexpr int COLOR_CAPABILITY_COUNT = 6;
/// Compile-time lookup table mapping ColorCapability to bitmask
/// This array is computed at compile time using constexpr
static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = {
compute_capability_bitmask(ColorCapability::ON_OFF), // 1 << 0
compute_capability_bitmask(ColorCapability::BRIGHTNESS), // 1 << 1
compute_capability_bitmask(ColorCapability::WHITE), // 1 << 2
compute_capability_bitmask(ColorCapability::COLOR_TEMPERATURE), // 1 << 3
compute_capability_bitmask(ColorCapability::COLD_WARM_WHITE), // 1 << 4
compute_capability_bitmask(ColorCapability::RGB), // 1 << 5
};
/// Bitmask for storing a set of ColorMode values efficiently.
/// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes).
class ColorModeMask {
public:
constexpr ColorModeMask() = default;
/// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE}
constexpr ColorModeMask(std::initializer_list<ColorMode> modes) {
for (auto mode : modes) {
this->add(mode);
}
}
constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); }
/// Add multiple modes at once using initializer list
constexpr void add(std::initializer_list<ColorMode> modes) {
for (auto mode : modes) {
this->add(mode);
}
}
constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; }
constexpr size_t size() const {
// Count set bits using Brian Kernighan's algorithm
// More efficient for sparse bitmasks (typical case: 2-4 modes out of 10)
uint16_t n = this->mask_;
size_t count = 0;
while (n) {
n &= n - 1; // Clear the least significant set bit
count++;
}
return count;
}
constexpr bool empty() const { return this->mask_ == 0; }
/// Iterator support for API encoding
class Iterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = ColorMode;
using difference_type = std::ptrdiff_t;
using pointer = const ColorMode *;
using reference = ColorMode;
constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); }
constexpr ColorMode operator*() const { return bit_to_mode(bit_); }
constexpr Iterator &operator++() {
++bit_;
advance_to_next_set_bit_();
return *this;
}
constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; }
constexpr bool operator!=(const Iterator &other) const { return !(*this == other); }
private:
constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); }
color_mode_bitmask_t mask_;
int bit_;
};
constexpr Iterator begin() const { return Iterator(mask_, 0); }
constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); }
/// Get the raw bitmask value for API encoding
constexpr color_mode_bitmask_t get_mask() const { return this->mask_; }
/// Find the next set bit in a bitmask starting from a given position
/// Returns the bit position, or MAX_BIT_INDEX if no more bits are set
static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) {
int bit = start_bit;
while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) {
++bit;
}
return bit;
}
/// Find the first set bit in a bitmask and return the corresponding ColorMode
/// Used for optimizing compute_color_mode_() intersection logic
static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) {
return bit_to_mode(find_next_set_bit(mask, 0));
}
/// Check if a ColorMode is present in a raw bitmask value
/// Useful for checking intersection results without creating a temporary ColorModeMask
static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) {
return (mask & (1 << mode_to_bit(mode))) != 0;
}
/// Check if any mode in the bitmask has a specific capability
/// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB)
bool has_capability(ColorCapability capability) const {
// Lookup the pre-computed bitmask for this capability and check intersection with our mask
// ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5
// We need to convert the power-of-2 value to an index
uint8_t cap_val = static_cast<uint8_t>(capability);
#if defined(__GNUC__) || defined(__clang__)
// Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n))
int index = __builtin_ctz(cap_val);
#else
// Fallback for compilers without __builtin_ctz
int index = 0;
while (cap_val > 1) {
cap_val >>= 1;
++index;
}
#endif
return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0;
}
private:
// Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan).
// Currently only 10 ColorMode values exist, so 16 bits is sufficient.
// Can be changed to uint32_t if more than 16 color modes are needed in the future.
// Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes).
color_mode_bitmask_t mask_{0};
};
} // namespace light
} // namespace esphome

View File

@@ -406,7 +406,7 @@ void LightCall::transform_parameters_() {
}
}
ColorMode LightCall::compute_color_mode_() {
const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes();
auto supported_modes = this->parent_->get_traits().get_supported_color_modes();
int supported_count = supported_modes.size();
// Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.
@@ -425,19 +425,20 @@ ColorMode LightCall::compute_color_mode_() {
// If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to
// pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode
// was used for some reason.
// Compute intersection of suitable and supported modes using bitwise AND
color_mode_bitmask_t intersection = this->get_suitable_color_modes_mask_() & supported_modes.get_mask();
std::set<ColorMode> suitable_modes = this->get_suitable_color_modes_();
// Don't change if the current mode is in the intersection (suitable AND supported)
if (ColorModeMask::mask_contains(intersection, current_mode)) {
// Don't change if the current mode is suitable.
if (suitable_modes.count(current_mode) > 0) {
ESP_LOGI(TAG, "'%s': color mode not specified; retaining %s", this->parent_->get_name().c_str(),
LOG_STR_ARG(color_mode_to_human(current_mode)));
return current_mode;
}
// Use the preferred suitable mode.
if (intersection != 0) {
ColorMode mode = ColorModeMask::first_mode_from_mask(intersection);
for (auto mode : suitable_modes) {
if (supported_modes.count(mode) == 0)
continue;
ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(),
LOG_STR_ARG(color_mode_to_human(mode)));
return mode;
@@ -450,7 +451,7 @@ ColorMode LightCall::compute_color_mode_() {
LOG_STR_ARG(color_mode_to_human(color_mode)));
return color_mode;
}
color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
std::set<ColorMode> LightCall::get_suitable_color_modes_() {
bool has_white = this->has_white() && this->white_ > 0.0f;
bool has_ct = this->has_color_temperature();
bool has_cwww =
@@ -458,44 +459,36 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() {
bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) ||
(this->has_red() || this->has_green() || this->has_blue());
// Build key from flags: [rgb][cwww][ct][white]
// Build key from flags: [rgb][cwww][ct][white]
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb);
switch (key) {
case KEY(true, false, false, false): // white only
return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, true, false, false): // ct only
return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE};
case KEY(true, true, false, false): // white + ct
return ColorModeMask(
{ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, false, true, false): // cwww only
return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, false, false, false): // none
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE,
ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE})
.get_mask();
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB,
ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE};
case KEY(true, false, false, true): // rgb + white
return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, true, false, true): // rgb + ct
case KEY(true, true, false, true): // rgb + white + ct
return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, false, true, true): // rgb + cwww
return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask();
return {ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, false, false, true): // rgb only
return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE,
ColorMode::RGB_COLD_WARM_WHITE})
.get_mask();
return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
default:
return 0; // conflicting flags
return {}; // conflicting flags
}
#undef KEY

View File

@@ -1,6 +1,7 @@
#pragma once
#include "light_color_values.h"
#include <set>
namespace esphome {
@@ -185,8 +186,8 @@ class LightCall {
//// Compute the color mode that should be used for this call.
ColorMode compute_color_mode_();
/// Get potential color modes bitmask for this light call.
color_mode_bitmask_t get_suitable_color_modes_mask_();
/// Get potential color modes for this light call.
std::set<ColorMode> get_suitable_color_modes_();
/// Some color modes also can be set using non-native parameters, transform those calls.
void transform_parameters_();

View File

@@ -43,6 +43,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
}
auto values = state.remote_values;
auto traits = state.get_output()->get_traits();
const auto color_mode = values.get_color_mode();
const char *mode_str = get_color_mode_json_str(color_mode);

View File

@@ -191,9 +191,11 @@ void LightState::current_values_as_brightness(float *brightness) {
this->current_values.as_brightness(brightness, this->gamma_correct_);
}
void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) {
auto traits = this->get_traits();
this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false);
}
void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) {
auto traits = this->get_traits();
this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false);
}
void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white,
@@ -207,6 +209,7 @@ void LightState::current_values_as_rgbct(float *red, float *green, float *blue,
white_brightness, this->gamma_correct_);
}
void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) {
auto traits = this->get_traits();
this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness);
}
void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) {

View File

@@ -2,6 +2,7 @@
#include "esphome/core/helpers.h"
#include "color_mode.h"
#include <set>
namespace esphome {
@@ -18,17 +19,18 @@ class LightTraits {
public:
LightTraits() = default;
const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; }
void set_supported_color_modes(ColorModeMask supported_color_modes) {
this->supported_color_modes_ = supported_color_modes;
}
void set_supported_color_modes(std::initializer_list<ColorMode> modes) {
this->supported_color_modes_ = ColorModeMask(modes);
const std::set<ColorMode> &get_supported_color_modes() const { return this->supported_color_modes_; }
void set_supported_color_modes(std::set<ColorMode> supported_color_modes) {
this->supported_color_modes_ = std::move(supported_color_modes);
}
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); }
bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); }
bool supports_color_capability(ColorCapability color_capability) const {
return this->supported_color_modes_.has_capability(color_capability);
for (auto mode : this->supported_color_modes_) {
if (mode & color_capability)
return true;
}
return false;
}
ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.", "v1.21")
@@ -57,9 +59,19 @@ class LightTraits {
void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; }
protected:
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// This method returns a reference to the internal color modes set.
// It is used by the API to avoid copying data when encoding messages.
// Warning: Do not use this method outside of the API connection code.
// It returns a reference to internal data that can be invalidated.
const std::set<ColorMode> &get_supported_color_modes_for_api_() const { return this->supported_color_modes_; }
#endif
std::set<ColorMode> supported_color_modes_{};
float min_mireds_{0};
float max_mireds_{0};
ColorModeMask supported_color_modes_{};
};
} // namespace light

View File

@@ -68,9 +68,6 @@ static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
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;
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection
*
@@ -180,11 +177,8 @@ class Logger : public Component {
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
va_list args, char *buffer, uint16_t *buffer_at,
uint16_t buffer_size) {
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size);
#elif defined(USE_ZEPHYR)
char buff[MAX_POINTER_REPRESENTATION];
this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(buff), buffer, buffer_at, buffer_size);
#else
this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size);
#endif
@@ -283,11 +277,7 @@ class Logger : public Component {
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
const char *HOT get_thread_name_(
#ifdef USE_ZEPHYR
char *buff
#endif
) {
const char *HOT get_thread_name_() {
#ifdef USE_ZEPHYR
k_tid_t current_task = k_current_get();
#else
@@ -301,13 +291,7 @@ class Logger : public Component {
#elif defined(USE_LIBRETINY)
return pcTaskGetTaskName(current_task);
#elif defined(USE_ZEPHYR)
const char *name = k_thread_name_get(current_task);
if (name) {
// zephyr print task names only if debug component is present
return name;
}
std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
return buff;
return k_thread_name_get(current_task);
#endif
}
}

View File

@@ -31,17 +31,18 @@ void MDNSComponent::setup() {
mdns_instance_name_set(this->hostname_.c_str());
for (const auto &service : services) {
auto txt_records = std::make_unique<mdns_txt_item_t[]>(service.txt_records.size());
for (size_t i = 0; i < service.txt_records.size(); i++) {
const auto &record = service.txt_records[i];
std::vector<mdns_txt_item_t> txt_records;
for (const auto &record : service.txt_records) {
mdns_txt_item_t it{};
// key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
// Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
txt_records[i].key = MDNS_STR_ARG(record.key);
txt_records[i].value = MDNS_STR_ARG(record.value);
it.key = MDNS_STR_ARG(record.key);
it.value = MDNS_STR_ARG(record.value);
txt_records.push_back(it);
}
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
txt_records.get(), service.txt_records.size());
txt_records.data(), txt_records.size());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));

View File

@@ -140,8 +140,11 @@ 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"] =
"Noise_NNpsk0_25519_ChaChaPoly_SHA256";
if (api::global_api_server->get_noise_ctx()->has_psk()) {
root["api_encryption"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
} else {
root["api_encryption_supported"] = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
}
#endif
},
2, this->discovery_info_.retain);

View File

@@ -17,11 +17,11 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto traits = this->device_->get_traits();
// current_temperature_topic
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
if (traits.get_supports_current_temperature()) {
root[MQTT_CURRENT_TEMPERATURE_TOPIC] = this->get_current_temperature_state_topic();
}
// current_humidity_topic
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
if (traits.get_supports_current_humidity()) {
root[MQTT_CURRENT_HUMIDITY_TOPIC] = this->get_current_humidity_state_topic();
}
// mode_command_topic
@@ -45,8 +45,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
if (traits.supports_mode(CLIMATE_MODE_HEAT_COOL))
modes.add("heat_cool");
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
if (traits.get_supports_two_point_target_temperature()) {
// temperature_low_command_topic
root[MQTT_TEMPERATURE_LOW_COMMAND_TOPIC] = this->get_target_temperature_low_command_topic();
// temperature_low_state_topic
@@ -62,7 +61,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
root[MQTT_TEMPERATURE_STATE_TOPIC] = this->get_target_temperature_state_topic();
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
if (traits.get_supports_target_humidity()) {
// target_humidity_command_topic
root[MQTT_TARGET_HUMIDITY_COMMAND_TOPIC] = this->get_target_humidity_command_topic();
// target_humidity_state_topic
@@ -110,7 +109,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
presets.add(preset);
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
if (traits.get_supports_action()) {
// action_topic
root[MQTT_ACTION_TOPIC] = this->get_action_state_topic();
}
@@ -175,8 +174,7 @@ void MQTTClimateComponent::setup() {
call.perform();
});
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
if (traits.get_supports_two_point_target_temperature()) {
this->subscribe(this->get_target_temperature_low_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
@@ -213,7 +211,7 @@ void MQTTClimateComponent::setup() {
});
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
if (traits.get_supports_target_humidity()) {
this->subscribe(this->get_target_humidity_command_topic(),
[this](const std::string &topic, const std::string &payload) {
auto val = parse_number<float>(payload);
@@ -292,14 +290,12 @@ bool MQTTClimateComponent::publish_state_() {
success = false;
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
!std::isnan(this->device_->current_temperature)) {
if (traits.get_supports_current_temperature() && !std::isnan(this->device_->current_temperature)) {
std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy);
if (!this->publish(this->get_current_temperature_state_topic(), payload))
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
if (traits.get_supports_two_point_target_temperature()) {
std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy);
if (!this->publish(this->get_target_temperature_low_state_topic(), payload))
success = false;
@@ -312,14 +308,12 @@ bool MQTTClimateComponent::publish_state_() {
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
!std::isnan(this->device_->current_humidity)) {
if (traits.get_supports_current_humidity() && !std::isnan(this->device_->current_humidity)) {
std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0);
if (!this->publish(this->get_current_humidity_state_topic(), payload))
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
!std::isnan(this->device_->target_humidity)) {
if (traits.get_supports_target_humidity() && !std::isnan(this->device_->target_humidity)) {
std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0);
if (!this->publish(this->get_target_humidity_state_topic(), payload))
success = false;
@@ -363,7 +357,7 @@ bool MQTTClimateComponent::publish_state_() {
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
if (traits.get_supports_action()) {
const char *payload;
switch (this->device_->action) {
case CLIMATE_ACTION_OFF:

View File

@@ -85,20 +85,24 @@ bool MQTTComponent::send_discovery_() {
}
// Fields from EntityBase
root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : "";
if (this->get_entity()->has_own_name()) {
root[MQTT_NAME] = this->friendly_name();
} else {
root[MQTT_NAME] = "";
}
if (this->is_disabled_by_default())
root[MQTT_ENABLED_BY_DEFAULT] = false;
if (!this->get_icon().empty())
root[MQTT_ICON] = this->get_icon();
const auto entity_category = this->get_entity()->get_entity_category();
switch (entity_category) {
switch (this->get_entity()->get_entity_category()) {
case ENTITY_CATEGORY_NONE:
break;
case ENTITY_CATEGORY_CONFIG:
root[MQTT_ENTITY_CATEGORY] = "config";
break;
case ENTITY_CATEGORY_DIAGNOSTIC:
root[MQTT_ENTITY_CATEGORY] = entity_category == ENTITY_CATEGORY_CONFIG ? "config" : "diagnostic";
root[MQTT_ENTITY_CATEGORY] = "diagnostic";
break;
}
@@ -109,14 +113,20 @@ bool MQTTComponent::send_discovery_() {
if (this->command_retain_)
root[MQTT_COMMAND_RETAIN] = true;
const Availability &avail =
this->availability_ == nullptr ? global_mqtt_client->get_availability() : *this->availability_;
if (!avail.topic.empty()) {
root[MQTT_AVAILABILITY_TOPIC] = avail.topic;
if (avail.payload_available != "online")
root[MQTT_PAYLOAD_AVAILABLE] = avail.payload_available;
if (avail.payload_not_available != "offline")
root[MQTT_PAYLOAD_NOT_AVAILABLE] = avail.payload_not_available;
if (this->availability_ == nullptr) {
if (!global_mqtt_client->get_availability().topic.empty()) {
root[MQTT_AVAILABILITY_TOPIC] = global_mqtt_client->get_availability().topic;
if (global_mqtt_client->get_availability().payload_available != "online")
root[MQTT_PAYLOAD_AVAILABLE] = global_mqtt_client->get_availability().payload_available;
if (global_mqtt_client->get_availability().payload_not_available != "offline")
root[MQTT_PAYLOAD_NOT_AVAILABLE] = global_mqtt_client->get_availability().payload_not_available;
}
} else if (!this->availability_->topic.empty()) {
root[MQTT_AVAILABILITY_TOPIC] = this->availability_->topic;
if (this->availability_->payload_available != "online")
root[MQTT_PAYLOAD_AVAILABLE] = this->availability_->payload_available;
if (this->availability_->payload_not_available != "offline")
root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available;
}
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
@@ -135,8 +145,10 @@ bool MQTTComponent::send_discovery_() {
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_();
const std::string &friendly_name_ref = App.get_friendly_name();
const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
std::string node_friendly_name = App.get_friendly_name();
if (node_friendly_name.empty()) {
node_friendly_name = node_name;
}
std::string node_area = App.get_area();
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
@@ -146,9 +158,13 @@ bool MQTTComponent::send_discovery_() {
#ifdef ESPHOME_PROJECT_NAME
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
device_info[MQTT_DEVICE_MANUFACTURER] =
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
if (model == nullptr) { // must never happen but check anyway
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME;
} else {
device_info[MQTT_DEVICE_MODEL] = model + 1;
device_info[MQTT_DEVICE_MANUFACTURER] = std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
}
#else
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time() + ")";
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;

View File

@@ -916,7 +916,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
auto min_temp_value = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy);
climate_value_row_(stream, obj, area, node, friendly_name, min_temp, min_temp_value);
// now check optional traits
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
if (traits.get_supports_current_temperature()) {
std::string current_temp = "current_temperature";
if (std::isnan(obj->current_temperature)) {
climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, true);
@@ -927,7 +927,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, false);
}
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
if (traits.get_supports_current_humidity()) {
std::string current_humidity = "current_humidity";
if (std::isnan(obj->current_humidity)) {
climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, true);
@@ -938,7 +938,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, false);
}
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
if (traits.get_supports_target_humidity()) {
std::string target_humidity = "target_humidity";
if (std::isnan(obj->target_humidity)) {
climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, true);
@@ -949,8 +949,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, false);
}
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
if (traits.get_supports_two_point_target_temperature()) {
std::string target_temp_low = "target_temperature_low";
auto target_temp_low_value = value_accuracy_to_string(obj->target_temperature_low, target_accuracy);
climate_value_row_(stream, obj, area, node, friendly_name, target_temp_low, target_temp_low_value);
@@ -962,7 +961,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
auto target_temp_value = value_accuracy_to_string(obj->target_temperature, target_accuracy);
climate_value_row_(stream, obj, area, node, friendly_name, target_temp, target_temp_value);
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
if (traits.get_supports_action()) {
std::string climate_trait_category = "action";
const auto *climate_trait_value = climate::climate_action_to_string(obj->action);
climate_setting_row_(stream, obj, area, node, friendly_name, climate_trait_category, climate_trait_value);

View File

@@ -81,7 +81,7 @@ CONFIG_SCHEMA = (
cv.int_range(min=0, max=0xFFFF, max_included=False),
),
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION): cv.pressure,
cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature_delta,
cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature,
cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id(
sensor.Sensor
),

View File

@@ -28,6 +28,21 @@
namespace esphome {
namespace statsd {
using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR };
using sensors_t = struct {
const char *name;
sensor_type_t type;
union {
#ifdef USE_SENSOR
esphome::sensor::Sensor *sensor;
#endif
#ifdef USE_BINARY_SENSOR
esphome::binary_sensor::BinarySensor *binary_sensor;
#endif
};
};
class StatsdComponent : public PollingComponent {
public:
~StatsdComponent();
@@ -56,20 +71,6 @@ class StatsdComponent : public PollingComponent {
const char *prefix_;
uint16_t port_;
using sensor_type_t = enum { TYPE_SENSOR, TYPE_BINARY_SENSOR };
using sensors_t = struct {
const char *name;
sensor_type_t type;
union {
#ifdef USE_SENSOR
esphome::sensor::Sensor *sensor;
#endif
#ifdef USE_BINARY_SENSOR
esphome::binary_sensor::BinarySensor *binary_sensor;
#endif
};
};
std::vector<sensors_t> sensors_;
#ifdef USE_ESP8266

View File

@@ -71,14 +71,9 @@ from esphome.const import (
CONF_VISUAL,
)
CONF_DEFAULT_PRESET = "default_preset"
CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION = "humidity_control_dehumidify_action"
CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION = "humidity_control_humidify_action"
CONF_HUMIDITY_CONTROL_OFF_ACTION = "humidity_control_off_action"
CONF_HUMIDITY_HYSTERESIS = "humidity_hysteresis"
CONF_ON_BOOT_RESTORE_FROM = "on_boot_restore_from"
CONF_PRESET_CHANGE = "preset_change"
CONF_TARGET_HUMIDITY_CHANGE_ACTION = "target_humidity_change_action"
CONF_DEFAULT_PRESET = "default_preset"
CONF_ON_BOOT_RESTORE_FROM = "on_boot_restore_from"
CODEOWNERS = ["@kbx81"]
@@ -246,14 +241,6 @@ def validate_thermostat(config):
CONF_MAX_HEATING_RUN_TIME,
CONF_SUPPLEMENTAL_HEATING_ACTION,
],
CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION: [
CONF_HUMIDITY_CONTROL_OFF_ACTION,
CONF_HUMIDITY_SENSOR,
],
CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION: [
CONF_HUMIDITY_CONTROL_OFF_ACTION,
CONF_HUMIDITY_SENSOR,
],
}
for config_trigger, req_triggers in requirements.items():
for req_trigger in req_triggers:
@@ -351,7 +338,7 @@ def validate_thermostat(config):
# Warn about using the removed CONF_DEFAULT_MODE and advise users
if CONF_DEFAULT_MODE in config and config[CONF_DEFAULT_MODE] is not None:
raise cv.Invalid(
f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}"
f"{CONF_DEFAULT_MODE} is no longer valid. Please switch to using presets and specify a {CONF_DEFAULT_PRESET}."
)
default_mode = config[CONF_DEFAULT_MODE]
@@ -601,24 +588,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(
single=True
),
cv.Optional(
CONF_TARGET_HUMIDITY_CHANGE_ACTION
): automation.validate_automation(single=True),
cv.Optional(
CONF_TARGET_TEMPERATURE_CHANGE_ACTION
): automation.validate_automation(single=True),
cv.Exclusive(
CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION,
group_of_exclusion="humidity_control",
): automation.validate_automation(single=True),
cv.Exclusive(
CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION,
group_of_exclusion="humidity_control",
): automation.validate_automation(single=True),
cv.Optional(
CONF_HUMIDITY_CONTROL_OFF_ACTION
): automation.validate_automation(single=True),
cv.Optional(CONF_HUMIDITY_HYSTERESIS, default=1.0): cv.percentage,
cv.Optional(CONF_DEFAULT_MODE, default=None): cv.valid,
cv.Optional(CONF_DEFAULT_PRESET): cv.templatable(cv.string),
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
@@ -910,39 +882,12 @@ async def to_code(config):
config[CONF_SWING_VERTICAL_ACTION],
)
cg.add(var.set_supports_swing_mode_vertical(True))
if CONF_TARGET_HUMIDITY_CHANGE_ACTION in config:
await automation.build_automation(
var.get_humidity_change_trigger(),
[],
config[CONF_TARGET_HUMIDITY_CHANGE_ACTION],
)
if CONF_TARGET_TEMPERATURE_CHANGE_ACTION in config:
await automation.build_automation(
var.get_temperature_change_trigger(),
[],
config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION],
)
if CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION in config:
cg.add(var.set_supports_dehumidification(True))
await automation.build_automation(
var.get_humidity_control_dehumidify_action_trigger(),
[],
config[CONF_HUMIDITY_CONTROL_DEHUMIDIFY_ACTION],
)
if CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION in config:
cg.add(var.set_supports_humidification(True))
await automation.build_automation(
var.get_humidity_control_humidify_action_trigger(),
[],
config[CONF_HUMIDITY_CONTROL_HUMIDIFY_ACTION],
)
if CONF_HUMIDITY_CONTROL_OFF_ACTION in config:
await automation.build_automation(
var.get_humidity_control_off_action_trigger(),
[],
config[CONF_HUMIDITY_CONTROL_OFF_ACTION],
)
cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS]))
if CONF_PRESET in config:
for preset_config in config[CONF_PRESET]:

View File

@@ -32,7 +32,6 @@ void ThermostatClimate::setup() {
if (this->humidity_sensor_ != nullptr) {
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->current_humidity = state;
this->switch_to_humidity_control_action_(this->compute_humidity_control_action_());
this->publish_state();
});
this->current_humidity = this->humidity_sensor_->state;
@@ -85,8 +84,6 @@ void ThermostatClimate::refresh() {
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
this->switch_to_fan_mode_(this->fan_mode.value(), false);
this->switch_to_swing_mode_(this->swing_mode, false);
this->switch_to_humidity_control_action_(this->compute_humidity_control_action_());
this->check_humidity_change_trigger_();
this->check_temperature_change_trigger_();
this->publish_state();
}
@@ -132,11 +129,6 @@ bool ThermostatClimate::hysteresis_valid() {
return true;
}
bool ThermostatClimate::humidity_hysteresis_valid() {
return !std::isnan(this->humidity_hysteresis_) && this->humidity_hysteresis_ >= 0.0f &&
this->humidity_hysteresis_ < 100.0f;
}
bool ThermostatClimate::limit_setpoints_for_heat_cool() {
return this->mode == climate::CLIMATE_MODE_HEAT_COOL ||
(this->mode == climate::CLIMATE_MODE_AUTO && this->supports_heat_cool_);
@@ -197,16 +189,6 @@ void ThermostatClimate::validate_target_temperature_high() {
}
}
void ThermostatClimate::validate_target_humidity() {
if (std::isnan(this->target_humidity)) {
this->target_humidity =
(this->get_traits().get_visual_max_humidity() - this->get_traits().get_visual_min_humidity()) / 2.0f;
} else {
this->target_humidity = clamp<float>(this->target_humidity, this->get_traits().get_visual_min_humidity(),
this->get_traits().get_visual_max_humidity());
}
}
void ThermostatClimate::control(const climate::ClimateCall &call) {
bool target_temperature_high_changed = false;
@@ -253,10 +235,6 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
this->validate_target_temperature();
}
}
if (call.get_target_humidity().has_value()) {
this->target_humidity = call.get_target_humidity().value();
this->validate_target_humidity();
}
// make any changes happen
this->refresh();
}
@@ -272,9 +250,6 @@ climate::ClimateTraits ThermostatClimate::traits() {
if (this->humidity_sensor_ != nullptr)
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
if (this->supports_humidification_ || this->supports_dehumidification_)
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
if (this->supports_auto_)
traits.add_supported_mode(climate::CLIMATE_MODE_AUTO);
if (this->supports_heat_cool_)
@@ -448,28 +423,6 @@ climate::ClimateAction ThermostatClimate::compute_supplemental_action_() {
return target_action;
}
HumidificationAction ThermostatClimate::compute_humidity_control_action_() {
auto target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
// if hysteresis value or current_humidity is not valid, we go to OFF
if (std::isnan(this->current_humidity) || !this->humidity_hysteresis_valid()) {
return THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
}
// ensure set point is valid before computing the action
this->validate_target_humidity();
// everything has been validated so we can now safely compute the action
if (this->dehumidification_required_() && this->humidification_required_()) {
// this is bad and should never happen, so just stop.
// target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
} else if (this->supports_dehumidification_ && this->dehumidification_required_()) {
target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
} else if (this->supports_humidification_ && this->humidification_required_()) {
target_action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
}
return target_action;
}
void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool publish_state) {
// setup_complete_ helps us ensure an action is called immediately after boot
if ((action == this->action) && this->setup_complete_) {
@@ -643,44 +596,6 @@ void ThermostatClimate::trigger_supplemental_action_() {
}
}
void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction action) {
// setup_complete_ helps us ensure an action is called immediately after boot
if ((action == this->humidification_action_) && this->setup_complete_) {
// already in target mode
return;
}
Trigger<> *trig = this->humidity_control_off_action_trigger_;
switch (action) {
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF:
// trig = this->humidity_control_off_action_trigger_;
ESP_LOGVV(TAG, "Switching to HUMIDIFICATION_OFF action");
break;
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY:
trig = this->humidity_control_dehumidify_action_trigger_;
ESP_LOGVV(TAG, "Switching to DEHUMIDIFY action");
break;
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY:
trig = this->humidity_control_humidify_action_trigger_;
ESP_LOGVV(TAG, "Switching to HUMIDIFY action");
break;
case THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE:
default:
action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF;
// trig = this->humidity_control_off_action_trigger_;
}
if (this->prev_humidity_control_trigger_ != nullptr) {
this->prev_humidity_control_trigger_->stop_action();
this->prev_humidity_control_trigger_ = nullptr;
}
this->humidification_action_ = action;
this->prev_humidity_control_trigger_ = trig;
if (trig != nullptr) {
trig->trigger();
}
}
void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state) {
// setup_complete_ helps us ensure an action is called immediately after boot
if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) {
@@ -972,20 +887,6 @@ void ThermostatClimate::idle_on_timer_callback_() {
this->switch_to_supplemental_action_(this->compute_supplemental_action_());
}
void ThermostatClimate::check_humidity_change_trigger_() {
if ((this->prev_target_humidity_ == this->target_humidity) && this->setup_complete_) {
return; // nothing changed, no reason to trigger
} else {
// save the new temperature so we can check it again later; the trigger will fire below
this->prev_target_humidity_ = this->target_humidity;
}
// trigger the action
Trigger<> *trig = this->humidity_change_trigger_;
if (trig != nullptr) {
trig->trigger();
}
}
void ThermostatClimate::check_temperature_change_trigger_() {
if (this->supports_two_points_) {
// setup_complete_ helps us ensure an action is called immediately after boot
@@ -1095,32 +996,6 @@ bool ThermostatClimate::supplemental_heating_required_() {
(this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING));
}
bool ThermostatClimate::dehumidification_required_() {
if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) {
// if the current humidity exceeds the target + hysteresis, dehumidification is required
return true;
} else if (this->current_humidity < this->target_humidity - this->humidity_hysteresis_) {
// if the current humidity is less than the target - hysteresis, dehumidification should stop
return false;
}
// if we get here, the current humidity is between target + hysteresis and target - hysteresis,
// so the action should not change
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY;
}
bool ThermostatClimate::humidification_required_() {
if (this->current_humidity < this->target_humidity - this->humidity_hysteresis_) {
// if the current humidity is below the target - hysteresis, humidification is required
return true;
} else if (this->current_humidity > this->target_humidity + this->humidity_hysteresis_) {
// if the current humidity is above the target + hysteresis, humidification should stop
return false;
}
// if we get here, the current humidity is between target - hysteresis and target + hysteresis,
// so the action should not change
return this->humidification_action_ == THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY;
}
void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) {
if (this->supports_heat_) {
ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C",
@@ -1277,12 +1152,8 @@ ThermostatClimate::ThermostatClimate()
swing_mode_off_trigger_(new Trigger<>()),
swing_mode_horizontal_trigger_(new Trigger<>()),
swing_mode_vertical_trigger_(new Trigger<>()),
humidity_change_trigger_(new Trigger<>()),
temperature_change_trigger_(new Trigger<>()),
preset_change_trigger_(new Trigger<>()),
humidity_control_dehumidify_action_trigger_(new Trigger<>()),
humidity_control_humidify_action_trigger_(new Trigger<>()),
humidity_control_off_action_trigger_(new Trigger<>()) {}
preset_change_trigger_(new Trigger<>()) {}
void ThermostatClimate::set_default_preset(const std::string &custom_preset) {
this->default_custom_preset_ = custom_preset;
@@ -1346,9 +1217,6 @@ void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sen
void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) {
this->humidity_sensor_ = humidity_sensor;
}
void ThermostatClimate::set_humidity_hysteresis(float humidity_hysteresis) {
this->humidity_hysteresis_ = std::clamp<float>(humidity_hysteresis, 0.0f, 100.0f);
}
void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; }
void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) {
this->supports_heat_cool_ = supports_heat_cool;
@@ -1416,18 +1284,6 @@ void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mod
void ThermostatClimate::set_supports_two_points(bool supports_two_points) {
this->supports_two_points_ = supports_two_points;
}
void ThermostatClimate::set_supports_dehumidification(bool supports_dehumidification) {
this->supports_dehumidification_ = supports_dehumidification;
if (supports_dehumidification) {
this->supports_humidification_ = false;
}
}
void ThermostatClimate::set_supports_humidification(bool supports_humidification) {
this->supports_humidification_ = supports_humidification;
if (supports_humidification) {
this->supports_dehumidification_ = false;
}
}
Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; }
Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() const {
@@ -1461,18 +1317,8 @@ Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this-
Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; }
Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; }
Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; }
Trigger<> *ThermostatClimate::get_humidity_change_trigger() const { return this->humidity_change_trigger_; }
Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; }
Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; }
Trigger<> *ThermostatClimate::get_humidity_control_dehumidify_action_trigger() const {
return this->humidity_control_dehumidify_action_trigger_;
}
Trigger<> *ThermostatClimate::get_humidity_control_humidify_action_trigger() const {
return this->humidity_control_humidify_action_trigger_;
}
Trigger<> *ThermostatClimate::get_humidity_control_off_action_trigger() const {
return this->humidity_control_off_action_trigger_;
}
void ThermostatClimate::dump_config() {
LOG_CLIMATE("", "Thermostat", this);
@@ -1576,12 +1422,7 @@ void ThermostatClimate::dump_config() {
" OFF: %s\n"
" HORIZONTAL: %s\n"
" VERTICAL: %s\n"
" Supports TWO SET POINTS: %s\n"
" Supported Humidity Parameters:\n"
" CURRENT: %s\n"
" TARGET: %s\n"
" DEHUMIDIFICATION: %s\n"
" HUMIDIFICATION: %s",
" Supports TWO SET POINTS: %s",
YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_),
YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_),
YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_),
@@ -1589,10 +1430,7 @@ void ThermostatClimate::dump_config() {
YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_),
YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_),
YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_),
YESNO(this->supports_two_points_),
YESNO(this->get_traits().has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)),
YESNO(this->supports_dehumidification_ || this->supports_humidification_),
YESNO(this->supports_dehumidification_), YESNO(this->supports_humidification_));
YESNO(this->supports_two_points_));
if (!this->preset_config_.empty()) {
ESP_LOGCONFIG(TAG, " Supported PRESETS:");

View File

@@ -13,13 +13,6 @@
namespace esphome {
namespace thermostat {
enum HumidificationAction : uint8_t {
THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF = 0,
THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY = 1,
THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY = 2,
THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE,
};
enum ThermostatClimateTimerIndex : uint8_t {
THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME = 0,
THERMOSTAT_TIMER_COOLING_OFF = 1,
@@ -97,7 +90,6 @@ class ThermostatClimate : public climate::Climate, public Component {
void set_idle_minimum_time_in_sec(uint32_t time);
void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
void set_humidity_hysteresis(float humidity_hysteresis);
void set_use_startup_delay(bool use_startup_delay);
void set_supports_auto(bool supports_auto);
void set_supports_heat_cool(bool supports_heat_cool);
@@ -123,8 +115,6 @@ class ThermostatClimate : public climate::Climate, public Component {
void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal);
void set_supports_swing_mode_off(bool supports_swing_mode_off);
void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical);
void set_supports_dehumidification(bool supports_dehumidification);
void set_supports_humidification(bool supports_humidification);
void set_supports_two_points(bool supports_two_points);
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
@@ -158,12 +148,8 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *get_swing_mode_horizontal_trigger() const;
Trigger<> *get_swing_mode_off_trigger() const;
Trigger<> *get_swing_mode_vertical_trigger() const;
Trigger<> *get_humidity_change_trigger() const;
Trigger<> *get_temperature_change_trigger() const;
Trigger<> *get_preset_change_trigger() const;
Trigger<> *get_humidity_control_dehumidify_action_trigger() const;
Trigger<> *get_humidity_control_humidify_action_trigger() const;
Trigger<> *get_humidity_control_off_action_trigger() const;
/// Get current hysteresis values
float cool_deadband();
float cool_overrun();
@@ -180,13 +166,11 @@ class ThermostatClimate : public climate::Climate, public Component {
climate::ClimateFanMode locked_fan_mode();
/// Set point and hysteresis validation
bool hysteresis_valid(); // returns true if valid
bool humidity_hysteresis_valid(); // returns true if valid
bool limit_setpoints_for_heat_cool(); // returns true if set points should be further limited within visual range
void validate_target_temperature();
void validate_target_temperatures(bool pin_target_temperature_high);
void validate_target_temperature_low();
void validate_target_temperature_high();
void validate_target_humidity();
protected:
/// Override control to change settings of the climate device.
@@ -208,13 +192,11 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Re-compute the required action of this climate controller.
climate::ClimateAction compute_action_(bool ignore_timers = false);
climate::ClimateAction compute_supplemental_action_();
HumidificationAction compute_humidity_control_action_();
/// Switch the climate device to the given climate action.
void switch_to_action_(climate::ClimateAction action, bool publish_state = true);
void switch_to_supplemental_action_(climate::ClimateAction action);
void trigger_supplemental_action_();
void switch_to_humidity_control_action_(HumidificationAction action);
/// Switch the climate device to the given climate fan mode.
void switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state = true);
@@ -225,9 +207,6 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Switch the climate device to the given climate swing mode.
void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state = true);
/// Check if the humidity change trigger should be called.
void check_humidity_change_trigger_();
/// Check if the temperature change trigger should be called.
void check_temperature_change_trigger_();
@@ -264,8 +243,6 @@ class ThermostatClimate : public climate::Climate, public Component {
bool heating_required_();
bool supplemental_cooling_required_();
bool supplemental_heating_required_();
bool dehumidification_required_();
bool humidification_required_();
void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config);
@@ -282,9 +259,6 @@ class ThermostatClimate : public climate::Climate, public Component {
/// The current supplemental action
climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF};
/// The current humidification action
HumidificationAction humidification_action_{THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE};
/// Default standard preset to use on start up
climate::ClimatePreset default_preset_{};
@@ -347,12 +321,6 @@ class ThermostatClimate : public climate::Climate, public Component {
/// A false value means that the controller has no such support.
bool supports_two_points_{false};
/// Whether the controller supports dehumidification and/or humidification
///
/// A false value means that the controller has no such support.
bool supports_dehumidification_{false};
bool supports_humidification_{false};
/// Flags indicating if maximum allowable run time was exceeded
bool cooling_max_runtime_exceeded_{false};
bool heating_max_runtime_exceeded_{false};
@@ -363,10 +331,9 @@ class ThermostatClimate : public climate::Climate, public Component {
/// setup_complete_ blocks modifying/resetting the temps immediately after boot
bool setup_complete_{false};
/// Store previously-known humidity and temperatures
/// Store previously-known temperatures
///
/// These are used to determine when a temperature/humidity has changed
float prev_target_humidity_{NAN};
/// These are used to determine when the temperature change trigger/action needs to be called
float prev_target_temperature_{NAN};
float prev_target_temperature_low_{NAN};
float prev_target_temperature_high_{NAN};
@@ -380,9 +347,6 @@ class ThermostatClimate : public climate::Climate, public Component {
float heating_deadband_{0};
float heating_overrun_{0};
/// Hysteresis values used for computing humidification action
float humidity_hysteresis_{0};
/// Maximum allowable temperature deltas before engaging supplemental cooling/heating actions
float supplemental_cool_delta_{0};
float supplemental_heat_delta_{0};
@@ -484,24 +448,12 @@ class ThermostatClimate : public climate::Climate, public Component {
/// The trigger to call when the controller should switch the swing mode to "vertical".
Trigger<> *swing_mode_vertical_trigger_{nullptr};
/// The trigger to call when the target humidity changes.
Trigger<> *humidity_change_trigger_{nullptr};
/// The trigger to call when the target temperature(s) change(es).
Trigger<> *temperature_change_trigger_{nullptr};
/// The trigger to call when the preset mode changes
Trigger<> *preset_change_trigger_{nullptr};
/// The trigger to call when dehumidification is required
Trigger<> *humidity_control_dehumidify_action_trigger_{nullptr};
/// The trigger to call when humidification is required
Trigger<> *humidity_control_humidify_action_trigger_{nullptr};
/// The trigger to call when (de)humidification should stop
Trigger<> *humidity_control_off_action_trigger_{nullptr};
/// A reference to the trigger that was previously active.
///
/// This is so that the previous trigger can be stopped before enabling a new one
@@ -510,7 +462,6 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *prev_fan_mode_trigger_{nullptr};
Trigger<> *prev_mode_trigger_{nullptr};
Trigger<> *prev_swing_mode_trigger_{nullptr};
Trigger<> *prev_humidity_control_trigger_{nullptr};
/// Default custom preset to use on start up
std::string default_custom_preset_{};

View File

@@ -27,14 +27,6 @@ class RealTimeClock : public PollingComponent {
this->apply_timezone_();
}
/// Set the time zone from raw buffer, only if it differs from the current one.
void set_timezone(const char *tz, size_t len) {
if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) {
this->timezone_.assign(tz, len);
this->apply_timezone_();
}
}
/// Get the time zone currently in use.
std::string get_timezone() { return this->timezone_; }
#endif

View File

@@ -953,21 +953,11 @@ void VoiceAssistant::on_set_configuration(const std::vector<std::string> &active
}
}
}
// Mark configuration dirty to trigger rebuild on next get_configuration() call.
this->config_needs_rebuild_ = true;
}
#endif
};
const Configuration &VoiceAssistant::get_configuration() {
// Return cached configuration if it hasn't changed. This prevents a use-after-free
// race condition when API message serialization creates StringRef pointers to strings
// in config_.available_wake_words, and avoids wastefully rebuilding on every call.
if (!this->config_needs_rebuild_) {
return this->config_;
}
this->config_.available_wake_words.clear();
this->config_.active_wake_words.clear();
@@ -996,8 +986,6 @@ const Configuration &VoiceAssistant::get_configuration() {
}
#endif
// Mark configuration as clean now that we've rebuilt it
this->config_needs_rebuild_ = false;
return this->config_;
};

View File

@@ -112,10 +112,7 @@ class VoiceAssistant : public Component {
void set_microphone_source(microphone::MicrophoneSource *mic_source) { this->mic_source_ = mic_source; }
#ifdef USE_MICRO_WAKE_WORD
void set_micro_wake_word(micro_wake_word::MicroWakeWord *mww) {
this->micro_wake_word_ = mww;
this->config_needs_rebuild_ = true;
}
void set_micro_wake_word(micro_wake_word::MicroWakeWord *mww) { this->micro_wake_word_ = mww; }
#endif
#ifdef USE_SPEAKER
void set_speaker(speaker::Speaker *speaker) {
@@ -316,11 +313,7 @@ class VoiceAssistant : public Component {
bool udp_socket_running_{false};
bool start_udp_socket_();
// Configuration caching for safety and performance. Only rebuild when config_needs_rebuild_
// is true to prevent use-after-free race condition when StringRef pointers reference
// wake word strings during API message serialization, and to avoid wasteful rebuilding.
Configuration config_{};
bool config_needs_rebuild_{true};
#ifdef USE_MICRO_WAKE_WORD
micro_wake_word::MicroWakeWord *micro_wake_word_{nullptr};

View File

@@ -1325,7 +1325,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
root["max_temp"] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy);
root["min_temp"] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy);
root["step"] = traits.get_visual_target_temperature_step();
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
if (traits.get_supports_action()) {
root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action));
root["state"] = root["action"];
has_state = true;
@@ -1345,15 +1345,14 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
if (traits.get_supports_swing_modes()) {
root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode));
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
if (traits.get_supports_current_temperature()) {
if (!std::isnan(obj->current_temperature)) {
root["current_temperature"] = value_accuracy_to_string(obj->current_temperature, current_accuracy);
} else {
root["current_temperature"] = "NA";
}
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
if (traits.get_supports_two_point_target_temperature()) {
root["target_temperature_low"] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy);
root["target_temperature_high"] = value_accuracy_to_string(obj->target_temperature_high, target_accuracy);
if (!has_state) {

View File

@@ -776,12 +776,13 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
uint16_t number = it.number;
auto records = std::make_unique<wifi_ap_record_t[]>(number);
err = esp_wifi_scan_get_ap_records(&number, records.get());
std::vector<wifi_ap_record_t> records(number);
err = esp_wifi_scan_get_ap_records(&number, records.data());
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
return;
}
records.resize(number);
scan_result_.init(number);
for (int i = 0; i < number; i++) {

View File

@@ -1,34 +0,0 @@
import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import CONF_ESPHOME, CONF_ID, CONF_NAME, Framework
import esphome.final_validate as fv
zephyr_ble_server_ns = cg.esphome_ns.namespace("zephyr_ble_server")
BLEServer = zephyr_ble_server_ns.class_("BLEServer", cg.Component)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLEServer),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_framework(Framework.ZEPHYR),
)
def _final_validate(_):
full_config = fv.full_config.get()
zephyr_add_prj_conf("BT_DEVICE_NAME", full_config[CONF_ESPHOME][CONF_NAME])
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
zephyr_add_prj_conf("BT", True)
zephyr_add_prj_conf("BT_PERIPHERAL", True)
zephyr_add_prj_conf("BT_RX_STACK_SIZE", 1536)
# zephyr_add_prj_conf("BT_LL_SW_SPLIT", True)
await cg.register_component(var, config)

View File

@@ -1,100 +0,0 @@
#ifdef USE_ZEPHYR
#include "ble_server.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
namespace esphome::zephyr_ble_server {
static const char *const TAG = "zephyr_ble_server";
static struct k_work advertise_work; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)
static const struct bt_data AD[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};
static const struct bt_data SD[] = {
#ifdef USE_OTA
BT_DATA_BYTES(BT_DATA_UUID128_ALL, 0x84, 0xaa, 0x60, 0x74, 0x52, 0x8a, 0x8b, 0x86, 0xd3, 0x4c, 0xb7, 0x1d, 0x1d,
0xdc, 0x53, 0x8d),
#endif
};
const struct bt_le_adv_param *const ADV_PARAM = BT_LE_ADV_CONN;
static void advertise(struct k_work *work) {
int rc = bt_le_adv_stop();
if (rc) {
ESP_LOGE(TAG, "Advertising failed to stop (rc %d)", rc);
}
rc = bt_le_adv_start(ADV_PARAM, AD, ARRAY_SIZE(AD), SD, ARRAY_SIZE(SD));
if (rc) {
ESP_LOGE(TAG, "Advertising failed to start (rc %d)", rc);
return;
}
ESP_LOGI(TAG, "Advertising successfully started");
}
static void connected(struct bt_conn *conn, uint8_t err) {
if (err) {
ESP_LOGE(TAG, "Connection failed (err 0x%02x)", err);
} else {
ESP_LOGI(TAG, "Connected");
}
}
static void disconnected(struct bt_conn *conn, uint8_t reason) {
ESP_LOGI(TAG, "Disconnected (reason 0x%02x)", reason);
k_work_submit(&advertise_work);
}
static void bt_ready(int err) {
if (err != 0) {
ESP_LOGE(TAG, "Bluetooth failed to initialise: %d", err);
} else {
k_work_submit(&advertise_work);
}
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};
void BLEServer::setup() {
k_work_init(&advertise_work, advertise);
resume_();
}
void BLEServer::loop() {
if (this->suspended_) {
resume_();
this->suspended_ = false;
}
}
void BLEServer::resume_() {
int rc = bt_enable(bt_ready);
if (rc != 0) {
ESP_LOGE(TAG, "Bluetooth enable failed: %d", rc);
return;
}
}
void BLEServer::on_shutdown() {
struct k_work_sync sync;
k_work_cancel_sync(&advertise_work, &sync);
bt_disable();
this->suspended_ = true;
}
} // namespace esphome::zephyr_ble_server
#endif

View File

@@ -1,19 +0,0 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/component.h"
namespace esphome::zephyr_ble_server {
class BLEServer : public Component {
public:
void setup() override;
void loop() override;
void on_shutdown() override;
protected:
void resume_();
bool suspended_ = false;
};
} // namespace esphome::zephyr_ble_server
#endif

View File

@@ -328,30 +328,17 @@ void HOT Scheduler::call(uint32_t now) {
// Single-core platforms don't use this queue and fall back to the heap-based approach.
//
// Note: Items cancelled via cancel_item_locked_() are marked with remove=true but still
// processed here. They are skipped during execution by should_skip_item_().
// This is intentional - no memory leak occurs.
//
// We use an index (defer_queue_front_) to track the read position instead of calling
// erase() on every pop, which would be O(n). The queue is processed once per loop -
// any items added during processing are left for the next loop iteration.
// Snapshot the queue end point - only process items that existed at loop start
// Items added during processing (by callbacks or other threads) run next loop
// No lock needed: single consumer (main loop), stale read just means we process less this iteration
size_t defer_queue_end = this->defer_queue_.size();
while (this->defer_queue_front_ < defer_queue_end) {
// processed here. They are removed from the queue normally via pop_front() but skipped
// during execution by should_skip_item_(). This is intentional - no memory leak occurs.
while (!this->defer_queue_.empty()) {
// The outer check is done without a lock for performance. If the queue
// appears non-empty, we lock and process an item. We don't need to check
// empty() again inside the lock because only this thread can remove items.
std::unique_ptr<SchedulerItem> item;
{
LockGuard lock(this->lock_);
// SAFETY: Moving out the unique_ptr leaves a nullptr in the vector at defer_queue_front_.
// This is intentional and safe because:
// 1. The vector is only cleaned up by cleanup_defer_queue_locked_() at the end of this function
// 2. Any code iterating defer_queue_ MUST check for nullptr items (see mark_matching_items_removed_
// and has_cancelled_timeout_in_container_ in scheduler.h)
// 3. The lock protects concurrent access, but the nullptr remains until cleanup
item = std::move(this->defer_queue_[this->defer_queue_front_]);
this->defer_queue_front_++;
item = std::move(this->defer_queue_.front());
this->defer_queue_.pop_front();
}
// Execute callback without holding lock to prevent deadlocks
@@ -362,13 +349,6 @@ void HOT Scheduler::call(uint32_t now) {
// Recycle the defer item after execution
this->recycle_item_(std::move(item));
}
// If we've consumed all items up to the snapshot point, clean up the dead space
// Single consumer (main loop), so no lock needed for this check
if (this->defer_queue_front_ >= defer_queue_end) {
LockGuard lock(this->lock_);
this->cleanup_defer_queue_locked_();
}
#endif /* not ESPHOME_THREAD_SINGLE */
// Convert the fresh timestamp from main loop to 64-bit for scheduler operations

View File

@@ -264,36 +264,6 @@ class Scheduler {
// Helper to recycle a SchedulerItem
void recycle_item_(std::unique_ptr<SchedulerItem> item);
#ifndef ESPHOME_THREAD_SINGLE
// Helper to cleanup defer_queue_ after processing
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
inline void cleanup_defer_queue_locked_() {
// Check if new items were added by producers during processing
if (this->defer_queue_front_ >= this->defer_queue_.size()) {
// Common case: no new items - clear everything
this->defer_queue_.clear();
} else {
// Rare case: new items were added during processing - compact the vector
// This only happens when:
// 1. A deferred callback calls defer() again, or
// 2. Another thread calls defer() while we're processing
//
// Move unprocessed items (added during this loop) to the front for next iteration
//
// SAFETY: Compacted items may include cancelled items (marked for removal via
// cancel_item_locked_() during execution). This is safe because should_skip_item_()
// checks is_item_removed_() before executing, so cancelled items will be skipped
// and recycled on the next loop iteration.
size_t remaining = this->defer_queue_.size() - this->defer_queue_front_;
for (size_t i = 0; i < remaining; i++) {
this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]);
}
this->defer_queue_.resize(remaining);
}
this->defer_queue_front_ = 0;
}
#endif /* not ESPHOME_THREAD_SINGLE */
// Helper to check if item is marked for removal (platform-specific)
// Returns true if item should be skipped, handles platform-specific synchronization
// For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
@@ -312,18 +282,13 @@ class Scheduler {
// Helper to mark matching items in a container as removed
// Returns the number of items marked for removal
// IMPORTANT: Caller must hold the scheduler lock before calling this function.
// For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
// function.
template<typename Container>
size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr,
SchedulerItem::Type type, bool match_retry) {
size_t count = 0;
for (auto &item : container) {
// Skip nullptr items (can happen in defer_queue_ when items are being processed)
// The defer_queue_ uses index-based processing: items are std::moved out but left in the
// vector as nullptr until cleanup. Even though this function is called with lock held,
// the vector can still contain nullptr items from the processing loop. This check prevents crashes.
if (!item)
continue;
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
// Mark item for removal (platform-specific)
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
@@ -346,12 +311,6 @@ class Scheduler {
bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,
bool match_retry) const {
for (const auto &item : container) {
// Skip nullptr items (can happen in defer_queue_ when items are being processed)
// The defer_queue_ uses index-based processing: items are std::moved out but left in the
// vector as nullptr until cleanup. If this function is called during defer queue processing,
// it will iterate over these nullptr items. This check prevents crashes.
if (!item)
continue;
if (is_item_removed_(item.get()) &&
this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
/* skip_removed= */ false)) {
@@ -365,12 +324,9 @@ class Scheduler {
std::vector<std::unique_ptr<SchedulerItem>> items_;
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
#ifndef ESPHOME_THREAD_SINGLE
// Single-core platforms don't need the defer queue and save ~32 bytes of RAM
// Using std::vector instead of std::deque avoids 512-byte chunked allocations
// Index tracking avoids O(n) erase() calls when draining the queue each loop
std::vector<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
size_t defer_queue_front_{0}; // Index of first valid item in defer_queue_ (tracks consumed items)
#endif /* ESPHOME_THREAD_SINGLE */
// Single-core platforms don't need the defer queue and save 40 bytes of RAM
std::deque<std::unique_ptr<SchedulerItem>> defer_queue_; // FIFO queue for defer() calls
#endif /* ESPHOME_THREAD_SINGLE */
uint32_t to_remove_{0};
// Memory pool for recycling SchedulerItem objects to reduce heap churn.

View File

@@ -1058,8 +1058,7 @@ class DownloadBinaryRequestHandler(BaseHandler):
"download",
f"{storage_json.name}-{file_name}",
)
path = storage_json.firmware_bin_path.parent.joinpath(file_name)
path = storage_json.firmware_bin_path.with_name(file_name)
if not path.is_file():
args = ["esphome", "idedata", settings.rel_path(configuration)]

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20251013.0
aioesphomeapi==42.2.0
aioesphomeapi==42.0.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import

View File

@@ -1415,13 +1415,7 @@ class RepeatedTypeInfo(TypeInfo):
super().__init__(field)
# Check if this is a pointer field by looking for container_pointer option
self._container_type = get_field_opt(field, pb.container_pointer, "")
# Check for non-template container pointer
self._container_no_template = get_field_opt(
field, pb.container_pointer_no_template, ""
)
self._use_pointer = bool(self._container_type) or bool(
self._container_no_template
)
self._use_pointer = bool(self._container_type)
# Check if this should use FixedVector instead of std::vector
self._use_fixed_vector = get_field_opt(field, pb.fixed_vector, False)
@@ -1440,18 +1434,12 @@ class RepeatedTypeInfo(TypeInfo):
@property
def cpp_type(self) -> str:
if self._container_no_template:
# Non-template container: use type as-is without appending template parameters
return f"const {self._container_no_template}*"
if self._use_pointer and self._container_type:
# For pointer fields, use the specified container type
# Two cases:
# 1. "std::set<climate::ClimateMode>" - Full type with template params, use as-is
# 2. "std::set" - No <>, append the element type
# If the container type already includes the element type (e.g., std::set<climate::ClimateMode>)
# use it as-is, otherwise append the element type
if "<" in self._container_type and ">" in self._container_type:
# Has template parameters specified, use as-is
return f"const {self._container_type}*"
# No <> at all, append element type
return f"const {self._container_type}<{self._ti.cpp_type}>*"
if self._use_fixed_vector:
return f"FixedVector<{self._ti.cpp_type}>"

View File

@@ -25,7 +25,6 @@ int main() { return 0;}
Path(zephyr_dir / "prj.conf").write_text(
"""
CONFIG_NEWLIB_LIBC=y
CONFIG_BT=y
CONFIG_ADC=y
""",
encoding="utf-8",

View File

@@ -8,12 +8,14 @@ sensor:
lambda: |-
if (millis() > 10000) {
return 0.6;
} else {
return 0.0;
}
return 0.0;
- platform: template
id: template_temperature
lambda: |-
if (millis() > 10000) {
return 42.0;
} else {
return 0.0;
}
return 0.0;

View File

@@ -5,8 +5,9 @@ sensor:
lambda: |-
if (millis() > 10000) {
return 42.0;
} else {
return 0.0;
}
return 0.0;
update_interval: 15s
binary_sensor:

View File

@@ -23,8 +23,9 @@ binary_sensor:
- lambda: |-
if (id(some_binary_sensor).state) {
return x;
} else {
return {};
}
return {};
- settle: 100ms
- timeout: 10s

View File

@@ -4,22 +4,25 @@ binary_sensor:
lambda: |-
if (millis() > 10000) {
return true;
} else {
return false;
}
return false;
- platform: template
id: bin2
lambda: |-
if (millis() > 20000) {
return true;
} else {
return false;
}
return false;
- platform: template
id: bin3
lambda: |-
if (millis() > 30000) {
return true;
} else {
return false;
}
return false;
sensor:
- platform: binary_sensor_map

View File

@@ -1,2 +0,0 @@
ble_nus:
type: logs

View File

@@ -1,2 +0,0 @@
ble_nus:
type: logs

View File

@@ -4,15 +4,17 @@ sensor:
lambda: |-
if (millis() > 10000) {
return 0.6;
} else {
return 0.0;
}
return 0.0;
- platform: template
id: template_temperature2
lambda: |-
if (millis() > 20000) {
return 0.8;
} else {
return 0.0;
}
return 0.0;
- platform: combination
type: kalman
name: Kalman-filtered temperature

View File

@@ -4,8 +4,9 @@ binary_sensor:
lambda: |-
if (millis() > 10000) {
return true;
} else {
return false;
}
return false;
sensor:
- platform: duty_time

View File

@@ -4,8 +4,9 @@ binary_sensor:
lambda: |-
if (millis() > 10000) {
return true;
} else {
return false;
}
return false;
switch:
- platform: template

View File

@@ -17,8 +17,9 @@ lock:
lambda: |-
if (millis() > 10000) {
return LOCK_STATE_LOCKED;
} else {
return LOCK_STATE_UNLOCKED;
}
return LOCK_STATE_UNLOCKED;
optimistic: true
assumed_state: false
on_unlock:

View File

@@ -72,9 +72,10 @@ binary_sensor:
if (id(template_sens).state > 30) {
// Garage Door is open.
return true;
} else {
// Garage Door is closed.
return false;
}
// Garage Door is closed.
return false;
on_state:
- mqtt.publish:
topic: some/topic/binary_sensor
@@ -216,8 +217,9 @@ cover:
lambda: |-
if (id(some_binary_sensor).state) {
return COVER_OPEN;
} else {
return COVER_CLOSED;
}
return COVER_CLOSED;
open_action:
- logger.log: open_action
close_action:
@@ -319,8 +321,9 @@ lock:
lambda: |-
if (id(some_binary_sensor).state) {
return LOCK_STATE_LOCKED;
} else {
return LOCK_STATE_UNLOCKED;
}
return LOCK_STATE_UNLOCKED;
lock_action:
- logger.log: lock_action
unlock_action:
@@ -357,8 +360,9 @@ sensor:
lambda: |-
if (id(some_binary_sensor).state) {
return 42.0;
} else {
return 0.0;
}
return 0.0;
update_interval: 60s
on_value:
- mqtt.publish:
@@ -386,8 +390,9 @@ switch:
lambda: |-
if (id(some_binary_sensor).state) {
return true;
} else {
return false;
}
return false;
turn_on_action:
- logger.log: turn_on_action
turn_off_action:
@@ -431,8 +436,9 @@ valve:
lambda: |-
if (id(some_binary_sensor).state) {
return VALVE_OPEN;
} else {
return VALVE_CLOSED;
}
return VALVE_CLOSED;
alarm_control_panel:
- platform: template

View File

@@ -27,8 +27,9 @@ sensor:
lambda: |-
if (millis() > 10000) {
return 42.0;
} else {
return 0.0;
}
return 0.0;
update_interval: 60s
climate:

View File

@@ -35,8 +35,9 @@ sensor:
lambda: |-
if (millis() > 10000) {
return 42.0;
} else {
return 0.0;
}
return 0.0;
update_interval: 60s
text_sensor:
@@ -48,8 +49,9 @@ text_sensor:
lambda: |-
if (millis() > 10000) {
return {"Hello World"};
} else {
return {"Goodbye (cruel) World"};
}
return {"Goodbye (cruel) World"};
update_interval: 60s
binary_sensor:
@@ -58,8 +60,9 @@ binary_sensor:
lambda: |-
if (millis() > 10000) {
return true;
} else {
return false;
}
return false;
switch:
- platform: template
@@ -67,8 +70,9 @@ switch:
lambda: |-
if (millis() > 10000) {
return true;
} else {
return false;
}
return false;
optimistic: true
fan:
@@ -81,8 +85,9 @@ cover:
lambda: |-
if (millis() > 10000) {
return COVER_OPEN;
} else {
return COVER_CLOSED;
}
return COVER_CLOSED;
lock:
- platform: template
@@ -90,8 +95,9 @@ lock:
lambda: |-
if (millis() > 10000) {
return LOCK_STATE_LOCKED;
} else {
return LOCK_STATE_UNLOCKED;
}
return LOCK_STATE_UNLOCKED;
optimistic: true
select:

View File

@@ -59,8 +59,9 @@ binary_sensor:
- lambda: |-
if (id(other_binary_sensor).state) {
return x;
} else {
return {};
}
return {};
- settle: 500ms
- timeout: 5s
@@ -71,8 +72,9 @@ sensor:
lambda: |-
if (id(some_binary_sensor).state) {
return 42.0;
} else {
return 0.0;
}
return 0.0;
update_interval: 60s
filters:
- calibrate_linear:
@@ -181,8 +183,9 @@ switch:
lambda: |-
if (id(some_binary_sensor).state) {
return true;
} else {
return false;
}
return false;
turn_on_action:
- logger.log: "turn_on_action"
turn_off_action:
@@ -200,8 +203,9 @@ cover:
lambda: |-
if (id(some_binary_sensor).state) {
return COVER_OPEN;
} else {
return COVER_CLOSED;
}
return COVER_CLOSED;
open_action:
- logger.log: open_action
close_action:
@@ -234,8 +238,9 @@ lock:
lambda: |-
if (id(some_binary_sensor).state) {
return LOCK_STATE_LOCKED;
} else {
return LOCK_STATE_UNLOCKED;
}
return LOCK_STATE_UNLOCKED;
lock_action:
- logger.log: lock_action
unlock_action:
@@ -250,8 +255,9 @@ valve:
lambda: |-
if (id(some_binary_sensor).state) {
return VALVE_OPEN;
} else {
return VALVE_CLOSED;
}
return VALVE_CLOSED;
open_action:
- logger.log: open_action
close_action:

View File

@@ -69,11 +69,6 @@ climate:
- logger.log: swing_vertical_action
swing_both_action:
- logger.log: swing_both_action
humidity_control_humidify_action:
- logger.log: humidity_control_humidify_action
humidity_control_off_action:
- logger.log: humidity_control_off_action
humidity_hysteresis: 1.0
startup_delay: true
supplemental_cooling_delta: 2.0
cool_deadband: 0.5

View File

@@ -17,10 +17,10 @@ sensor:
name: HLW8012 Voltage
power:
name: HLW8012 Power
id: total_daily_energy_hlw8012_power
id: hlw8012_power
energy:
name: HLW8012 Energy
id: total_daily_energy_hlw8012_energy
id: hlw8012_energy
update_interval: 15s
current_resistor: 0.001 ohm
voltage_divider: 2351
@@ -29,4 +29,4 @@ sensor:
model: hlw8012
- platform: total_daily_energy
name: HLW8012 Total Daily Energy
power_id: total_daily_energy_hlw8012_power
power_id: hlw8012_power

View File

@@ -35,26 +35,6 @@ from esphome.zeroconf import DiscoveredImport
from .common import get_fixture_path
def get_build_path(base_path: Path, device_name: str) -> Path:
"""Get the build directory path for a device.
This is a test helper that constructs the standard ESPHome build directory
structure. Note: This helper does NOT perform path traversal sanitization
because it's only used in tests where we control the inputs. The actual
web_server.py code handles sanitization in DownloadBinaryRequestHandler.get()
via file_name.replace("..", "").lstrip("/").
Args:
base_path: The base temporary path (typically tmp_path from pytest)
device_name: The name of the device (should not contain path separators
in production use, but tests may use it for specific scenarios)
Returns:
Path to the build directory (.esphome/build/device_name)
"""
return base_path / ".esphome" / "build" / device_name
class DashboardTestHelper:
def __init__(self, io_loop: IOLoop, client: AsyncHTTPClient, port: int) -> None:
self.io_loop = io_loop
@@ -437,180 +417,6 @@ async def test_download_binary_handler_idedata_fallback(
assert response.body == b"bootloader content"
@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_ext_storage_path")
async def test_download_binary_handler_subdirectory_file(
dashboard: DashboardTestHelper,
tmp_path: Path,
mock_storage_json: MagicMock,
) -> None:
"""Test the DownloadBinaryRequestHandler.get with file in subdirectory (nRF52 case).
This is a regression test for issue #11343 where the Path migration broke
downloads for nRF52 firmware files in subdirectories like 'zephyr/zephyr.uf2'.
The issue was that with_name() doesn't accept path separators:
- Before: path = storage_json.firmware_bin_path.with_name(file_name)
ValueError: Invalid name 'zephyr/zephyr.uf2'
- After: path = storage_json.firmware_bin_path.parent.joinpath(file_name)
Works correctly with subdirectory paths
"""
# Create a fake nRF52 build structure with firmware in subdirectory
build_dir = get_build_path(tmp_path, "nrf52-device")
zephyr_dir = build_dir / "zephyr"
zephyr_dir.mkdir(parents=True)
# Create the main firmware binary (would be in build root)
firmware_file = build_dir / "firmware.bin"
firmware_file.write_bytes(b"main firmware")
# Create the UF2 file in zephyr subdirectory (nRF52 specific)
uf2_file = zephyr_dir / "zephyr.uf2"
uf2_file.write_bytes(b"nRF52 UF2 firmware content")
# Mock storage JSON
mock_storage = Mock()
mock_storage.name = "nrf52-device"
mock_storage.firmware_bin_path = firmware_file
mock_storage_json.load.return_value = mock_storage
# Request the UF2 file with subdirectory path
response = await dashboard.fetch(
"/download.bin?configuration=nrf52-device.yaml&file=zephyr/zephyr.uf2",
method="GET",
)
assert response.code == 200
assert response.body == b"nRF52 UF2 firmware content"
assert response.headers["Content-Type"] == "application/octet-stream"
assert "attachment" in response.headers["Content-Disposition"]
# Download name should be device-name + full file path
assert "nrf52-device-zephyr/zephyr.uf2" in response.headers["Content-Disposition"]
@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_ext_storage_path")
async def test_download_binary_handler_subdirectory_file_url_encoded(
dashboard: DashboardTestHelper,
tmp_path: Path,
mock_storage_json: MagicMock,
) -> None:
"""Test the DownloadBinaryRequestHandler.get with URL-encoded subdirectory path.
Verifies that URL-encoded paths (e.g., zephyr%2Fzephyr.uf2) are correctly
decoded and handled, and that custom download names work with subdirectories.
"""
# Create a fake build structure with firmware in subdirectory
build_dir = get_build_path(tmp_path, "test")
zephyr_dir = build_dir / "zephyr"
zephyr_dir.mkdir(parents=True)
firmware_file = build_dir / "firmware.bin"
firmware_file.write_bytes(b"content")
uf2_file = zephyr_dir / "zephyr.uf2"
uf2_file.write_bytes(b"content")
# Mock storage JSON
mock_storage = Mock()
mock_storage.name = "test_device"
mock_storage.firmware_bin_path = firmware_file
mock_storage_json.load.return_value = mock_storage
# Request with URL-encoded path and custom download name
response = await dashboard.fetch(
"/download.bin?configuration=test.yaml&file=zephyr%2Fzephyr.uf2&download=custom_name.bin",
method="GET",
)
assert response.code == 200
assert "custom_name.bin" in response.headers["Content-Disposition"]
@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_ext_storage_path")
@pytest.mark.parametrize(
"attack_path",
[
pytest.param("../../../secrets.yaml", id="basic_traversal"),
pytest.param("..%2F..%2F..%2Fsecrets.yaml", id="url_encoded"),
pytest.param("zephyr/../../../secrets.yaml", id="traversal_with_prefix"),
pytest.param("/etc/passwd", id="absolute_path"),
pytest.param("//etc/passwd", id="double_slash_absolute"),
pytest.param("....//secrets.yaml", id="multiple_dots"),
],
)
async def test_download_binary_handler_path_traversal_protection(
dashboard: DashboardTestHelper,
tmp_path: Path,
mock_storage_json: MagicMock,
attack_path: str,
) -> None:
"""Test that DownloadBinaryRequestHandler prevents path traversal attacks.
Verifies that attempts to use '..' in file paths are sanitized to prevent
accessing files outside the build directory. Tests multiple attack vectors.
"""
# Create build structure
build_dir = get_build_path(tmp_path, "test")
build_dir.mkdir(parents=True)
firmware_file = build_dir / "firmware.bin"
firmware_file.write_bytes(b"firmware content")
# Create a sensitive file outside the build directory that should NOT be accessible
sensitive_file = tmp_path / "secrets.yaml"
sensitive_file.write_bytes(b"secret: my_secret_password")
# Mock storage JSON
mock_storage = Mock()
mock_storage.name = "test_device"
mock_storage.firmware_bin_path = firmware_file
mock_storage_json.load.return_value = mock_storage
# Attempt path traversal attack - should be blocked
with pytest.raises(HTTPClientError) as exc_info:
await dashboard.fetch(
f"/download.bin?configuration=test.yaml&file={attack_path}",
method="GET",
)
# Should get 404 (file not found after sanitization) or 500 (idedata fails)
assert exc_info.value.code in (404, 500)
@pytest.mark.asyncio
@pytest.mark.usefixtures("mock_ext_storage_path")
async def test_download_binary_handler_multiple_subdirectory_levels(
dashboard: DashboardTestHelper,
tmp_path: Path,
mock_storage_json: MagicMock,
) -> None:
"""Test downloading files from multiple subdirectory levels.
Verifies that joinpath correctly handles multi-level paths like 'build/output/firmware.bin'.
"""
# Create nested directory structure
build_dir = get_build_path(tmp_path, "test")
nested_dir = build_dir / "build" / "output"
nested_dir.mkdir(parents=True)
firmware_file = build_dir / "firmware.bin"
firmware_file.write_bytes(b"main")
nested_file = nested_dir / "firmware.bin"
nested_file.write_bytes(b"nested firmware content")
# Mock storage JSON
mock_storage = Mock()
mock_storage.name = "test_device"
mock_storage.firmware_bin_path = firmware_file
mock_storage_json.load.return_value = mock_storage
response = await dashboard.fetch(
"/download.bin?configuration=test.yaml&file=build/output/firmware.bin",
method="GET",
)
assert response.code == 200
assert response.body == b"nested firmware content"
@pytest.mark.asyncio
async def test_edit_request_handler_post_invalid_file(
dashboard: DashboardTestHelper,

View File

@@ -34,9 +34,10 @@ binary_sensor:
ESP_LOGD("test", "Button ON at %u", now);
}
return true;
} else {
// Only log state change
if (id(ir_remote_button).state) {
ESP_LOGD("test", "Button OFF at %u", now);
}
return false;
}
// Only log state change
if (id(ir_remote_button).state) {
ESP_LOGD("test", "Button OFF at %u", now);
}
return false;

View File

@@ -8,7 +8,6 @@ import asyncio
from typing import Any
from aioesphomeapi import LightState
from aioesphomeapi.model import ColorMode
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -36,51 +35,10 @@ async def test_light_calls(
# Get the light entities
entities = await client.list_entities_services()
lights = [e for e in entities[0] if e.object_id.startswith("test_")]
assert len(lights) >= 3 # Should have RGBCW, RGB, and Binary lights
assert len(lights) >= 2 # Should have RGBCW and RGB lights
rgbcw_light = next(light for light in lights if "RGBCW" in light.name)
rgb_light = next(light for light in lights if "RGB Light" in light.name)
binary_light = next(light for light in lights if "Binary" in light.name)
# Test color mode encoding: Verify supported_color_modes contains actual ColorMode enum values
# not bit positions. This is critical - the iterator must convert bit positions to actual
# ColorMode enum values for API encoding.
# RGBCW light (rgbww platform) should support RGB_COLD_WARM_WHITE mode
assert ColorMode.RGB_COLD_WARM_WHITE in rgbcw_light.supported_color_modes, (
f"RGBCW light missing RGB_COLD_WARM_WHITE mode. Got: {rgbcw_light.supported_color_modes}"
)
# Verify it's the actual enum value, not bit position
assert ColorMode.RGB_COLD_WARM_WHITE.value in [
mode.value for mode in rgbcw_light.supported_color_modes
], (
f"RGBCW light has wrong color mode values. Expected {ColorMode.RGB_COLD_WARM_WHITE.value} "
f"(RGB_COLD_WARM_WHITE), got: {[mode.value for mode in rgbcw_light.supported_color_modes]}"
)
# RGB light should support RGB mode
assert ColorMode.RGB in rgb_light.supported_color_modes, (
f"RGB light missing RGB color mode. Got: {rgb_light.supported_color_modes}"
)
# Verify it's the actual enum value, not bit position
assert ColorMode.RGB.value in [
mode.value for mode in rgb_light.supported_color_modes
], (
f"RGB light has wrong color mode values. Expected {ColorMode.RGB.value} (RGB), got: "
f"{[mode.value for mode in rgb_light.supported_color_modes]}"
)
# Binary light (on/off only) should support ON_OFF mode
assert ColorMode.ON_OFF in binary_light.supported_color_modes, (
f"Binary light missing ON_OFF color mode. Got: {binary_light.supported_color_modes}"
)
# Verify it's the actual enum value, not bit position
assert ColorMode.ON_OFF.value in [
mode.value for mode in binary_light.supported_color_modes
], (
f"Binary light has wrong color mode values. Expected {ColorMode.ON_OFF.value} (ON_OFF), got: "
f"{[mode.value for mode in binary_light.supported_color_modes]}"
)
async def wait_for_state_change(key: int, timeout: float = 1.0) -> Any:
"""Wait for a state change for the given entity key."""

View File

@@ -321,14 +321,12 @@ def test_choose_upload_log_host_with_serial_device_no_ports(
) -> None:
"""Test SERIAL device when no serial ports are found."""
setup_core()
with pytest.raises(
EsphomeError, match="All specified devices .* could not be resolved"
):
choose_upload_log_host(
default="SERIAL",
check_default=None,
purpose=Purpose.UPLOADING,
)
result = choose_upload_log_host(
default="SERIAL",
check_default=None,
purpose=Purpose.UPLOADING,
)
assert result == []
assert "No serial ports found, skipping SERIAL device" in caplog.text
@@ -369,14 +367,12 @@ def test_choose_upload_log_host_with_ota_device_with_api_config() -> None:
"""Test OTA device when API is configured (no upload without OTA in config)."""
setup_core(config={CONF_API: {}}, address="192.168.1.100")
with pytest.raises(
EsphomeError, match="All specified devices .* could not be resolved"
):
choose_upload_log_host(
default="OTA",
check_default=None,
purpose=Purpose.UPLOADING,
)
result = choose_upload_log_host(
default="OTA",
check_default=None,
purpose=Purpose.UPLOADING,
)
assert result == []
def test_choose_upload_log_host_with_ota_device_with_api_config_logging() -> None:
@@ -409,14 +405,12 @@ def test_choose_upload_log_host_with_ota_device_no_fallback() -> None:
"""Test OTA device with no valid fallback options."""
setup_core()
with pytest.raises(
EsphomeError, match="All specified devices .* could not be resolved"
):
choose_upload_log_host(
default="OTA",
check_default=None,
purpose=Purpose.UPLOADING,
)
result = choose_upload_log_host(
default="OTA",
check_default=None,
purpose=Purpose.UPLOADING,
)
assert result == []
@pytest.mark.usefixtures("mock_choose_prompt")
@@ -621,19 +615,21 @@ def test_choose_upload_log_host_empty_defaults_list() -> None:
@pytest.mark.usefixtures("mock_no_serial_ports", "mock_no_mqtt_logging")
def test_choose_upload_log_host_all_devices_unresolved() -> None:
def test_choose_upload_log_host_all_devices_unresolved(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test when all specified devices cannot be resolved."""
setup_core()
with pytest.raises(
EsphomeError,
match=r"All specified devices \['SERIAL', 'OTA'\] could not be resolved",
):
choose_upload_log_host(
default=["SERIAL", "OTA"],
check_default=None,
purpose=Purpose.UPLOADING,
)
result = choose_upload_log_host(
default=["SERIAL", "OTA"],
check_default=None,
purpose=Purpose.UPLOADING,
)
assert result == []
assert (
"All specified devices: ['SERIAL', 'OTA'] could not be resolved." in caplog.text
)
@pytest.mark.usefixtures("mock_no_serial_ports", "mock_no_mqtt_logging")
@@ -766,14 +762,12 @@ def test_choose_upload_log_host_no_address_with_ota_config() -> None:
"""Test OTA device when OTA is configured but no address is set."""
setup_core(config={CONF_OTA: {}})
with pytest.raises(
EsphomeError, match="All specified devices .* could not be resolved"
):
choose_upload_log_host(
default="OTA",
check_default=None,
purpose=Purpose.UPLOADING,
)
result = choose_upload_log_host(
default="OTA",
check_default=None,
purpose=Purpose.UPLOADING,
)
assert result == []
@dataclass