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>
This commit is contained in:
cjkas 2026-10-16 09:36:46 +02:00
parent 5fd5feda28
commit 395cb2d1c7
20 changed files with 1354 additions and 569 deletions

View file

@ -0,0 +1,78 @@
#include <unity.h>
#include "LoopQueue.h"
static loop_item_t request(shade_request_type_t type) {
loop_item_t item;
item.request.type = type;
return item;
}
static loop_item_t item(loop_item_type_t type) {
loop_item_t item;
item.type = type;
return item;
}
static const shade_request_type_t RADIO_REQUESTS[] = {
shade_request_type_t::shade_command, shade_request_type_t::group_command, shade_request_type_t::tilt_command,
shade_request_type_t::shade_repeat, shade_request_type_t::group_repeat, shade_request_type_t::shade_sensor,
shade_request_type_t::group_sensor, shade_request_type_t::set_favorite, shade_request_type_t::unpair
};
static const shade_request_type_t OFF_RADIO_REQUESTS[] = {
shade_request_type_t::set_positions, shade_request_type_t::set_rolling_code
};
static const loop_item_type_t OTHER_ITEMS[] = { loop_item_type_t::restore_shades, loop_item_type_t::load_shades };
void setUp() {}
void tearDown() {}
static void test_only_position_and_rolling_code_requests_stay_off_the_radio() {
int offRadio = 0;
for(uint8_t t = 0; t <= static_cast<uint8_t>(shade_request_type_t::set_rolling_code); t++) {
if(!shadeRequestUsesRadio(static_cast<shade_request_type_t>(t))) offRadio++;
}
TEST_ASSERT_EQUAL_INT(2, offRadio);
for(shade_request_type_t t : OFF_RADIO_REQUESTS) TEST_ASSERT_FALSE(loopItemUsesRadio(request(t)));
for(loop_item_type_t t : OTHER_ITEMS) TEST_ASSERT_FALSE(loopItemUsesRadio(item(t)));
}
static void test_off_radio_items_are_never_held() {
// Nothing goes on air, so a full or suspended radio doesn't matter.
for(shade_request_type_t t : OFF_RADIO_REQUESTS) {
TEST_ASSERT_FALSE(holdLoopItem(request(t), false, false));
TEST_ASSERT_FALSE(holdLoopItem(request(t), true, false));
}
for(loop_item_type_t t : OTHER_ITEMS) {
TEST_ASSERT_FALSE(holdLoopItem(item(t), false, false));
TEST_ASSERT_FALSE(holdLoopItem(item(t), true, false));
}
}
static void test_radio_request_waits_for_room() {
for(shade_request_type_t t : RADIO_REQUESTS) {
TEST_ASSERT_TRUE(holdLoopItem(request(t), false, false));
TEST_ASSERT_FALSE(holdLoopItem(request(t), false, true));
}
}
static void test_requests_are_not_held_while_suspended() {
// Nothing goes on air during a frequency scan or OTA anyway.
for(shade_request_type_t t : RADIO_REQUESTS) {
if(t == shade_request_type_t::unpair) continue;
TEST_ASSERT_FALSE(holdLoopItem(request(t), true, false));
TEST_ASSERT_FALSE(holdLoopItem(request(t), true, true));
}
}
static void test_unpair_waits_out_a_suspend() {
// The client was already told the shade is unpaired.
TEST_ASSERT_TRUE(holdLoopItem(request(shade_request_type_t::unpair), true, false));
TEST_ASSERT_TRUE(holdLoopItem(request(shade_request_type_t::unpair), true, true));
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_only_position_and_rolling_code_requests_stay_off_the_radio);
RUN_TEST(test_off_radio_items_are_never_held);
RUN_TEST(test_radio_request_waits_for_room);
RUN_TEST(test_requests_are_not_held_while_suspended);
RUN_TEST(test_unpair_waits_out_a_suspend);
return UNITY_END();
}

View file

