Compare commits

...
Author SHA1 Message Date
J. Nick Koston d06f58f972 [homeassistant] Keep the button, number, select, switch and text action strings in flash 2026-10-14 15:43:39 -10:00
J. Nick Koston 657a854431 Look a string up in a PROGMEM_STRING_TABLE
progmem_string_table_find() returns the index of a RAM string in the table, for parsing a name
against a fixed set.
2026-10-14 15:39:31 -10:00
J. Nick Koston a28cd24dcd [core] Add ProgmemStringTableCopy
Hands the strings of a PROGMEM_STRING_TABLE out as plain pointers for code that passes them on
without reading them, such as an API request: a stack copy on the ESP8266, the table in place
elsewhere.
2026-10-14 15:15:03 -10:00
7 changed files with 150 additions and 60 deletions
@@ -5,12 +5,21 @@
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
ESPHOME_LOG_TAG(TAG, "homeassistant.button");
// In flash on the ESP8266; copied to the stack only while a request is built
PROGMEM_STRING_TABLE(ButtonActionStrings, "button.press", "input_button.press", "entity_id");
enum ButtonActionString : uint8_t {
BUTTON_ACTION_STRING_SERVICE,
BUTTON_ACTION_STRING_INPUT_SERVICE,
BUTTON_ACTION_STRING_ENTITY_ID,
};
void HomeassistantButton::dump_config() {
LOG_BUTTON("", "Homeassistant Button", this);
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
@@ -31,21 +40,17 @@ void HomeassistantButton::press_action() {
}
#endif
static constexpr auto SERVICE_BUTTON = StringRef::from_lit("button.press");
static constexpr auto SERVICE_INPUT_BUTTON = StringRef::from_lit("input_button.press");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr char INPUT_PREFIX[] = "input_";
ProgmemStringTableCopy<ButtonActionStrings> strings;
// An input helper entity id starts with the "input_" its action name starts with
const bool input_helper =
strncmp(this->entity_id_, strings.get(BUTTON_ACTION_STRING_INPUT_SERVICE), sizeof("input_") - 1) == 0;
api::HomeassistantActionRequest resp;
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = SERVICE_INPUT_BUTTON;
} else {
resp.service = SERVICE_BUTTON;
}
resp.service =
StringRef(strings.get(input_helper ? BUTTON_ACTION_STRING_INPUT_SERVICE : BUTTON_ACTION_STRING_SERVICE));
resp.data.init(1);
auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.key = ENTITY_ID_KEY;
entity_id_kv.key = StringRef(strings.get(BUTTON_ACTION_STRING_ENTITY_ID));
entity_id_kv.value = StringRef(this->entity_id_);
api::global_api_server->send_homeassistant_action(resp);
@@ -5,12 +5,22 @@
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
ESPHOME_LOG_TAG(TAG, "homeassistant.number");
// In flash on the ESP8266; copied to the stack only while a request is built
PROGMEM_STRING_TABLE(NumberActionStrings, "number.set_value", "input_number.set_value", "entity_id", "value");
enum NumberActionString : uint8_t {
NUMBER_ACTION_STRING_SERVICE,
NUMBER_ACTION_STRING_INPUT_SERVICE,
NUMBER_ACTION_STRING_ENTITY_ID,
NUMBER_ACTION_STRING_VALUE,
};
void HomeassistantNumber::state_changed_(StringRef state) {
auto number_value = parse_number<float>(state.c_str());
if (!number_value.has_value()) {
@@ -89,27 +99,22 @@ void HomeassistantNumber::control(float value) {
this->publish_state(value);
static constexpr auto NUMBER_SERVICE_NAME = StringRef::from_lit("number.set_value");
static constexpr auto INPUT_NUMBER_SERVICE_NAME = StringRef::from_lit("input_number.set_value");
static constexpr char INPUT_PREFIX[] = "input_";
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
ProgmemStringTableCopy<NumberActionStrings> strings;
// An input helper entity id starts with the "input_" its action name starts with
const bool input_helper =
strncmp(this->entity_id_, strings.get(NUMBER_ACTION_STRING_INPUT_SERVICE), sizeof("input_") - 1) == 0;
api::HomeassistantActionRequest resp;
// input_number entities have their own set_value action
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = INPUT_NUMBER_SERVICE_NAME;
} else {
resp.service = NUMBER_SERVICE_NAME;
}
resp.service =
StringRef(strings.get(input_helper ? NUMBER_ACTION_STRING_INPUT_SERVICE : NUMBER_ACTION_STRING_SERVICE));
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
entity_id.key = ENTITY_ID_KEY;
entity_id.key = StringRef(strings.get(NUMBER_ACTION_STRING_ENTITY_ID));
entity_id.value = StringRef(this->entity_id_);
auto &entity_value = resp.data.emplace_back();
entity_value.key = VALUE_KEY;
entity_value.key = StringRef(strings.get(NUMBER_ACTION_STRING_VALUE));
// Stack buffer - no heap allocation; %g produces shortest representation
char value_buf[16];
buf_append_printf(value_buf, sizeof(value_buf), 0, "%g", value);
@@ -6,12 +6,22 @@
#include "esphome/components/api/api_server.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
ESPHOME_LOG_TAG(TAG, "homeassistant.select");
// In flash on the ESP8266; copied to the stack only while a request is built
PROGMEM_STRING_TABLE(SelectActionStrings, "select.select_option", "input_select.select_option", "entity_id", "option");
enum SelectActionString : uint8_t {
SELECT_ACTION_STRING_SERVICE,
SELECT_ACTION_STRING_INPUT_SERVICE,
SELECT_ACTION_STRING_ENTITY_ID,
SELECT_ACTION_STRING_OPTION,
};
namespace {
/// Emit the UTF-8 encoding of a code point. Rejects NUL, which would end the option early.
@@ -237,26 +247,21 @@ void HomeassistantSelect::control(size_t index) {
}
#endif
static constexpr auto SERVICE_SELECT = StringRef::from_lit("select.select_option");
static constexpr auto SERVICE_INPUT_SELECT = StringRef::from_lit("input_select.select_option");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto OPTION_KEY = StringRef::from_lit("option");
static constexpr char INPUT_PREFIX[] = "input_";
ProgmemStringTableCopy<SelectActionStrings> strings;
// An input helper entity id starts with the "input_" its action name starts with
const bool input_helper =
strncmp(this->entity_id_, strings.get(SELECT_ACTION_STRING_INPUT_SERVICE), sizeof("input_") - 1) == 0;
api::HomeassistantActionRequest resp;
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = SERVICE_INPUT_SELECT;
} else {
resp.service = SERVICE_SELECT;
}
resp.service =
StringRef(strings.get(input_helper ? SELECT_ACTION_STRING_INPUT_SERVICE : SELECT_ACTION_STRING_SERVICE));
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
entity_id.key = ENTITY_ID_KEY;
entity_id.key = StringRef(strings.get(SELECT_ACTION_STRING_ENTITY_ID));
entity_id.value = StringRef(this->entity_id_);
auto &option = resp.data.emplace_back();
option.key = OPTION_KEY;
option.key = StringRef(strings.get(SELECT_ACTION_STRING_OPTION));
option.value = StringRef(this->option_at(index));
api::global_api_server->send_homeassistant_action(resp);
@@ -1,12 +1,21 @@
#include "homeassistant_switch.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
ESPHOME_LOG_TAG(TAG, "homeassistant.switch");
// In flash on the ESP8266; copied to the stack only while a request is built
PROGMEM_STRING_TABLE(SwitchActionStrings, "homeassistant.turn_on", "homeassistant.turn_off", "entity_id");
enum SwitchActionString : uint8_t {
SWITCH_ACTION_STRING_TURN_ON,
SWITCH_ACTION_STRING_TURN_OFF,
SWITCH_ACTION_STRING_ENTITY_ID,
};
using namespace esphome::switch_;
void HomeassistantSwitch::setup() {
@@ -47,20 +56,13 @@ void HomeassistantSwitch::write_state(bool state) {
}
#endif
static constexpr auto SERVICE_ON = StringRef::from_lit("homeassistant.turn_on");
static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
ProgmemStringTableCopy<SwitchActionStrings> strings;
api::HomeassistantActionRequest resp;
if (state) {
resp.service = SERVICE_ON;
} else {
resp.service = SERVICE_OFF;
}
resp.service = StringRef(strings.get(state ? SWITCH_ACTION_STRING_TURN_ON : SWITCH_ACTION_STRING_TURN_OFF));
resp.data.init(1);
auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.key = ENTITY_ID_KEY;
entity_id_kv.key = StringRef(strings.get(SWITCH_ACTION_STRING_ENTITY_ID));
entity_id_kv.value = StringRef(this->entity_id_);
api::global_api_server->send_homeassistant_action(resp);
@@ -6,12 +6,22 @@
#include "esphome/components/api/api_server.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::homeassistant {
ESPHOME_LOG_TAG(TAG, "homeassistant.text");
// In flash on the ESP8266; copied to the stack only while a request is built
PROGMEM_STRING_TABLE(TextActionStrings, "text.set_value", "input_text.set_value", "entity_id", "value");
enum TextActionString : uint8_t {
TEXT_ACTION_STRING_SERVICE,
TEXT_ACTION_STRING_INPUT_SERVICE,
TEXT_ACTION_STRING_ENTITY_ID,
TEXT_ACTION_STRING_VALUE,
};
void HomeassistantText::state_changed_(StringRef state) {
if (state == this->state) {
return;
@@ -86,26 +96,20 @@ void HomeassistantText::control(const std::string &value) {
}
#endif
static constexpr auto SERVICE_TEXT = StringRef::from_lit("text.set_value");
static constexpr auto SERVICE_INPUT_TEXT = StringRef::from_lit("input_text.set_value");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
static constexpr char INPUT_PREFIX[] = "input_";
ProgmemStringTableCopy<TextActionStrings> strings;
// An input helper entity id starts with the "input_" its action name starts with
const bool input_helper =
strncmp(this->entity_id_, strings.get(TEXT_ACTION_STRING_INPUT_SERVICE), sizeof("input_") - 1) == 0;
api::HomeassistantActionRequest resp;
if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
resp.service = SERVICE_INPUT_TEXT;
} else {
resp.service = SERVICE_TEXT;
}
resp.service = StringRef(strings.get(input_helper ? TEXT_ACTION_STRING_INPUT_SERVICE : TEXT_ACTION_STRING_SERVICE));
resp.data.init(2);
auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.key = ENTITY_ID_KEY;
entity_id_kv.key = StringRef(strings.get(TEXT_ACTION_STRING_ENTITY_ID));
entity_id_kv.value = StringRef(this->entity_id_);
auto &value_kv = resp.data.emplace_back();
value_kv.key = VALUE_KEY;
value_kv.key = StringRef(strings.get(TEXT_ACTION_STRING_VALUE));
value_kv.value = StringRef(value.data(), value.size());
api::global_api_server->send_homeassistant_action(resp);
+34
View File
@@ -153,4 +153,38 @@ struct LogString;
} \
}
/// The index of the string value (len bytes, in RAM) in a PROGMEM_STRING_TABLE, or -1 when it is
/// not one of its strings
template<typename Table> int8_t progmem_string_table_find(const char *value, size_t len) {
for (uint8_t i = 0; i < Table::COUNT; i++) {
const char *entry = Table::get_(i, i);
if (ESPHOME_strlen_P(entry) == len && ESPHOME_strncmp_P(value, entry, len) == 0) {
return i;
}
}
return -1;
}
/// The strings of a PROGMEM_STRING_TABLE addressable as plain pointers, for code that hands them on
/// without reading them itself, such as an API request. On the ESP8266 this is a copy of the table on
/// the stack that lives as long as the object; elsewhere the table is read in place.
///
/// Example, with an enum that numbers the table's strings:
/// ProgmemStringTableCopy<MyStrings> strings;
/// request.service = StringRef(strings.get(MY_STRING_TURN_ON));
template<typename Name> class ProgmemStringTableCopy {
static_assert(Name::Table::INDEXED, "the table needs offsets");
public:
#ifdef USE_ESP8266
ProgmemStringTableCopy() { progmem_memcpy(this->blob_, Name::BLOB.data(), Name::BLOB_SIZE); }
const char *get(uint8_t idx) const { return this->blob_ + progmem_read_byte(&Name::OFFSETS[idx]); }
private:
char blob_[Name::BLOB_SIZE];
#else
const char *get(uint8_t idx) const { return Name::get_(idx, idx); }
#endif
};
} // namespace esphome
@@ -0,0 +1,35 @@
#include <gtest/gtest.h>
#include <cstring>
#include "esphome/core/progmem.h"
namespace esphome::testing {
PROGMEM_STRING_TABLE(Names, "light.turn_on", "entity_id", "rgb_color");
TEST(ProgmemStringTableCopy, HandsOutEveryString) {
ProgmemStringTableCopy<Names> strings;
EXPECT_STREQ(strings.get(0), "light.turn_on");
EXPECT_STREQ(strings.get(1), "entity_id");
EXPECT_STREQ(strings.get(2), "rgb_color");
}
TEST(ProgmemStringTableCopy, StringsStayValidWhileTheCopyLives) {
ProgmemStringTableCopy<Names> strings;
const char *key = strings.get(1);
const char *other = strings.get(2);
EXPECT_EQ(std::strlen(key), 9U);
EXPECT_STREQ(key, "entity_id");
EXPECT_STREQ(other, "rgb_color");
}
TEST(ProgmemStringTableFind, FindsWholeStringsOnly) {
EXPECT_EQ(progmem_string_table_find<Names>("entity_id", 9), 1);
EXPECT_EQ(progmem_string_table_find<Names>("rgb_color", 9), 2);
EXPECT_EQ(progmem_string_table_find<Names>("entity", 6), -1);
EXPECT_EQ(progmem_string_table_find<Names>("entity_id_x", 11), -1);
EXPECT_EQ(progmem_string_table_find<Names>("", 0), -1);
}
} // namespace esphome::testing