ESPHome 2026.10.0
Loading...
Searching...
No Matches
homeassistant_select.cpp
Go to the documentation of this file.
2
3#include <cstring>
4
8#include "esphome/core/log.h"
10
11namespace esphome::homeassistant {
12
13static const char *const TAG = "homeassistant.select";
14
15namespace {
16
18template<typename C> bool emit_utf8(uint32_t code_point, C &on_char) {
19 if (code_point == 0 || code_point > 0x10FFFF)
20 return false;
21 if (code_point < 0x80) {
22 on_char(static_cast<char>(code_point));
23 } else if (code_point < 0x800) {
24 on_char(static_cast<char>(0xC0 | (code_point >> 6)));
25 on_char(static_cast<char>(0x80 | (code_point & 0x3F)));
26 } else if (code_point < 0x10000) {
27 on_char(static_cast<char>(0xE0 | (code_point >> 12)));
28 on_char(static_cast<char>(0x80 | ((code_point >> 6) & 0x3F)));
29 on_char(static_cast<char>(0x80 | (code_point & 0x3F)));
30 } else {
31 on_char(static_cast<char>(0xF0 | (code_point >> 18)));
32 on_char(static_cast<char>(0x80 | ((code_point >> 12) & 0x3F)));
33 on_char(static_cast<char>(0x80 | ((code_point >> 6) & 0x3F)));
34 on_char(static_cast<char>(0x80 | (code_point & 0x3F)));
35 }
36 return true;
37}
38
45template<typename C, typename O> bool parse_option_list(StringRef src, C &&on_char, O &&on_option) {
46 const char *pos = src.c_str();
47 const char *const end = pos + src.size();
48 auto skip_spaces = [&]() {
49 while (pos < end && *pos == ' ')
50 pos++;
51 };
52
53 skip_spaces();
54 if (pos == end || *pos != '[')
55 return false;
56 pos++;
57 skip_spaces();
58 if (pos < end && *pos == ']') {
59 pos++;
60 } else {
61 while (true) {
62 skip_spaces();
63 if (pos == end || (*pos != '\'' && *pos != '"'))
64 return false;
65 const char quote = *pos++;
66 while (true) {
67 if (pos == end)
68 return false;
69 const char c = *pos++;
70 if (c == quote)
71 break;
72 if (c != '\\') {
73 on_char(c);
74 continue;
75 }
76 if (pos == end)
77 return false;
78 const char escape = *pos++;
79 switch (escape) {
80 case '\\':
81 case '\'':
82 case '"':
83 on_char(escape);
84 break;
85 case 'n':
86 on_char('\n');
87 break;
88 case 'r':
89 on_char('\r');
90 break;
91 case 't':
92 on_char('\t');
93 break;
94 case 'x':
95 case 'u':
96 case 'U': {
97 const size_t digits = escape == 'x' ? 2 : (escape == 'u' ? 4 : 8);
98 if (static_cast<size_t>(end - pos) < digits)
99 return false;
100 auto code_point = parse_hex<uint32_t>(pos, digits);
101 if (!code_point.has_value() || !emit_utf8(*code_point, on_char))
102 return false;
103 pos += digits;
104 break;
105 }
106 default:
107 return false;
108 }
109 }
110 on_option();
111 skip_spaces();
112 if (pos == end)
113 return false;
114 const char separator = *pos++;
115 if (separator == ']')
116 break;
117 if (separator != ',')
118 return false;
119 }
120 }
121 skip_spaces();
122 return pos == end;
123}
124
125} // namespace
126
128 this->options_buffer_ = std::make_unique<char[]>(this->options_buffer_size_);
129 this->option_list_ = std::make_unique<const char *[]>(this->max_options_);
130
131 // Subscribe to the options first: Home Assistant answers subscriptions in order, so the options are
132 // known by the time the first state arrives.
134 this->entity_id_, "options", [this](StringRef options) { this->options_changed_(options); });
136 [this](StringRef state) { this->state_changed_(state); });
137}
138
140 // First pass: validate and measure the new options, and look for the active option among them, before
141 // anything is overwritten. A list that does not fit leaves the current options in place.
142 const char *active = this->has_state() ? this->option_at(this->active_index_) : nullptr;
143 size_t count = 0;
144 size_t bytes = 0;
145 size_t match_len = 0;
146 bool matching = active != nullptr;
147 optional<size_t> new_active;
148 bool valid = parse_option_list(
149 options,
150 [&](char c) {
151 bytes++;
152 if (matching && active[match_len] == c) {
153 match_len++;
154 } else {
155 matching = false;
156 }
157 },
158 [&]() {
159 bytes++; // NUL terminator
160 if (matching && active[match_len] == '\0' && !new_active.has_value())
161 new_active = count;
162 count++;
163 match_len = 0;
164 matching = active != nullptr;
165 });
166 if (!valid) {
167 ESP_LOGW(TAG, "'%s': Can't parse options %s", this->entity_id_, options.c_str());
168 return;
169 }
170 if (count > this->max_options_) {
171 ESP_LOGE(TAG, "'%s': %zu options exceed max_options (%u)", this->entity_id_, count, this->max_options_);
172 return;
173 }
174 if (bytes > this->options_buffer_size_) {
175 ESP_LOGE(TAG, "'%s': Options need %zu bytes, more than options_buffer_size (%u)", this->entity_id_, bytes,
176 this->options_buffer_size_);
177 return;
178 }
179
180 // Second pass: the input is known to be valid and to fit, so write it out
181 char *buffer = this->options_buffer_.get();
182 size_t offset = 0;
183 size_t option_start = 0;
184 size_t index = 0;
185 parse_option_list(
186 options, [&](char c) { buffer[offset++] = c; },
187 [&]() {
188 buffer[offset++] = '\0';
189 this->option_list_[index++] = buffer + option_start;
190 option_start = offset;
191 });
192 // The traits only point at the list, which this select owns; set_options() would copy it to the heap
193 this->traits.set_options_static(this->option_list_.get(), count);
194
195 // The active option may have moved to another index, or may be gone
196 if (new_active.has_value()) {
197 this->active_index_ = *new_active;
198 } else {
199 this->set_has_state(false);
200 }
201 ESP_LOGD(TAG, "'%s': Got %zu options", this->entity_id_, count);
202}
203
205 auto index = this->index_of(state.c_str(), state.size());
206 if (!index.has_value()) {
207 ESP_LOGW(TAG, "'%s': State '%s' is not one of the options", this->entity_id_, state.c_str());
208 return;
209 }
210 if (this->has_state() && this->active_index_ == *index)
211 return;
212 ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_, state.c_str());
213 this->publish_state(*index);
214}
215
217 LOG_SELECT("", "Homeassistant Select", this);
218 ESP_LOGCONFIG(TAG,
219 " Entity ID: '%s'\n"
220 " Max options: %u\n"
221 " Options buffer size: %u",
222 this->entity_id_, this->max_options_, this->options_buffer_size_);
223}
224
226
228 if (!api::global_api_server->is_connected()) {
229 ESP_LOGE(TAG, "No clients connected to API server");
230 return;
231 }
232
233#ifdef USE_API_WIZARD_LINKED_INPUTS
234 if (this->entity_id_[0] == '\0') {
235 ESP_LOGW(TAG, "'%s': No entity ID set yet", this->get_name().c_str());
236 return;
237 }
238#endif
239
240 static constexpr auto SERVICE_SELECT = StringRef::from_lit("select.select_option");
241 static constexpr auto SERVICE_INPUT_SELECT = StringRef::from_lit("input_select.select_option");
242 static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
243 static constexpr auto OPTION_KEY = StringRef::from_lit("option");
244 static constexpr char INPUT_PREFIX[] = "input_";
245
247 if (strncmp(this->entity_id_, INPUT_PREFIX, sizeof(INPUT_PREFIX) - 1) == 0) {
248 resp.service = SERVICE_INPUT_SELECT;
249 } else {
250 resp.service = SERVICE_SELECT;
251 }
252
253 resp.data.init(2);
254 auto &entity_id = resp.data.emplace_back();
255 entity_id.key = ENTITY_ID_KEY;
256 entity_id.value = StringRef(this->entity_id_);
257
258 auto &option = resp.data.emplace_back();
259 option.key = OPTION_KEY;
260 option.value = StringRef(this->option_at(index));
261
263}
264
265} // namespace esphome::homeassistant
const StringRef & get_name() const
Definition entity_base.h:71
void set_has_state(bool state)
bool has_state() const
StringRef is a reference to a string owned by something else.
Definition string_ref.h:30
constexpr const char * c_str() const
Definition string_ref.h:77
static constexpr StringRef from_lit(const CharT(&s)[N])
Definition string_ref.h:54
void send_homeassistant_action(const HomeassistantActionRequest &call)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function< void(StringRef)> &&f)
FixedVector< HomeassistantServiceMap > data
Definition api_pb2.h:1414
const char * option_at(size_t index) const
Return the option value at the provided index offset (as const char* from flash).
Definition select.cpp:60
optional< size_t > index_of(const char *option, size_t len) const
Find the (optional) index offset of the provided option value.
Definition select.cpp:35
SelectTraits traits
Definition select.h:31
void publish_state(const std::string &state)
Definition select.h:36
void set_options_static(const char *const *options, size_t count)
Codegen only: points at a table that outlives the select.
uint8_t options
bool state
Definition fan.h:2
unsigned count
Definition gif.cpp:51
uint32_t buffer
Definition gif.cpp:50
APIServer * global_api_server
constexpr float AFTER_CONNECTION
For components that should be initialized after a data connection (API/MQTT) is connected.
Definition component.h:57
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count)
Parse bytes from a hex-encoded string into a byte array.
Definition helpers.cpp:301
size_t size_t pos
Definition helpers.h:1186
const void * src
Definition hal.h:64
bool valid
static void uint32_t
uint8_t end[39]
Definition sun_gtil2.cpp:17