ESPSomfy-RTS/test/test_mqttcommands/test_mqttcommands.cpp
cjkas 395cb2d1c7 refactor: carry out shade requests from one module behind a LoopQueue
HTTP routes and MQTT commands now both build a shade request and submit it
to the LoopQueue, which loop() drains into ShadeRequests. ShadeRequests
finds the shade or group, turns client positions into stored ones, checks
ranges and fills in default repeats, and reaches the shades through the
ShadeOps seam: SomfyShadeOps on the device, a recording fake in the native
tests. MqttCommands turns a .../set topic into a shade request.

Behaviour that changes:
- MQTT tiltTarget, mypos, myTiltPos and tiltPosition, and /setPositions,
  now honour flipPosition like the other routes.
- MQTT mypos keeps the favorite tilt (was set to 0) and myTiltPos keeps
  the favorite lift.
- repeat 0 on /shadeCommand means no repeats, as it did for groups; a
  JSON repeat over 127 is capped instead of wrapping to the default.
- Position corrections go through the setters, so MQTT publishes them.
- MQTT commands wait while the radio is full instead of being refused.
- The queue is drained every loop() pass and no longer from inside a
  GitHub firmware download.

WebCommand.h is replaced by LoopQueue.h and ShadeRequests.h. 45 new native
tests, 271 in all. Flash: esp32devdbg +976 bytes, esp32dev +1,380 bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:42:58 +02:00

165 lines
7 KiB
C++

