Fix HASelect long-options parsing and add dynamic HANumber config updates.

This prevents overflow-related crashes when select options exceed 255 characters, adds runtime min/max/step updates that republish discovery config, and includes related HAText compilation fixes with test coverage.
This commit is contained in:
2026-04-16 16:44:44 +10:00
parent 2ebdcf302b
commit d954543236
7 changed files with 109 additions and 13 deletions
+8
View File
@@ -37,6 +37,14 @@ bool HANumber::setState(const HANumeric& state, const bool force)
return false;
}
void HANumber::updateMinMaxStep(const float min, const float max, const float step)
{
_minValue = HANumeric(min, _precision);
_maxValue = HANumeric(max, _precision);
_step = HANumeric(step, _precision);
publishConfig();
}
void HANumber::buildSerializer()
{
if (_serializer || !uniqueId()) {
+10
View File
@@ -189,6 +189,16 @@ public:
inline void setStep(const float step)
{ _step = HANumeric(step, _precision); }
/**
* Updates min/max/step and republishes discovery config.
* This can be used when limits need to change at runtime.
*
* @param min The minimal value.
* @param max The maximal value.
* @param step The slider step value.
*/
void updateMinMaxStep(const float min, const float max, const float step);
/**
* Registers callback that will be called each time the number is changed in the HA panel.
* Please note that it's not possible to register multiple callbacks for the same number.
+10 -3
View File
@@ -54,7 +54,7 @@ void HASelect::setOptions(const char* options)
return;
}
uint8_t optionLen = 0;
uint16_t optionLen = 0;
for (uint16_t i = 0; i < optionsLen; i++) {
if (options[i] == ';' || options[i] == 0) {
if (optionLen == 0) {
@@ -65,7 +65,10 @@ void HASelect::setOptions(const char* options)
option[optionLen] = 0;
memcpy(option, &options[i - optionLen], optionLen);
_options->add(option);
if (!_options->add(option)) {
delete[] option;
break;
}
optionLen = 0;
continue;
}
@@ -213,8 +216,12 @@ uint8_t HASelect::countOptionsInString(const char* options) const
return 0;
}
for (uint8_t i = 0; i < optionsLen; i++) {
for (uint16_t i = 0; i < optionsLen; i++) {
if (options[i] == ';') {
if (optionsNb == 255) {
break;
}
optionsNb++;
}
}
+6 -6
View File
@@ -10,8 +10,8 @@ HAText::HAText(const char* uniqueId) :
_retain(false),
_optimistic(false),
_mode(ModeText),
_min(),
_max(),
_minValue(),
_maxValue(),
_pattern(nullptr),
_currentState(nullptr),
_commandCallback(nullptr)
@@ -62,18 +62,18 @@ void HAText::buildSerializer()
);
_serializer->set(AHATOFSTR(HAPatternProperty), _pattern);
if (_min.isSet()) {
if (_minValue.isSet()) {
_serializer->set(
AHATOFSTR(HAMinProperty),
&_min,
&_minValue,
HASerializer::NumberPropertyType
);
}
if (_max.isSet()) {
if (_maxValue.isSet()) {
_serializer->set(
AHATOFSTR(HAMaxProperty),
&_max,
&_maxValue,
HASerializer::NumberPropertyType
);
}
+4 -4
View File
@@ -103,7 +103,7 @@ public:
* @param min Minimum number of characters.
*/
inline void setMin(const uint16_t min)
{ _min = HANumeric(min, PrecisionP0); }
{ _minValue = HANumeric(min, PrecisionP0); }
/**
* Sets maximum accepted length of the command payload.
@@ -111,7 +111,7 @@ public:
* @param max Maximum number of characters.
*/
inline void setMax(const uint16_t max)
{ _max = HANumeric(max, PrecisionP0); }
{ _maxValue = HANumeric(max, PrecisionP0); }
/**
* Sets regex pattern for command payload validation in Home Assistant.
@@ -186,10 +186,10 @@ private:
Mode _mode;
/// The minimal number of characters accepted by Home Assistant.
HANumeric _min;
HANumeric _minValue;
/// The maximum number of characters accepted by Home Assistant.
HANumeric _max;
HANumeric _maxValue;
/// Regular expression pattern accepted by Home Assistant. It can be nullptr.
const char* _pattern;
+51
View File
@@ -785,6 +785,57 @@ AHA_TEST(NumberTest, step_setter_p3) {
)
}
AHA_TEST(NumberTest, update_min_max_step_before_connect) {
prepareTest
HANumber number(testUniqueId);
number.updateMinMaxStep(1, 99, 5);
assertEntityConfig(
mock,
number,
(
"{"
"\"uniq_id\":\"uniqueNumber\","
"\"min\":1,"
"\"max\":99,"
"\"step\":5,"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueNumber/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueNumber/cmd_t\""
"}"
)
)
}
AHA_TEST(NumberTest, update_min_max_step_republishes_config) {
prepareTest
HANumber number(testUniqueId);
mqtt.loop();
assertEqual(2, mock->getFlushedMessagesNb()); // config + default state
number.updateMinMaxStep(1, 99, 5);
assertEqual(3, mock->getFlushedMessagesNb());
assertMqttMessage(
2,
AHATOFSTR(ConfigTopic),
(
"{"
"\"uniq_id\":\"uniqueNumber\","
"\"min\":1,"
"\"max\":99,"
"\"step\":5,"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueNumber/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueNumber/cmd_t\""
"}"
),
true
)
}
AHA_TEST(NumberTest, command_none) {
prepareTest
+20
View File
@@ -157,6 +157,26 @@ AHA_TEST(SelectTest, multiple_options) {
assertEqual(2, mock->getFlushedMessagesNb());
}
AHA_TEST(SelectTest, long_options_string_over_255_chars) {
prepareTest
char options[273];
for (uint16_t i = 0; i < 270; i++) {
options[i] = 'A';
}
options[270] = ';';
options[271] = 'B';
options[272] = 0;
HASelect select(testUniqueId);
select.setOptions(options);
assertTrue(select.getOptions() != nullptr);
assertEqual((uint8_t)2, select.getOptions()->getItemsNb());
assertEqual("B", select.getOptions()->getItem(1));
}
AHA_TEST(SelectTest, command_subscription) {
prepareTest