mirror of
https://github.com/esphome/esphome.git
synced 2026-01-17 15:34:50 -07:00
Compare commits
4 Commits
string_ref
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4fb6988ff | ||
|
|
d31b733dce | ||
|
|
b25a2f8d8e | ||
|
|
3f892711c7 |
@@ -69,7 +69,6 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
JsonObjectConst,
|
||||
Parented,
|
||||
PollingComponent,
|
||||
StringRef,
|
||||
arduino_json_ns,
|
||||
bool_,
|
||||
const_char_ptr,
|
||||
|
||||
@@ -77,7 +77,7 @@ FanSpeedSetTrigger = fan_ns.class_(
|
||||
"FanSpeedSetTrigger", automation.Trigger.template(cg.int_)
|
||||
)
|
||||
FanPresetSetTrigger = fan_ns.class_(
|
||||
"FanPresetSetTrigger", automation.Trigger.template(cg.StringRef)
|
||||
"FanPresetSetTrigger", automation.Trigger.template(cg.std_string)
|
||||
)
|
||||
|
||||
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
|
||||
@@ -287,7 +287,7 @@ async def setup_fan_core_(var, config):
|
||||
await automation.build_automation(trigger, [(cg.int_, "x")], conf)
|
||||
for conf in config.get(CONF_ON_PRESET_SET, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.StringRef, "x")], conf)
|
||||
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
|
||||
|
||||
|
||||
async def register_fan(var, config):
|
||||
|
||||
@@ -208,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger<int> {
|
||||
int last_speed_;
|
||||
};
|
||||
|
||||
class FanPresetSetTrigger : public Trigger<StringRef> {
|
||||
class FanPresetSetTrigger : public Trigger<std::string> {
|
||||
public:
|
||||
FanPresetSetTrigger(Fan *state) {
|
||||
state->add_on_state_callback([this, state]() {
|
||||
@@ -216,7 +216,7 @@ class FanPresetSetTrigger : public Trigger<StringRef> {
|
||||
auto should_trigger = preset_mode != this->last_preset_mode_;
|
||||
this->last_preset_mode_ = preset_mode;
|
||||
if (should_trigger) {
|
||||
this->trigger(preset_mode);
|
||||
this->trigger(std::string(preset_mode));
|
||||
}
|
||||
});
|
||||
this->last_preset_mode_ = state->get_preset_mode();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "light_json_schema.h"
|
||||
#include "color_mode.h"
|
||||
#include "light_output.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
@@ -8,29 +9,32 @@ namespace esphome::light {
|
||||
|
||||
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
|
||||
|
||||
// Get JSON string for color mode using linear search (avoids large switch jump table)
|
||||
static const char *get_color_mode_json_str(ColorMode mode) {
|
||||
// Parallel arrays: mode values and their corresponding strings
|
||||
// Uses less RAM than a switch jump table on sparse enum values
|
||||
static constexpr ColorMode MODES[] = {
|
||||
ColorMode::ON_OFF,
|
||||
ColorMode::BRIGHTNESS,
|
||||
ColorMode::WHITE,
|
||||
ColorMode::COLOR_TEMPERATURE,
|
||||
ColorMode::COLD_WARM_WHITE,
|
||||
ColorMode::RGB,
|
||||
ColorMode::RGB_WHITE,
|
||||
ColorMode::RGB_COLOR_TEMPERATURE,
|
||||
ColorMode::RGB_COLD_WARM_WHITE,
|
||||
};
|
||||
static constexpr const char *STRINGS[] = {
|
||||
"onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", "rgbww",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(MODES) / sizeof(MODES[0]); i++) {
|
||||
if (MODES[i] == mode)
|
||||
return STRINGS[i];
|
||||
// Get JSON string for color mode.
|
||||
// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would
|
||||
// generate a large jump table. Converting to bit index (0-9) allows a compact switch.
|
||||
static ProgmemStr get_color_mode_json_str(ColorMode mode) {
|
||||
switch (ColorModeBitPolicy::to_bit(mode)) {
|
||||
case 1:
|
||||
return ESPHOME_F("onoff");
|
||||
case 2:
|
||||
return ESPHOME_F("brightness");
|
||||
case 3:
|
||||
return ESPHOME_F("white");
|
||||
case 4:
|
||||
return ESPHOME_F("color_temp");
|
||||
case 5:
|
||||
return ESPHOME_F("cwww");
|
||||
case 6:
|
||||
return ESPHOME_F("rgb");
|
||||
case 7:
|
||||
return ESPHOME_F("rgbw");
|
||||
case 8:
|
||||
return ESPHOME_F("rgbct");
|
||||
case 9:
|
||||
return ESPHOME_F("rgbww");
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
@@ -44,7 +48,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
auto values = state.remote_values;
|
||||
|
||||
const auto color_mode = values.get_color_mode();
|
||||
const char *mode_str = get_color_mode_json_str(color_mode);
|
||||
const auto *mode_str = get_color_mode_json_str(color_mode);
|
||||
if (mode_str != nullptr) {
|
||||
root[ESPHOME_F("color_mode")] = mode_str;
|
||||
}
|
||||
|
||||
@@ -561,8 +561,9 @@ const char *OpenTherm::message_id_to_str(MessageId id) {
|
||||
}
|
||||
|
||||
void OpenTherm::debug_data(OpenthermData &data) {
|
||||
ESP_LOGD(TAG, "%s %s %s %s", format_bin(data.type).c_str(), format_bin(data.id).c_str(),
|
||||
format_bin(data.valueHB).c_str(), format_bin(data.valueLB).c_str());
|
||||
char type_buf[9], id_buf[9], hb_buf[9], lb_buf[9];
|
||||
ESP_LOGD(TAG, "%s %s %s %s", format_bin_to(type_buf, data.type), format_bin_to(id_buf, data.id),
|
||||
format_bin_to(hb_buf, data.valueHB), format_bin_to(lb_buf, data.valueLB));
|
||||
ESP_LOGD(TAG, "type: %s; id: %u; HB: %u; LB: %u; uint_16: %u; float: %f",
|
||||
this->message_type_to_str((MessageType) data.type), data.id, data.valueHB, data.valueLB, data.u16(),
|
||||
data.f88());
|
||||
|
||||
@@ -33,7 +33,7 @@ SelectPtr = Select.operator("ptr")
|
||||
# Triggers
|
||||
SelectStateTrigger = select_ns.class_(
|
||||
"SelectStateTrigger",
|
||||
automation.Trigger.template(cg.StringRef, cg.size_t),
|
||||
automation.Trigger.template(cg.std_string, cg.size_t),
|
||||
)
|
||||
|
||||
# Actions
|
||||
@@ -100,7 +100,7 @@ async def setup_select_core_(var, config, *, options: list[str]):
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.StringRef, "x"), (cg.size_t, "i")], conf
|
||||
trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf
|
||||
)
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
namespace esphome::select {
|
||||
|
||||
class SelectStateTrigger : public Trigger<StringRef, size_t> {
|
||||
class SelectStateTrigger : public Trigger<std::string, size_t> {
|
||||
public:
|
||||
explicit SelectStateTrigger(Select *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback(
|
||||
[this](size_t index) { this->trigger(StringRef(this->parent_->option_at(index)), index); });
|
||||
[this](size_t index) { this->trigger(std::string(this->parent_->option_at(index)), index); });
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -143,7 +143,7 @@ bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) {
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
bool ListEntitiesIterator::on_infrared(infrared::Infrared *obj) {
|
||||
// Infrared web_server support not yet implemented - this stub acknowledges the entity
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::infrared_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER_LOCAL
|
||||
#if USE_WEBSERVER_VERSION == 2
|
||||
#include "server_index_v2.h"
|
||||
@@ -1952,6 +1956,110 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (infrared::Infrared *obj : App.get_infrareds()) {
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->infrared_json_(obj, detail);
|
||||
request->send(200, ESPHOME_F("application/json"), data.c_str());
|
||||
return;
|
||||
}
|
||||
if (!match.method_equals(ESPHOME_F("transmit"))) {
|
||||
request->send(404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow transmit if the device supports it
|
||||
if (!obj->has_transmitter()) {
|
||||
request->send(400, ESPHOME_F("text/plain"), "Device does not support transmission");
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse parameters
|
||||
auto call = obj->make_call();
|
||||
|
||||
// Parse carrier frequency (optional)
|
||||
if (request->hasParam(ESPHOME_F("carrier_frequency"))) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str());
|
||||
if (value.has_value()) {
|
||||
call.set_carrier_frequency(*value);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse repeat count (optional, defaults to 1)
|
||||
if (request->hasParam(ESPHOME_F("repeat_count"))) {
|
||||
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("repeat_count"))->value().c_str());
|
||||
if (value.has_value()) {
|
||||
call.set_repeat_count(*value);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse base64url-encoded raw timings (required)
|
||||
// Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
|
||||
if (!request->hasParam(ESPHOME_F("data"))) {
|
||||
request->send(400, ESPHOME_F("text/plain"), "Missing 'data' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
|
||||
std::string encoded =
|
||||
request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
|
||||
// Validate base64url is not empty
|
||||
if (encoded.empty()) {
|
||||
request->send(400, ESPHOME_F("text/plain"), "Empty 'data' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266 is single-threaded, call directly
|
||||
call.set_raw_timings_base64url(encoded);
|
||||
call.perform();
|
||||
#else
|
||||
// Defer to main loop for thread safety. Move encoded string into lambda to ensure
|
||||
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
|
||||
// must remain valid until perform() completes.
|
||||
this->defer([call, encoded = std::move(encoded)]() mutable {
|
||||
call.set_raw_timings_base64url(encoded);
|
||||
call.perform();
|
||||
});
|
||||
#endif
|
||||
|
||||
request->send(200);
|
||||
return;
|
||||
}
|
||||
request->send(404);
|
||||
}
|
||||
|
||||
std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL);
|
||||
}
|
||||
|
||||
std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_icon_state_value(root, obj, "infrared", "", 0, start_config);
|
||||
|
||||
auto traits = obj->get_traits();
|
||||
|
||||
root[ESPHOME_F("supports_transmitter")] = traits.get_supports_transmitter();
|
||||
root[ESPHOME_F("supports_receiver")] = traits.get_supports_receiver();
|
||||
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
|
||||
return builder.serialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void WebServer::on_event(event::Event *obj) {
|
||||
if (!this->include_internal_ && obj->is_internal())
|
||||
@@ -2083,24 +2191,21 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
||||
const auto &url = request->url();
|
||||
const auto method = request->method();
|
||||
|
||||
// Static URL checks
|
||||
static const char *const STATIC_URLS[] = {
|
||||
"/",
|
||||
// Static URL checks - use ESPHOME_F to keep strings in flash on ESP8266
|
||||
if (url == ESPHOME_F("/"))
|
||||
return true;
|
||||
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
||||
"/events",
|
||||
if (url == ESPHOME_F("/events"))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||
"/0.css",
|
||||
if (url == ESPHOME_F("/0.css"))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||
"/0.js",
|
||||
if (url == ESPHOME_F("/0.js"))
|
||||
return true;
|
||||
#endif
|
||||
};
|
||||
|
||||
for (const auto &static_url : STATIC_URLS) {
|
||||
if (url == static_url)
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
|
||||
if (method == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network")))
|
||||
@@ -2120,90 +2225,100 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
||||
if (!is_get_or_post)
|
||||
return false;
|
||||
|
||||
// Use lookup tables for domain checks
|
||||
static const char *const GET_ONLY_DOMAINS[] = {
|
||||
// Check GET-only domains - use ESPHOME_F to keep strings in flash on ESP8266
|
||||
if (is_get) {
|
||||
#ifdef USE_SENSOR
|
||||
"sensor",
|
||||
if (match.domain_equals(ESPHOME_F("sensor")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
"binary_sensor",
|
||||
if (match.domain_equals(ESPHOME_F("binary_sensor")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
"text_sensor",
|
||||
if (match.domain_equals(ESPHOME_F("text_sensor")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
"event",
|
||||
if (match.domain_equals(ESPHOME_F("event")))
|
||||
return true;
|
||||
#endif
|
||||
};
|
||||
|
||||
static const char *const GET_POST_DOMAINS[] = {
|
||||
#ifdef USE_SWITCH
|
||||
"switch",
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
"button",
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
"fan",
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
"light",
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
"cover",
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
"number",
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
"date",
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
"time",
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
"datetime",
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
"text",
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
"select",
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
"climate",
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
"lock",
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
"valve",
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
"alarm_control_panel",
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
"update",
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
"water_heater",
|
||||
#endif
|
||||
};
|
||||
|
||||
// Check GET-only domains
|
||||
if (is_get) {
|
||||
for (const auto &domain : GET_ONLY_DOMAINS) {
|
||||
if (match.domain_equals(domain))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check GET+POST domains
|
||||
if (is_get_or_post) {
|
||||
for (const auto &domain : GET_POST_DOMAINS) {
|
||||
if (match.domain_equals(domain))
|
||||
return true;
|
||||
}
|
||||
#ifdef USE_SWITCH
|
||||
if (match.domain_equals(ESPHOME_F("switch")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
if (match.domain_equals(ESPHOME_F("button")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
if (match.domain_equals(ESPHOME_F("fan")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
if (match.domain_equals(ESPHOME_F("light")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
if (match.domain_equals(ESPHOME_F("cover")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
if (match.domain_equals(ESPHOME_F("number")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
if (match.domain_equals(ESPHOME_F("date")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
if (match.domain_equals(ESPHOME_F("time")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
if (match.domain_equals(ESPHOME_F("datetime")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
if (match.domain_equals(ESPHOME_F("text")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (match.domain_equals(ESPHOME_F("select")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
if (match.domain_equals(ESPHOME_F("climate")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
if (match.domain_equals(ESPHOME_F("lock")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
if (match.domain_equals(ESPHOME_F("valve")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
if (match.domain_equals(ESPHOME_F("alarm_control_panel")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
if (match.domain_equals(ESPHOME_F("update")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
if (match.domain_equals(ESPHOME_F("water_heater")))
|
||||
return true;
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
if (match.domain_equals(ESPHOME_F("infrared")))
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -2352,6 +2467,11 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
||||
else if (match.domain_equals(ESPHOME_F("water_heater"))) {
|
||||
this->handle_water_heater_request(request, match);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
else if (match.domain_equals(ESPHOME_F("infrared"))) {
|
||||
this->handle_infrared_request(request, match);
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
// No matching handler found - send 404
|
||||
|
||||
@@ -460,6 +460,13 @@ class WebServer : public Controller,
|
||||
static std::string water_heater_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
/// Handle an infrared request under '/infrared/<id>/transmit'.
|
||||
void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match);
|
||||
|
||||
static std::string infrared_all_json_generator(WebServer *web_server, void *source);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void on_event(event::Event *obj) override;
|
||||
|
||||
@@ -662,6 +669,9 @@ class WebServer : public Controller,
|
||||
#ifdef USE_WATER_HEATER
|
||||
std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config);
|
||||
#endif
|
||||
|
||||
@@ -404,15 +404,31 @@ std::string format_hex_pretty(const std::string &data, char separator, bool show
|
||||
return format_hex_pretty_uint8(reinterpret_cast<const uint8_t *>(data.data()), data.length(), separator, show_length);
|
||||
}
|
||||
|
||||
char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
|
||||
if (buffer_size == 0) {
|
||||
return buffer;
|
||||
}
|
||||
// Calculate max bytes we can format: each byte needs 8 chars
|
||||
size_t max_bytes = (buffer_size - 1) / 8;
|
||||
if (max_bytes == 0 || length == 0) {
|
||||
buffer[0] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
size_t bytes_to_format = std::min(length, max_bytes);
|
||||
|
||||
for (size_t byte_idx = 0; byte_idx < bytes_to_format; byte_idx++) {
|
||||
for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
|
||||
buffer[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
|
||||
}
|
||||
}
|
||||
buffer[bytes_to_format * 8] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string format_bin(const uint8_t *data, size_t length) {
|
||||
std::string result;
|
||||
result.resize(length * 8);
|
||||
for (size_t byte_idx = 0; byte_idx < length; byte_idx++) {
|
||||
for (size_t bit_idx = 0; bit_idx < 8; bit_idx++) {
|
||||
result[byte_idx * 8 + bit_idx] = ((data[byte_idx] >> (7 - bit_idx)) & 1) + '0';
|
||||
}
|
||||
}
|
||||
|
||||
format_bin_to(&result[0], length * 8 + 1, data, length);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1096,9 +1096,66 @@ std::string format_hex_pretty(T val, char separator = '.', bool show_length = tr
|
||||
return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T), separator, show_length);
|
||||
}
|
||||
|
||||
/// Calculate buffer size needed for format_bin_to: "01234567...\0" = bytes * 8 + 1
|
||||
constexpr size_t format_bin_size(size_t byte_count) { return byte_count * 8 + 1; }
|
||||
|
||||
/** Format byte array as binary string to buffer.
|
||||
*
|
||||
* Each byte is formatted as 8 binary digits (MSB first).
|
||||
* Truncates output if data exceeds buffer capacity.
|
||||
*
|
||||
* @param buffer Output buffer to write to.
|
||||
* @param buffer_size Size of the output buffer.
|
||||
* @param data Pointer to the byte array to format.
|
||||
* @param length Number of bytes in the array.
|
||||
* @return Pointer to buffer.
|
||||
*
|
||||
* Buffer size needed: length * 8 + 1 (use format_bin_size()).
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* char buf[9]; // format_bin_size(1)
|
||||
* format_bin_to(buf, sizeof(buf), data, 1); // "10101011"
|
||||
* @endcode
|
||||
*/
|
||||
char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length);
|
||||
|
||||
/// Format byte array as binary to buffer. Automatically deduces buffer size.
|
||||
template<size_t N> inline char *format_bin_to(char (&buffer)[N], const uint8_t *data, size_t length) {
|
||||
static_assert(N >= 9, "Buffer must hold at least one binary byte (9 chars)");
|
||||
return format_bin_to(buffer, N, data, length);
|
||||
}
|
||||
|
||||
/** Format an unsigned integer in binary to buffer, MSB first.
|
||||
*
|
||||
* @tparam N Buffer size (must be >= sizeof(T) * 8 + 1).
|
||||
* @tparam T Unsigned integer type.
|
||||
* @param buffer Output buffer to write to.
|
||||
* @param val The unsigned integer value to format.
|
||||
* @return Pointer to buffer.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* char buf[9]; // format_bin_size(sizeof(uint8_t))
|
||||
* format_bin_to(buf, uint8_t{0xAA}); // "10101010"
|
||||
* char buf16[17]; // format_bin_size(sizeof(uint16_t))
|
||||
* format_bin_to(buf16, uint16_t{0x1234}); // "0001001000110100"
|
||||
* @endcode
|
||||
*/
|
||||
template<size_t N, typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
|
||||
inline char *format_bin_to(char (&buffer)[N], T val) {
|
||||
static_assert(N >= sizeof(T) * 8 + 1, "Buffer too small for type");
|
||||
val = convert_big_endian(val);
|
||||
return format_bin_to(buffer, reinterpret_cast<const uint8_t *>(&val), sizeof(T));
|
||||
}
|
||||
|
||||
/// Format the byte array \p data of length \p len in binary.
|
||||
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
std::string format_bin(const uint8_t *data, size_t length);
|
||||
/// Format an unsigned integer in binary, starting with the most significant byte.
|
||||
/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_bin(T val) {
|
||||
val = convert_big_endian(val);
|
||||
return format_bin(reinterpret_cast<uint8_t *>(&val), sizeof(T));
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#define ESPHOME_strncpy_P strncpy_P
|
||||
#define ESPHOME_strncat_P strncat_P
|
||||
#define ESPHOME_snprintf_P snprintf_P
|
||||
// Type for pointers to PROGMEM strings (for use with ESPHOME_F return values)
|
||||
using ProgmemStr = const __FlashStringHelper *;
|
||||
#else
|
||||
#define ESPHOME_F(string_literal) (string_literal)
|
||||
#define ESPHOME_PGM_P const char *
|
||||
@@ -19,4 +21,6 @@
|
||||
#define ESPHOME_strncpy_P strncpy
|
||||
#define ESPHOME_strncat_P strncat
|
||||
#define ESPHOME_snprintf_P snprintf
|
||||
// Type for pointers to strings (no PROGMEM on non-ESP8266 platforms)
|
||||
using ProgmemStr = const char *;
|
||||
#endif
|
||||
|
||||
@@ -72,7 +72,6 @@ class StringRef {
|
||||
|
||||
constexpr const char *c_str() const { return base_; }
|
||||
constexpr size_type size() const { return len_; }
|
||||
constexpr size_type length() const { return len_; }
|
||||
constexpr bool empty() const { return len_ == 0; }
|
||||
constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); }
|
||||
|
||||
@@ -81,32 +80,6 @@ class StringRef {
|
||||
|
||||
operator std::string() const { return str(); }
|
||||
|
||||
/// Find first occurrence of substring, returns std::string::npos if not found.
|
||||
/// Note: Requires the underlying string to be null-terminated.
|
||||
size_type find(const char *s, size_type pos = 0) const {
|
||||
if (pos >= len_)
|
||||
return std::string::npos;
|
||||
const char *result = std::strstr(base_ + pos, s);
|
||||
// Verify entire match is within bounds (strstr searches to null terminator)
|
||||
if (result && result + std::strlen(s) <= base_ + len_)
|
||||
return static_cast<size_type>(result - base_);
|
||||
return std::string::npos;
|
||||
}
|
||||
size_type find(char c, size_type pos = 0) const {
|
||||
if (pos >= len_)
|
||||
return std::string::npos;
|
||||
const void *result = std::memchr(base_ + pos, static_cast<unsigned char>(c), len_ - pos);
|
||||
return result ? static_cast<size_type>(static_cast<const char *>(result) - base_) : std::string::npos;
|
||||
}
|
||||
|
||||
/// Return substring as std::string
|
||||
std::string substr(size_type pos = 0, size_type count = std::string::npos) const {
|
||||
if (pos >= len_)
|
||||
return std::string();
|
||||
size_type actual_count = (count == std::string::npos || pos + count > len_) ? len_ - pos : count;
|
||||
return std::string(base_ + pos, actual_count);
|
||||
}
|
||||
|
||||
private:
|
||||
const char *base_;
|
||||
size_type len_;
|
||||
@@ -187,43 +160,6 @@ inline std::string operator+(const std::string &lhs, const StringRef &rhs) {
|
||||
str.append(rhs.c_str(), rhs.size());
|
||||
return str;
|
||||
}
|
||||
// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef)
|
||||
// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation.
|
||||
namespace internal {
|
||||
// NOLINTBEGIN(google-runtime-int)
|
||||
template<typename R, typename F> inline R parse_number(const StringRef &str, size_t *pos, F conv) {
|
||||
char *end;
|
||||
R result = conv(str.c_str(), &end);
|
||||
// Set pos to 0 on conversion failure (when no characters consumed), otherwise index after number
|
||||
if (pos)
|
||||
*pos = (end == str.c_str()) ? 0 : static_cast<size_t>(end - str.c_str());
|
||||
return result;
|
||||
}
|
||||
template<typename R, typename F> inline R parse_number(const StringRef &str, size_t *pos, int base, F conv) {
|
||||
char *end;
|
||||
R result = conv(str.c_str(), &end, base);
|
||||
// Set pos to 0 on conversion failure (when no characters consumed), otherwise index after number
|
||||
if (pos)
|
||||
*pos = (end == str.c_str()) ? 0 : static_cast<size_t>(end - str.c_str());
|
||||
return result;
|
||||
}
|
||||
// NOLINTEND(google-runtime-int)
|
||||
} // namespace internal
|
||||
// NOLINTBEGIN(readability-identifier-naming,google-runtime-int)
|
||||
inline int stoi(const StringRef &str, size_t *pos = nullptr, int base = 10) {
|
||||
return static_cast<int>(internal::parse_number<long>(str, pos, base, std::strtol));
|
||||
}
|
||||
inline long stol(const StringRef &str, size_t *pos = nullptr, int base = 10) {
|
||||
return internal::parse_number<long>(str, pos, base, std::strtol);
|
||||
}
|
||||
inline float stof(const StringRef &str, size_t *pos = nullptr) {
|
||||
return internal::parse_number<float>(str, pos, std::strtof);
|
||||
}
|
||||
inline double stod(const StringRef &str, size_t *pos = nullptr) {
|
||||
return internal::parse_number<double>(str, pos, std::strtod);
|
||||
}
|
||||
// NOLINTEND(readability-identifier-naming,google-runtime-int)
|
||||
|
||||
#ifdef USE_JSON
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); }
|
||||
|
||||
@@ -44,4 +44,3 @@ gpio_Flags = gpio_ns.enum("Flags", is_class=True)
|
||||
EntityCategory = esphome_ns.enum("EntityCategory")
|
||||
Parented = esphome_ns.class_("Parented")
|
||||
ESPTime = esphome_ns.struct("ESPTime")
|
||||
StringRef = esphome_ns.class_("StringRef")
|
||||
|
||||
@@ -682,6 +682,7 @@ def lint_trailing_whitespace(fname, match):
|
||||
# Heap-allocating helpers that cause fragmentation on long-running embedded devices.
|
||||
# These return std::string and should be replaced with stack-based alternatives.
|
||||
HEAP_ALLOCATING_HELPERS = {
|
||||
"format_bin": "format_bin_to() with a stack buffer",
|
||||
"format_hex": "format_hex_to() with a stack buffer",
|
||||
"format_hex_pretty": "format_hex_pretty_to() with a stack buffer",
|
||||
"format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer",
|
||||
@@ -699,6 +700,7 @@ HEAP_ALLOCATING_HELPERS = {
|
||||
# get_mac_address(?!_) ensures we don't match get_mac_address_into_buffer, etc.
|
||||
# CPP_RE_EOL captures rest of line so NOLINT comments are detected
|
||||
r"[^\w]("
|
||||
r"format_bin(?!_)|"
|
||||
r"format_hex(?!_)|"
|
||||
r"format_hex_pretty(?!_)|"
|
||||
r"format_mac_address_pretty|"
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
esphome:
|
||||
name: select-stringref-test
|
||||
friendly_name: Select StringRef Test
|
||||
|
||||
host:
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
|
||||
select:
|
||||
- platform: template
|
||||
name: "Test Select"
|
||||
id: test_select
|
||||
optimistic: true
|
||||
options:
|
||||
- "Option A"
|
||||
- "Option B"
|
||||
- "Option C"
|
||||
initial_option: "Option A"
|
||||
on_value:
|
||||
then:
|
||||
# Test 1: Log the value directly (StringRef -> const char* via c_str())
|
||||
- logger.log:
|
||||
format: "Select value: %s"
|
||||
args: ['x.c_str()']
|
||||
# Test 2: String concatenation (StringRef + const char* -> std::string)
|
||||
- lambda: |-
|
||||
std::string with_suffix = x + " selected";
|
||||
ESP_LOGI("test", "Concatenated: %s", with_suffix.c_str());
|
||||
# Test 3: Comparison (StringRef == const char*)
|
||||
- lambda: |-
|
||||
if (x == "Option B") {
|
||||
ESP_LOGI("test", "Option B was selected");
|
||||
}
|
||||
# Test 4: Use index parameter (variable name is 'i')
|
||||
- lambda: |-
|
||||
ESP_LOGI("test", "Select index: %d", (int)i);
|
||||
# Test 5: StringRef.length() method
|
||||
- lambda: |-
|
||||
ESP_LOGI("test", "Length: %d", (int)x.length());
|
||||
# Test 6: StringRef.find() method with substring
|
||||
- lambda: |-
|
||||
if (x.find("Option") != std::string::npos) {
|
||||
ESP_LOGI("test", "Found 'Option' in value");
|
||||
}
|
||||
# Test 7: StringRef.find() method with character
|
||||
- lambda: |-
|
||||
size_t space_pos = x.find(' ');
|
||||
if (space_pos != std::string::npos) {
|
||||
ESP_LOGI("test", "Space at position: %d", (int)space_pos);
|
||||
}
|
||||
# Test 8: StringRef.substr() method
|
||||
- lambda: |-
|
||||
std::string prefix = x.substr(0, 6);
|
||||
ESP_LOGI("test", "Substr prefix: %s", prefix.c_str());
|
||||
|
||||
# Second select with numeric options to test ADL functions
|
||||
- platform: template
|
||||
name: "Baud Rate"
|
||||
id: baud_select
|
||||
optimistic: true
|
||||
options:
|
||||
- "9600"
|
||||
- "115200"
|
||||
initial_option: "9600"
|
||||
on_value:
|
||||
then:
|
||||
# Test 9: stoi via ADL
|
||||
- lambda: |-
|
||||
int baud = stoi(x);
|
||||
ESP_LOGI("test", "stoi result: %d", baud);
|
||||
# Test 10: stol via ADL
|
||||
- lambda: |-
|
||||
long baud_long = stol(x);
|
||||
ESP_LOGI("test", "stol result: %ld", baud_long);
|
||||
# Test 11: stof via ADL
|
||||
- lambda: |-
|
||||
float baud_float = stof(x);
|
||||
ESP_LOGI("test", "stof result: %.0f", baud_float);
|
||||
# Test 12: stod via ADL
|
||||
- lambda: |-
|
||||
double baud_double = stod(x);
|
||||
ESP_LOGI("test", "stod result: %.0f", baud_double);
|
||||
@@ -1,143 +0,0 @@
|
||||
"""Integration test for select on_value trigger with StringRef parameter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_select_stringref_trigger(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test select on_value trigger passes StringRef that works with string operations."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track log messages to verify StringRef operations work
|
||||
value_logged_future = loop.create_future()
|
||||
concatenated_future = loop.create_future()
|
||||
comparison_future = loop.create_future()
|
||||
index_logged_future = loop.create_future()
|
||||
length_future = loop.create_future()
|
||||
find_substr_future = loop.create_future()
|
||||
find_char_future = loop.create_future()
|
||||
substr_future = loop.create_future()
|
||||
# ADL functions
|
||||
stoi_future = loop.create_future()
|
||||
stol_future = loop.create_future()
|
||||
stof_future = loop.create_future()
|
||||
stod_future = loop.create_future()
|
||||
|
||||
# Patterns to match in logs
|
||||
value_pattern = re.compile(r"Select value: Option B")
|
||||
concatenated_pattern = re.compile(r"Concatenated: Option B selected")
|
||||
comparison_pattern = re.compile(r"Option B was selected")
|
||||
index_pattern = re.compile(r"Select index: 1")
|
||||
length_pattern = re.compile(r"Length: 8") # "Option B" is 8 chars
|
||||
find_substr_pattern = re.compile(r"Found 'Option' in value")
|
||||
find_char_pattern = re.compile(r"Space at position: 6") # space at index 6
|
||||
substr_pattern = re.compile(r"Substr prefix: Option")
|
||||
# ADL function patterns (115200 from baud rate select)
|
||||
stoi_pattern = re.compile(r"stoi result: 115200")
|
||||
stol_pattern = re.compile(r"stol result: 115200")
|
||||
stof_pattern = re.compile(r"stof result: 115200")
|
||||
stod_pattern = re.compile(r"stod result: 115200")
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
if not value_logged_future.done() and value_pattern.search(line):
|
||||
value_logged_future.set_result(True)
|
||||
if not concatenated_future.done() and concatenated_pattern.search(line):
|
||||
concatenated_future.set_result(True)
|
||||
if not comparison_future.done() and comparison_pattern.search(line):
|
||||
comparison_future.set_result(True)
|
||||
if not index_logged_future.done() and index_pattern.search(line):
|
||||
index_logged_future.set_result(True)
|
||||
if not length_future.done() and length_pattern.search(line):
|
||||
length_future.set_result(True)
|
||||
if not find_substr_future.done() and find_substr_pattern.search(line):
|
||||
find_substr_future.set_result(True)
|
||||
if not find_char_future.done() and find_char_pattern.search(line):
|
||||
find_char_future.set_result(True)
|
||||
if not substr_future.done() and substr_pattern.search(line):
|
||||
substr_future.set_result(True)
|
||||
# ADL functions
|
||||
if not stoi_future.done() and stoi_pattern.search(line):
|
||||
stoi_future.set_result(True)
|
||||
if not stol_future.done() and stol_pattern.search(line):
|
||||
stol_future.set_result(True)
|
||||
if not stof_future.done() and stof_pattern.search(line):
|
||||
stof_future.set_result(True)
|
||||
if not stod_future.done() and stod_pattern.search(line):
|
||||
stod_future.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "select-stringref-test"
|
||||
|
||||
# List entities to find our select
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
select_entity = next(
|
||||
(e for e in entities if hasattr(e, "options") and e.name == "Test Select"),
|
||||
None,
|
||||
)
|
||||
assert select_entity is not None, "Test Select entity not found"
|
||||
|
||||
baud_entity = next(
|
||||
(e for e in entities if hasattr(e, "options") and e.name == "Baud Rate"),
|
||||
None,
|
||||
)
|
||||
assert baud_entity is not None, "Baud Rate entity not found"
|
||||
|
||||
# Change select to Option B - this should trigger on_value with StringRef
|
||||
client.select_command(select_entity.key, "Option B")
|
||||
# Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod)
|
||||
client.select_command(baud_entity.key, "115200")
|
||||
|
||||
# Wait for all log messages confirming StringRef operations work
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
asyncio.gather(
|
||||
value_logged_future,
|
||||
concatenated_future,
|
||||
comparison_future,
|
||||
index_logged_future,
|
||||
length_future,
|
||||
find_substr_future,
|
||||
find_char_future,
|
||||
substr_future,
|
||||
stoi_future,
|
||||
stol_future,
|
||||
stof_future,
|
||||
stod_future,
|
||||
),
|
||||
timeout=5.0,
|
||||
)
|
||||
except TimeoutError:
|
||||
results = {
|
||||
"value_logged": value_logged_future.done(),
|
||||
"concatenated": concatenated_future.done(),
|
||||
"comparison": comparison_future.done(),
|
||||
"index_logged": index_logged_future.done(),
|
||||
"length": length_future.done(),
|
||||
"find_substr": find_substr_future.done(),
|
||||
"find_char": find_char_future.done(),
|
||||
"substr": substr_future.done(),
|
||||
"stoi": stoi_future.done(),
|
||||
"stol": stol_future.done(),
|
||||
"stof": stof_future.done(),
|
||||
"stod": stod_future.done(),
|
||||
}
|
||||
pytest.fail(f"StringRef operations failed - received: {results}")
|
||||
Reference in New Issue
Block a user