@ -0,0 +1,165 @@
#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();
}

View file

@ -0,0 +1,398 @@
#include <unity.h>
#include <map>
#include <string>
#include "ShadeRequests.h"
// Records every call as a line so a test reads as "this request, these calls".
class FakeShadeOps : public ShadeOps {
public:
std::map<uint8_t, shade_info_t> shades;
std::map<uint8_t, uint8_t> groups; // group id -> its repeats
bool radioTakes = true; // false: the radio refuses everything
bool lastCommand = false; // what isLastCommand answers
std::string calls;
bool findShade(uint8_t id, shade_info_t &info) override {
auto it = shades.find(id);
if(it == shades.end()) return false;
info = it->second;
return true;
}
bool findGroup(uint8_t id, uint8_t &repeats) override {
auto it = groups.find(id);
if(it == groups.end()) return false;
repeats = it->second;
return true;
}
bool moveToTarget(uint8_t id, float pos) override { return log("moveToTarget %u %g", id, pos); }
bool moveToTiltTarget(uint8_t id, float tilt) override { return log("moveToTiltTarget %u %g", id, tilt); }
bool sendCommand(uint8_t id, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) override {
return log("sendCommand %u %s repeat=%u step=%u", id, name(cmd), repeat, stepSize);
}
bool sendTiltCommand(uint8_t id, somfy_commands cmd) override { return log("sendTiltCommand %u %s", id, name(cmd)); }
bool isLastCommand(uint8_t, somfy_commands) override { return lastCommand; }
bool repeatFrame(uint8_t id, uint8_t repeat) override { return log("repeatFrame %u repeat=%u", id, repeat); }
bool sendSensorCommand(uint8_t id, int8_t windy, int8_t sunny, uint8_t repeat) override {
return log("sendSensorCommand %u windy=%d sunny=%d repeat=%u", id, windy, sunny, repeat);
}
bool setMyPosition(uint8_t id, int8_t pos, int8_t tilt) override { return log("setMyPosition %u %d %d", id, pos, tilt); }
void setPositions(uint8_t id, float pos, float tilt) override { log("setPositions %u %g %g", id, pos, tilt); }
void setUnpaired(uint8_t id) override { log("setUnpaired %u", id); }
void setRollingCode(uint8_t id, uint16_t code) override { log("setRollingCode %u %u", id, code); }
bool sendGroupCommand(uint8_t id, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) override {
return log("sendGroupCommand %u %s repeat=%u step=%u", id, name(cmd), repeat, stepSize);
}
bool isLastGroupCommand(uint8_t, somfy_commands) override { return lastCommand; }
bool repeatGroupFrame(uint8_t id, uint8_t repeat) override { return log("repeatGroupFrame %u repeat=%u", id, repeat); }
bool sendGroupSensorCommand(uint8_t id, int8_t windy, int8_t sunny, uint8_t repeat) override {
return log("sendGroupSensorCommand %u windy=%d sunny=%d repeat=%u", id, windy, sunny, repeat);
}
private:
static const char *name(somfy_commands cmd) {
static std::string s;
s = translateSomfyCommand(cmd).c_str();
return s.c_str();
}
template<typename... Args>
bool log(const char *fmt, Args... args) {
char line[128];
snprintf(line, sizeof(line), fmt, args...);
if(!calls.empty()) calls += "; ";
calls += line;
return radioTakes;
}
};
static FakeShadeOps ops;
static ShadeRequests requests(ops);
static shade_info_t rollerShade() {
shade_info_t s;
s.repeats = 2;
return s;
}
static shade_info_t flippedShade() {
shade_info_t s = rollerShade();
s.flipPosition = true;
return s;
}
static shade_info_t tiltShade(tilt_types tilt) {
shade_info_t s = rollerShade();
s.tiltType = tilt;
return s;
}
static shade_request_t request(shade_request_type_t type, uint8_t id = 4) {
shade_request_t req;
req.type = type;
req.id = id;
return req;
}
void setUp() {
ops.shades.clear();
ops.groups.clear();
ops.radioTakes = true;
ops.lastCommand = false;
ops.calls.clear();
}
void tearDown() {}
/*********************************************************************
* Finding the shade or group
********************************************************************/
static void test_a_missing_shade_is_dropped() {
shade_request_t req = request(shade_request_type_t::shade_command);
req.target = 30;
TEST_ASSERT_TRUE(requests.run(req));
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
}
static void test_a_missing_group_is_dropped() {
TEST_ASSERT_TRUE(requests.run(request(shade_request_type_t::group_command, 2)));
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
}
static void test_a_shade_id_is_not_a_group_id() {
ops.shades[2] = rollerShade();
requests.run(request(shade_request_type_t::group_command, 2));
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
}
/*********************************************************************
* Positions as the client sees them
********************************************************************/
static void test_target_moves_a_shade_there() {
ops.shades[4] = rollerShade();
shade_request_t req = request(shade_request_type_t::shade_command);
req.target = 30;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("moveToTarget 4 30", ops.calls.c_str());
}
static void test_target_on_a_flipped_shade_is_turned_round() {
ops.shades[4] = flippedShade();
shade_request_t req = request(shade_request_type_t::shade_command);
req.target = 30;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("moveToTarget 4 70", ops.calls.c_str());
}
static void test_tilt_target_on_a_flipped_shade_is_turned_round() {
// MQTT used to send this one straight through.
ops.shades[4] = flippedShade();
shade_request_t req = request(shade_request_type_t::tilt_command);
req.target = 30;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("moveToTiltTarget 4 70", ops.calls.c_str());
}
static void test_without_a_target_the_command_is_sent() {
ops.shades[4] = rollerShade();
shade_request_t req = request(shade_request_type_t::shade_command);
req.command = somfy_commands::Down;
req.stepSize = 3;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Down repeat=2 step=3", ops.calls.c_str());
}
static void test_tilt_without_a_target_sends_the_tilt_command() {
ops.shades[4] = rollerShade();
shade_request_t req = request(shade_request_type_t::tilt_command);
req.command = somfy_commands::Up;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("sendTiltCommand 4 Up", ops.calls.c_str());
}
static void test_set_positions_on_a_flipped_shade_are_turned_round() {
// /setPositions used to send these straight through.
ops.shades[4] = flippedShade();
ops.shades[4].tiltType = tilt_types::integrated;
shade_request_t req = request(shade_request_type_t::set_positions);
req.position = 20;
req.tiltPosition = 90;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("setPositions 4 80 10", ops.calls.c_str());
}
static void test_set_positions_leaves_the_tilt_of_a_shade_without_one() {
ops.shades[4] = rollerShade();
shade_request_t req = request(shade_request_type_t::set_positions);
req.position = 20;
req.tiltPosition = 90;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("setPositions 4 20 -1", ops.calls.c_str());
}
static void test_set_positions_with_nothing_in_range_does_nothing() {
ops.shades[4] = tiltShade(tilt_types::integrated);
shade_request_t req = request(shade_request_type_t::set_positions);
req.position = -1;
req.tiltPosition = 101;
TEST_ASSERT_TRUE(requests.run(req));
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
}
static void test_client_values_are_checked_on_the_way_in() {
TEST_ASSERT_EQUAL_INT8(0, requestTarget(0));
TEST_ASSERT_EQUAL_INT8(100, requestTarget(100));
TEST_ASSERT_EQUAL_INT8(-1, requestTarget(101));
TEST_ASSERT_EQUAL_INT8(-1, requestTarget(255));
TEST_ASSERT_EQUAL_INT8(-1, requestRepeat(-5));
TEST_ASSERT_EQUAL_INT8(0, requestRepeat(0));
TEST_ASSERT_EQUAL_INT8(127, requestRepeat(200));
}
/*********************************************************************
* Repeats
********************************************************************/
static void test_no_repeat_means_the_shades_own() {
ops.shades[4] = rollerShade();
shade_request_t req = request(shade_request_type_t::shade_command);
req.command = somfy_commands::Up;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Up repeat=2 step=0", ops.calls.c_str());
}
static void test_repeat_zero_means_no_repeats_for_a_shade_too() {
// It used to mean "the shade's own" for /shadeCommand but not for groups.
ops.shades[4] = rollerShade();
ops.groups[2] = 3;
shade_request_t req = request(shade_request_type_t::shade_command);
req.command = somfy_commands::Up;
req.repeat = 0;
requests.run(req);
shade_request_t group = request(shade_request_type_t::group_command, 2);
group.command = somfy_commands::Up;
group.repeat = 0;
requests.run(group);
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Up repeat=0 step=0; sendGroupCommand 2 Up repeat=0 step=0", ops.calls.c_str());
}
static void test_no_repeat_means_the_groups_own() {
ops.groups[2] = 3;
shade_request_t req = request(shade_request_type_t::group_command, 2);
req.command = somfy_commands::Down;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("sendGroupCommand 2 Down repeat=3 step=0", ops.calls.c_str());
}
/*********************************************************************
* Repeating the last command
********************************************************************/
static void test_repeat_sends_a_new_command_when_it_was_not_the_last() {
ops.shades[4] = rollerShade();
shade_request_t req = request(shade_request_type_t::shade_repeat);
req.command = somfy_commands::Prog;
req.repeat = 10;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Prog repeat=10 step=0", ops.calls.c_str());
}
static void test_repeat_repeats_the_last_frame() {
ops.shades[4] = rollerShade();
ops.lastCommand = true;
shade_request_t req = request(shade_request_type_t::shade_repeat);
req.command = somfy_commands::Prog;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("repeatFrame 4 repeat=2", ops.calls.c_str());
}
static void test_prog_on_a_one_button_garage_door_is_toggle() {
ops.shades[4] = rollerShade();
ops.shades[4].shadeType = shade_types::garage1;
shade_request_t req = request(shade_request_type_t::shade_repeat);
req.command = somfy_commands::Prog;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Toggle repeat=2 step=0", ops.calls.c_str());
}
static void test_group_repeat_repeats_the_last_frame() {
ops.groups[2] = 3;
ops.lastCommand = true;
requests.run(request(shade_request_type_t::group_repeat, 2));
TEST_ASSERT_EQUAL_STRING("repeatGroupFrame 2 repeat=3", ops.calls.c_str());
}
/*********************************************************************
* Sensor frames
********************************************************************/
static void test_sensor_sends_what_was_given() {
ops.shades[4] = rollerShade();
shade_request_t req = request(shade_request_type_t::shade_sensor);
req.sunny = 1;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("sendSensorCommand 4 windy=-1 sunny=1 repeat=2", ops.calls.c_str());
}
static void test_group_sensor_uses_the_groups_repeats() {
ops.groups[2] = 3;
shade_request_t req = request(shade_request_type_t::group_sensor, 2);
req.windy = 0;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("sendGroupSensorCommand 2 windy=0 sunny=-1 repeat=3", ops.calls.c_str());
}
/*********************************************************************
* The favorite
********************************************************************/
static void test_favorite_on_a_flipped_shade_is_turned_round() {
ops.shades[4] = flippedShade();
shade_request_t req = request(shade_request_type_t::set_favorite);
req.position = 30;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 70 -1", ops.calls.c_str());
}
static void test_favorite_without_a_tilt_keeps_the_favorite_tilt() {
// MQTT mypos used to set the favorite tilt to 0 here.
ops.shades[4] = tiltShade(tilt_types::integrated);
ops.shades[4].myTiltPos = 40.0f;
shade_request_t req = request(shade_request_type_t::set_favorite);
req.position = 30;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 30 40", ops.calls.c_str());
}
static void test_favorite_tilt_alone_keeps_the_favorite_lift() {
ops.shades[4] = tiltShade(tilt_types::integrated);
ops.shades[4].myPos = 60.0f;
ops.shades[4].currentPos = 20.0f;
shade_request_t req = request(shade_request_type_t::set_favorite);
req.tiltPosition = 25;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 60 25", ops.calls.c_str());
}
static void test_favorite_tilt_alone_without_a_favorite_uses_where_the_shade_is() {
ops.shades[4] = tiltShade(tilt_types::integrated);
ops.shades[4].currentPos = 20.0f;
shade_request_t req = request(shade_request_type_t::set_favorite);
req.tiltPosition = 25;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 20 25", ops.calls.c_str());
}
static void test_favorite_tilt_on_a_flipped_shade_is_turned_round() {
ops.shades[4] = flippedShade();
ops.shades[4].tiltType = tilt_types::integrated;
shade_request_t req = request(shade_request_type_t::set_favorite);
req.position = 30;
req.tiltPosition = 10;
requests.run(req);
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 70 90", ops.calls.c_str());
}
/*********************************************************************
* Pairing and rolling codes
********************************************************************/
static void test_unpair_holds_prog_longer_on_a_56_bit_motor() {
ops.shades[4] = rollerShade();
ops.shades[5] = rollerShade();
ops.shades[5].bitLength = 80;
requests.run(request(shade_request_type_t::unpair, 4));
requests.run(request(shade_request_type_t::unpair, 5));
TEST_ASSERT_EQUAL_STRING(
"sendCommand 4 Prog repeat=7 step=0; setUnpaired 4; sendCommand 5 Prog repeat=1 step=0; setUnpaired 5",
ops.calls.c_str());
}
static void test_unpair_the_radio_refused_leaves_the_shade_paired() {
ops.shades[4] = rollerShade();
ops.radioTakes = false;
TEST_ASSERT_FALSE(requests.run(request(shade_request_type_t::unpair)));
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Prog repeat=7 step=0", ops.calls.c_str());
}
static void test_rolling_code_is_set() {
ops.shades[4] = rollerShade();
shade_request_t req = request(shade_request_type_t::set_rolling_code);
req.rollingCode = 1234;
TEST_ASSERT_TRUE(requests.run(req));
TEST_ASSERT_EQUAL_STRING("setRollingCode 4 1234", ops.calls.c_str());
}
/*********************************************************************
* The radio refusing
********************************************************************/
static void test_a_refused_command_is_reported() {
ops.shades[4] = rollerShade();
ops.radioTakes = false;
shade_request_t req = request(shade_request_type_t::shade_command);
req.target = 50;
TEST_ASSERT_FALSE(requests.run(req));
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_a_missing_shade_is_dropped);
RUN_TEST(test_a_missing_group_is_dropped);
RUN_TEST(test_a_shade_id_is_not_a_group_id);
RUN_TEST(test_target_moves_a_shade_there);
RUN_TEST(test_target_on_a_flipped_shade_is_turned_round);
RUN_TEST(test_tilt_target_on_a_flipped_shade_is_turned_round);
RUN_TEST(test_without_a_target_the_command_is_sent);
RUN_TEST(test_tilt_without_a_target_sends_the_tilt_command);
RUN_TEST(test_set_positions_on_a_flipped_shade_are_turned_round);
RUN_TEST(test_set_positions_leaves_the_tilt_of_a_shade_without_one);
RUN_TEST(test_set_positions_with_nothing_in_range_does_nothing);
RUN_TEST(test_client_values_are_checked_on_the_way_in);
RUN_TEST(test_no_repeat_means_the_shades_own);
RUN_TEST(test_repeat_zero_means_no_repeats_for_a_shade_too);
RUN_TEST(test_no_repeat_means_the_groups_own);
RUN_TEST(test_repeat_sends_a_new_command_when_it_was_not_the_last);
RUN_TEST(test_repeat_repeats_the_last_frame);
RUN_TEST(test_prog_on_a_one_button_garage_door_is_toggle);
RUN_TEST(test_group_repeat_repeats_the_last_frame);
RUN_TEST(test_sensor_sends_what_was_given);
RUN_TEST(test_group_sensor_uses_the_groups_repeats);
RUN_TEST(test_favorite_on_a_flipped_shade_is_turned_round);
RUN_TEST(test_favorite_without_a_tilt_keeps_the_favorite_tilt);
RUN_TEST(test_favorite_tilt_alone_keeps_the_favorite_lift);
RUN_TEST(test_favorite_tilt_alone_without_a_favorite_uses_where_the_shade_is);
RUN_TEST(test_favorite_tilt_on_a_flipped_shade_is_turned_round);
RUN_TEST(test_unpair_holds_prog_longer_on_a_56_bit_motor);
RUN_TEST(test_unpair_the_radio_refused_leaves_the_shade_paired);
RUN_TEST(test_rolling_code_is_set);
RUN_TEST(test_a_refused_command_is_reported);
return UNITY_END();
}

