Files
arduino-home-assistant/test/test_entities_numeric/tests/test_select.cpp
T
alex b9f4fd7f4b Per-entity availability, deferred discovery publish, suggested precision
- Add HAAvailabilityConfig for per-entity availability_topic and payloads
- Defer discovery until MQTT connected; publish availability after discovery
- HASensor/HANumber: optional suggested_display_precision in discovery
- HASerializer/HADictionary: extend for new keys; update device types and mocks
- Docs and examples reflect availability and precision usage
- Tests updated for discovery and availability behavior
- Ignore .cursor/ in repo root
2026-05-05 17:47:55 +10:00

516 lines
14 KiB
C++

#include <Arduino.h>
#include <ArduinoHA.h>
#include "../../shared/aha_unity_helpers.h"
#define prepareTest \
initMqttTest(testDeviceId) \
lastCommandCallbackCall.reset();
#define assertCommandCallbackCalled(expectedIndex, callerPtr) \
TEST_ASSERT_TRUE(lastCommandCallbackCall.called); \
TEST_ASSERT_EQUAL(expectedIndex, lastCommandCallbackCall.index); \
TEST_ASSERT_EQUAL(callerPtr, lastCommandCallbackCall.caller);
#define assertCommandCallbackNotCalled() \
TEST_ASSERT_FALSE(lastCommandCallbackCall.called);
struct CommandCallback {
bool called = false;
int8_t index = -1;
HASelect* caller = nullptr;
void reset() {
called = false;
index = -1;
caller = nullptr;
}
};
static const char* testDeviceId = "testDevice";
static const char* testUniqueId = "uniqueSelect";
static CommandCallback lastCommandCallbackCall;
const char ConfigTopic[] PROGMEM = {"homeassistant/select/testDevice/uniqueSelect/config"};
const char StateTopic[] PROGMEM = {"testData/testDevice/uniqueSelect/stat_t"};
const char CommandTopic[] PROGMEM = {"testData/testDevice/uniqueSelect/cmd_t"};
void onCommandReceived(int8_t index, HASelect* caller)
{
lastCommandCallbackCall.called = true;
lastCommandCallbackCall.index = index;
lastCommandCallbackCall.caller = caller;
}
void onCommandDeferredPublish(int8_t index, HASelect* caller)
{
TEST_ASSERT_TRUE(caller->setState(index));
}
void test_SelectTest_invalid_unique_id(void) {
prepareTest
HASelect select(nullptr);
select.buildSerializerTest();
HASerializer* serializer = select.getSerializer();
TEST_ASSERT_TRUE(serializer == nullptr);
}
void test_SelectTest_no_options(void) {
prepareTest
HASelect select(testUniqueId);
select.buildSerializerTest();
HASerializer* serializer = select.getSerializer();
TEST_ASSERT_TRUE(serializer == nullptr);
TEST_ASSERT_TRUE(select.getOptions() == nullptr);
}
void test_SelectTest_invalid_options_nullptr(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions(nullptr);
select.buildSerializerTest();
HASerializer* serializer = select.getSerializer();
TEST_ASSERT_TRUE(serializer == nullptr);
TEST_ASSERT_TRUE(select.getOptions() == nullptr);
}
void test_SelectTest_invalid_options_empty(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("");
select.buildSerializerTest();
HASerializer* serializer = select.getSerializer();
TEST_ASSERT_TRUE(serializer == nullptr);
TEST_ASSERT_TRUE(select.getOptions() == nullptr);
}
void test_SelectTest_extended_unique_id(void) {
prepareTest
device.enableExtendedUniqueIds();
HASelect select(testUniqueId);
select.setOptions("Option A");
TEST_ASSERT_EQUAL(1, select.getOptions()->getItemsNb());
AHA_ASSERT_ENTITY_CONFIG(
mock,
select,
(
"{"
"\"uniq_id\":\"testDevice_uniqueSelect\","
"\"options\":[\"Option A\"],"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueSelect/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueSelect/cmd_t\""
"}"
)
);
TEST_ASSERT_EQUAL(2, mock->getFlushedMessagesNb());
}
void test_SelectTest_single_option(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A");
TEST_ASSERT_EQUAL(1, select.getOptions()->getItemsNb());
AHA_ASSERT_ENTITY_CONFIG(
mock,
select,
(
"{"
"\"uniq_id\":\"uniqueSelect\","
"\"options\":[\"Option A\"],"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueSelect/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueSelect/cmd_t\""
"}"
)
);
TEST_ASSERT_EQUAL(2, mock->getFlushedMessagesNb());
}
void test_SelectTest_multiple_options(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
TEST_ASSERT_EQUAL(3, select.getOptions()->getItemsNb());
AHA_ASSERT_ENTITY_CONFIG(
mock,
select,
(
"{"
"\"uniq_id\":\"uniqueSelect\","
"\"options\":[\"Option A\",\"B\",\"C\"],"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueSelect/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueSelect/cmd_t\""
"}"
)
);
TEST_ASSERT_EQUAL(2, mock->getFlushedMessagesNb());
}
void test_SelectTest_long_options_string_over_255_chars(void) {
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);
TEST_ASSERT_TRUE(select.getOptions() != nullptr);
TEST_ASSERT_EQUAL((uint8_t)2, select.getOptions()->getItemsNb());
TEST_ASSERT_EQUAL_STRING("B", select.getOptions()->getItem(1));
}
void test_SelectTest_command_subscription(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
mqtt.loop();
TEST_ASSERT_EQUAL(1, mock->getSubscriptionsNb());
AHA_ASSERT_EQUAL_FLASH_TOPIC(AHATOFSTR(CommandTopic), mock->getSubscriptions()[0]->topic);
}
void test_SelectTest_availability(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.setAvailability(true);
mqtt.loop();
// availability is published after config in HACover
AHA_ASSERT_MQTT_MESSAGE(mock,
1,
"testData/testDevice/uniqueSelect/avty_t",
"online",
true
);
}
void test_SelectTest_publish_last_known_state(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.setCurrentState(1);
mqtt.loop();
TEST_ASSERT_EQUAL(2, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL_STRING("B", select.getCurrentOption());
AHA_ASSERT_MQTT_MESSAGE(mock, 1, AHATOFSTR(StateTopic), "B", true);
}
void test_SelectTest_publish_nothing_if_retained(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.setCurrentState(1);
select.setRetain(true);
mqtt.loop();
TEST_ASSERT_EQUAL(1, mock->getFlushedMessagesNb()); // only config should be pushed
}
void test_SelectTest_publish_state_none(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
mqtt.loop();
TEST_ASSERT_EQUAL(2, mock->getFlushedMessagesNb());
AHA_ASSERT_MQTT_MESSAGE(mock, 1, AHATOFSTR(StateTopic), "None", true);
}
void test_SelectTest_name_setter(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.setName("testName");
AHA_ASSERT_ENTITY_CONFIG(
mock,
select,
(
"{"
"\"name\":\"testName\","
"\"uniq_id\":\"uniqueSelect\","
"\"options\":[\"Option A\",\"B\",\"C\"],"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueSelect/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueSelect/cmd_t\""
"}"
)
);
}
void test_SelectTest_object_id_setter(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.setObjectId("testId");
AHA_ASSERT_ENTITY_CONFIG(
mock,
select,
(
"{"
"\"obj_id\":\"testId\","
"\"uniq_id\":\"uniqueSelect\","
"\"options\":[\"Option A\",\"B\",\"C\"],"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueSelect/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueSelect/cmd_t\""
"}"
)
);
}
void test_SelectTest_icon_setter(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.setIcon("testIcon");
AHA_ASSERT_ENTITY_CONFIG(
mock,
select,
(
"{"
"\"uniq_id\":\"uniqueSelect\","
"\"ic\":\"testIcon\","
"\"options\":[\"Option A\",\"B\",\"C\"],"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueSelect/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueSelect/cmd_t\""
"}"
)
);
}
void test_SelectTest_retain_setter(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.setRetain(true);
AHA_ASSERT_ENTITY_CONFIG(
mock,
select,
(
"{"
"\"uniq_id\":\"uniqueSelect\","
"\"options\":[\"Option A\",\"B\",\"C\"],"
"\"ret\":true,"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueSelect/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueSelect/cmd_t\""
"}"
)
);
}
void test_SelectTest_optimistic_setter(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.setOptimistic(true);
AHA_ASSERT_ENTITY_CONFIG(
mock,
select,
(
"{"
"\"uniq_id\":\"uniqueSelect\","
"\"options\":[\"Option A\",\"B\",\"C\"],"
"\"opt\":true,"
"\"dev\":{\"ids\":\"testDevice\"},"
"\"stat_t\":\"testData/testDevice/uniqueSelect/stat_t\","
"\"cmd_t\":\"testData/testDevice/uniqueSelect/cmd_t\""
"}"
)
);
}
void test_SelectTest_current_state_getter(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.setCurrentState(1);
TEST_ASSERT_EQUAL(0, mock->getFlushedMessagesNb());
TEST_ASSERT_EQUAL(1, select.getCurrentState());
TEST_ASSERT_EQUAL_STRING("B", select.getCurrentOption());
}
void test_SelectTest_publish_state_first(void) {
prepareTest
mock->connectDummy();
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
TEST_ASSERT_TRUE(select.setState(0));
TEST_ASSERT_TRUE(select.getOptions() != nullptr);
TEST_ASSERT_EQUAL_STRING("Option A", select.getCurrentOption());
TEST_ASSERT_EQUAL(3, select.getOptions()->getItemsNb());
AHA_ASSERT_SINGLE_MQTT_MESSAGE(mock, AHATOFSTR(StateTopic), "Option A", true);
}
void test_SelectTest_publish_state_last(void) {
prepareTest
mock->connectDummy();
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
TEST_ASSERT_TRUE(select.setState(2));
TEST_ASSERT_TRUE(select.getOptions() != nullptr);
TEST_ASSERT_EQUAL_STRING("C", select.getCurrentOption());
TEST_ASSERT_EQUAL(3, select.getOptions()->getItemsNb());
AHA_ASSERT_SINGLE_MQTT_MESSAGE(mock, AHATOFSTR(StateTopic), "C", true);
}
void test_SelectTest_publish_state_only(void) {
prepareTest
mock->connectDummy();
HASelect select(testUniqueId);
select.setOptions("Option A");
TEST_ASSERT_TRUE(select.setState(0));
TEST_ASSERT_TRUE(select.getOptions() != nullptr);
TEST_ASSERT_EQUAL(1, select.getOptions()->getItemsNb());
AHA_ASSERT_SINGLE_MQTT_MESSAGE(mock, AHATOFSTR(StateTopic), "Option A", true);
}
void test_SelectTest_publish_state_debounce(void) {
prepareTest
mock->connectDummy();
HASelect select(testUniqueId);
select.setOptions("Option A");
select.setCurrentState(0);
// it shouldn't publish data if state doesn't change
TEST_ASSERT_TRUE(select.setState(0));
TEST_ASSERT_EQUAL(mock->getFlushedMessagesNb(), 0);
}
void test_SelectTest_publish_state_debounce_skip(void) {
prepareTest
mock->connectDummy();
HASelect select(testUniqueId);
select.setOptions("Option A");
select.setCurrentState(0);
TEST_ASSERT_TRUE(select.setState(0, true));
AHA_ASSERT_SINGLE_MQTT_MESSAGE(mock, AHATOFSTR(StateTopic), "Option A", true);
}
void test_SelectTest_command_option_first(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.onCommand(onCommandReceived);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("Option A"));
assertCommandCallbackCalled(0, &select)
}
void test_SelectTest_command_option_middle(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.onCommand(onCommandReceived);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("B"));
assertCommandCallbackCalled(1, &select)
}
void test_SelectTest_command_option_last(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.onCommand(onCommandReceived);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("C"));
assertCommandCallbackCalled(2, &select)
}
void test_SelectTest_command_option_non_existing(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.onCommand(onCommandReceived);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("DoesNotExist"));
assertCommandCallbackNotCalled()
}
void test_SelectTest_callback_publish_is_deferred_until_after_dispatch(void) {
prepareTest
mock->connectDummy();
mqtt.resetDeferredPublishTestCounters();
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.onCommand(onCommandDeferredPublish);
mock->fakeMessage(AHATOFSTR(CommandTopic), F("B"));
TEST_ASSERT_EQUAL_UINT16(1, mqtt.getDeferredPublishEnqueueCountForTest());
TEST_ASSERT_EQUAL_UINT8(1, mock->getFlushedMessagesNb());
AHA_ASSERT_MQTT_MESSAGE(mock, 0, AHATOFSTR(StateTopic), "B", true);
}
void test_SelectTest_different_select_command(void) {
prepareTest
HASelect select(testUniqueId);
select.setOptions("Option A;B;C");
select.onCommand(onCommandReceived);
mock->fakeMessage(
F("testData/testDevice/uniqueDifferentSelect/cmd_t"),
F("Option A")
);
assertCommandCallbackNotCalled()
}