#include <unity.h>
#include <string.h>
#include "entryPoints/MqttCommands.h"
static shade_request_t req;
static bool parse(const char *topic, const char *value) {
req = shade_request_t();
return parseMqttCommand(topic, reinterpret_cast<const uint8_t *>(value), strlen(value), req);
}
static void assertType(shade_request_type_t expected) {
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(expected), static_cast<uint8_t>(req.type));
}
static void assertCommand(somfy_commands expected) {
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(expected), static_cast<uint8_t>(req.command));
}
void setUp() {}
void tearDown() {}
/*********************************************************************
* Topics
********************************************************************/
static void test_the_last_four_segments_name_the_command() {
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/target/set", "30"));
assertType(shade_request_type_t::shade_command);
TEST_ASSERT_EQUAL_UINT8(4, req.id);
TEST_ASSERT_EQUAL_INT8(30, req.target);
}
static void test_the_root_topic_may_be_empty_or_have_slashes() {
TEST_ASSERT_TRUE(parse("shades/12/target/set", "30"));
TEST_ASSERT_EQUAL_UINT8(12, req.id);
TEST_ASSERT_TRUE(parse("home/somfy/rts/shades/255/target/set", "30"));
TEST_ASSERT_EQUAL_UINT8(255, req.id);
}
static void test_topics_it_does_not_take() {
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/target", "30")); // not a set topic
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/target/get", "30"));
TEST_ASSERT_FALSE(parse("espsomfy/rooms/4/target/set", "30")); // no such entity
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/name/set", "30")); // no such command
TEST_ASSERT_FALSE(parse("espsomfy/shades/x4/target/set", "30")); // not an id
TEST_ASSERT_FALSE(parse("espsomfy/shades/256/target/set", "30"));
TEST_ASSERT_FALSE(parse("espsomfy/shades//target/set", "30"));
TEST_ASSERT_FALSE(parse("target/set", "30"));
TEST_ASSERT_FALSE(parse("", "30"));
}
static void test_the_payload_need_not_be_terminated() {
const char payload[] = { '4', '2', 'x', 'y' };
req = shade_request_t();
TEST_ASSERT_TRUE(parseMqttCommand("espsomfy/shades/4/target/set", reinterpret_cast<const uint8_t *>(payload), 2, req));
TEST_ASSERT_EQUAL_INT8(42, req.target);
}
/*********************************************************************
* Shades
********************************************************************/
static void test_targets_must_be_in_range() {
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/target/set", "100"));
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/target/set", "101"));
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/target/set", "-1"));
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/tiltTarget/set", "101"));
}
static void test_tilt_target() {
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/tiltTarget/set", "30"));
assertType(shade_request_type_t::tilt_command);
TEST_ASSERT_EQUAL_INT8(30, req.target);
}
static void test_direction_sends_up_down_or_my() {
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/direction/set", "-1"));
assertType(shade_request_type_t::shade_command);
TEST_ASSERT_EQUAL_INT8(-1, req.target);
assertCommand(somfy_commands::Up);
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/direction/set", "1"));
assertCommand(somfy_commands::Down);
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/direction/set", "0"));
assertCommand(somfy_commands::My);
}
static void test_favorite_lift_and_tilt() {
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/mypos/set", "30"));
assertType(shade_request_type_t::set_favorite);
TEST_ASSERT_EQUAL_INT8(30, req.position);
TEST_ASSERT_EQUAL_INT8(-1, req.tiltPosition);
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/myTiltPos/set", "60"));
assertType(shade_request_type_t::set_favorite);
TEST_ASSERT_EQUAL_INT8(-1, req.position);
TEST_ASSERT_EQUAL_INT8(60, req.tiltPosition);
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/mypos/set", "120"));
}
static void test_sun_flag() {
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/sunFlag/set", "1"));
assertType(shade_request_type_t::shade_command);
assertCommand(somfy_commands::SunFlag);
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/sunFlag/set", "0"));
assertCommand(somfy_commands::Flag);
}
static void test_sunny_and_windy_leave_the_other_alone() {
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/sunny/set", "1"));
assertType(shade_request_type_t::shade_sensor);
TEST_ASSERT_EQUAL_INT8(1, req.sunny);
TEST_ASSERT_EQUAL_INT8(-1, req.windy);
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/windy/set", "0"));
TEST_ASSERT_EQUAL_INT8(-1, req.sunny);
TEST_ASSERT_EQUAL_INT8(0, req.windy);
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/windy/set", "-1"));
}
static void test_position_and_tilt_position_correct_the_shade() {
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/position/set", "30"));
assertType(shade_request_type_t::set_positions);
TEST_ASSERT_EQUAL_INT8(30, req.position);
TEST_ASSERT_EQUAL_INT8(-1, req.tiltPosition);
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/tiltPosition/set", "60"));
TEST_ASSERT_EQUAL_INT8(-1, req.position);
TEST_ASSERT_EQUAL_INT8(60, req.tiltPosition);
}
static void test_shade_requests_use_the_default_repeats() {
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/direction/set", "1"));
TEST_ASSERT_EQUAL_INT8(-1, req.repeat);
}
/*********************************************************************
* Groups
********************************************************************/
static void test_group_direction_and_sun_flag() {
TEST_ASSERT_TRUE(parse("espsomfy/groups/2/direction/set", "-1"));
assertType(shade_request_type_t::group_command);
TEST_ASSERT_EQUAL_UINT8(2, req.id);
assertCommand(somfy_commands::Up);
TEST_ASSERT_TRUE(parse("espsomfy/groups/2/sunFlag/set", "1"));
assertType(shade_request_type_t::group_command);
assertCommand(somfy_commands::SunFlag);
}
static void test_group_sunny_and_windy() {
TEST_ASSERT_TRUE(parse("espsomfy/groups/2/sunny/set", "0"));
assertType(shade_request_type_t::group_sensor);
TEST_ASSERT_EQUAL_INT8(0, req.sunny);
TEST_ASSERT_EQUAL_INT8(-1, req.windy);
TEST_ASSERT_TRUE(parse("espsomfy/groups/2/windy/set", "1"));
TEST_ASSERT_EQUAL_INT8(1, req.windy);
}
static void test_groups_only_take_their_commands() {
TEST_ASSERT_FALSE(parse("espsomfy/groups/2/target/set", "30"));
TEST_ASSERT_FALSE(parse("espsomfy/groups/2/mypos/set", "30"));
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_the_last_four_segments_name_the_command);
RUN_TEST(test_the_root_topic_may_be_empty_or_have_slashes);
RUN_TEST(test_topics_it_does_not_take);
RUN_TEST(test_the_payload_need_not_be_terminated);
RUN_TEST(test_targets_must_be_in_range);
RUN_TEST(test_tilt_target);
RUN_TEST(test_direction_sends_up_down_or_my);
RUN_TEST(test_favorite_lift_and_tilt);
RUN_TEST(test_sun_flag);
RUN_TEST(test_sunny_and_windy_leave_the_other_alone);
RUN_TEST(test_position_and_tilt_position_correct_the_shade);
RUN_TEST(test_shade_requests_use_the_default_repeats);
RUN_TEST(test_group_direction_and_sun_flag);
RUN_TEST(test_group_sunny_and_windy);
RUN_TEST(test_groups_only_take_their_commands);
return UNITY_END();
}