View file

@ -1,62 +0,0 @@
#include <unity.h>
#include "WebCommand.h"
static const web_cmd_t RADIO_COMMANDS[] = {
web_cmd_t::shade_command, web_cmd_t::group_command, web_cmd_t::tilt_command,
web_cmd_t::shade_repeat, web_cmd_t::group_repeat, web_cmd_t::shade_sensor,
web_cmd_t::group_sensor, web_cmd_t::set_my_position, web_cmd_t::unpair_shade
};
void setUp() {}
void tearDown() {}
static const web_cmd_t LOOP_COMMANDS[] = {
web_cmd_t::set_positions, web_cmd_t::set_rolling_code, web_cmd_t::restore_shades, web_cmd_t::load_shades
};
static void test_only_loop_commands_stay_off_the_radio() {
int offRadio = 0;
for(uint8_t t = 0; t <= static_cast<uint8_t>(web_cmd_t::load_shades); t++) {
if(!webCommandUsesRadio(static_cast<web_cmd_t>(t))) offRadio++;
}
TEST_ASSERT_EQUAL_INT(4, offRadio);
for(web_cmd_t t : LOOP_COMMANDS) TEST_ASSERT_FALSE(webCommandUsesRadio(t));
}
static void test_loop_commands_are_never_held() {
// Nothing goes on air, so a full or suspended radio doesn't matter.
for(web_cmd_t t : LOOP_COMMANDS) {
TEST_ASSERT_FALSE(holdWebCommand(t, false, false));
TEST_ASSERT_FALSE(holdWebCommand(t, true, false));
}
}
static void test_radio_command_waits_for_room() {
for(web_cmd_t t : RADIO_COMMANDS) {
TEST_ASSERT_TRUE(holdWebCommand(t, false, false));
TEST_ASSERT_FALSE(holdWebCommand(t, false, true));
}
}
static void test_commands_are_not_held_while_suspended() {
// Nothing goes on air during a frequency scan or OTA anyway.
for(web_cmd_t t : RADIO_COMMANDS) {
if(t == web_cmd_t::unpair_shade) continue;
TEST_ASSERT_FALSE(holdWebCommand(t, true, false));
TEST_ASSERT_FALSE(holdWebCommand(t, true, true));
}
}
static void test_unpair_waits_out_a_suspend() {
// The client was already told the shade is unpaired.
TEST_ASSERT_TRUE(holdWebCommand(web_cmd_t::unpair_shade, true, false));
TEST_ASSERT_TRUE(holdWebCommand(web_cmd_t::unpair_shade, true, true));
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_only_loop_commands_stay_off_the_radio);
RUN_TEST(test_loop_commands_are_never_held);
RUN_TEST(test_radio_command_waits_for_room);
RUN_TEST(test_commands_are_not_held_while_suspended);
RUN_TEST(test_unpair_waits_out_a_suspend);
return UNITY_END();
}