fix: command op replies that said ok when nothing was done

From the review of #100:
- setMyPosition on a moving shade replies busy ("The shade is moving"),
  and setPositions with only a tilt, for a shade without one, replies
  bad request ("The shade has no tilt"). Both said ok before.
- PROTOCOL.md lists where the ops differ from the routes, and how
  sendRemoteCommand is answered.
- The LoopQueue hold comment covers protocol 2 replies and an unpair
  held through a frequency scan.
- One shadeRequestIsForGroup() for the group cases; ProtoOps shares one
  reader for "one or both" fields and one for ids, and the radio
  refused text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-24 12:49:23 +02:00
parent 598d708b12
commit 5737a58790
12 changed files with 103 additions and 49 deletions

View file

@ -13,6 +13,6 @@ Runs against a real device on your network, by hand, never in CI. It checks sock
| `--radio --shade <id>` | also send real commands: that shade moves a little and back |
| `--record` | save hello, the sync and the schema to `test/fixtures/proto2/`, for the web UI and Home Assistant tests |
A plain run sends nothing on air, changes no settings and doesn't reboot the device. The command tests send ops that are refused before they reach the radio, and one `setPositions` that sets a shade to the position it already has. It does fill every socket slot for a few seconds, and replays a page load.
A plain run sends nothing on air, changes no settings and doesn't reboot the device. The command tests send ops that are refused before they reach the radio, and one `setPositions` that sets a shade to the position the sync gave: that drops the fraction of a percent the shade's believed position may have had. It does fill every socket slot for a few seconds, and replays a page load.
The tests run in whatever security mode the device is in; the suite never changes it. To test both modes, run the suite once in each.

View file

@ -215,6 +215,12 @@ static void test_request_results_become_replies() {
TEST_ASSERT_EQUAL_STRING("No shade with that id", protoRequestErrorDesc(shade, shade_request_result_t::not_found));
TEST_ASSERT_EQUAL_STRING("No group with that id", protoRequestErrorDesc(group, shade_request_result_t::not_found));
TEST_ASSERT_EQUAL(proto_error_t::radioUnavailable, protoRequestError(shade_request_result_t::radio_refused));
TEST_ASSERT_EQUAL_STRING("The radio didn't take the command", protoRequestErrorDesc(shade, shade_request_result_t::radio_refused));
// A favorite waits for the shade to stop: the client may try again.
TEST_ASSERT_EQUAL(proto_error_t::busy, protoRequestError(shade_request_result_t::shade_moving));
TEST_ASSERT_EQUAL_STRING("The shade is moving", protoRequestErrorDesc(shade, shade_request_result_t::shade_moving));
TEST_ASSERT_EQUAL(proto_error_t::badRequest, protoRequestError(shade_request_result_t::no_tilt));
TEST_ASSERT_EQUAL_STRING("The shade has no tilt", protoRequestErrorDesc(shade, shade_request_result_t::no_tilt));
}
int main(int argc, char **argv) {

View file

@ -174,6 +174,13 @@ static void test_set_positions_leaves_the_tilt_of_a_shade_without_one() {
requests.run(req);
TEST_ASSERT_EQUAL_STRING("setPositions 4 20 -1", ops.calls.c_str());
}
static void test_set_positions_tilt_alone_on_a_shade_without_one_is_refused() {
ops.shades[4] = rollerShade();
shade_request_t req = request(shade_request_type_t::set_positions);
req.tiltPosition = 40;
TEST_ASSERT_EQUAL(shade_request_result_t::no_tilt, requests.run(req));
TEST_ASSERT_EQUAL_STRING("", 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);
@ -279,6 +286,21 @@ static void test_group_sensor_uses_the_groups_repeats() {
/*********************************************************************
* The favorite
********************************************************************/
static void test_favorite_on_a_moving_shade_is_refused() {
ops.shades[4] = rollerShade();
ops.shades[4].idle = false;
shade_request_t req = request(shade_request_type_t::set_favorite);
req.position = 50;
TEST_ASSERT_EQUAL(shade_request_result_t::shade_moving, requests.run(req));
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
}
static void test_group_requests_take_a_group_id() {
TEST_ASSERT_TRUE(shadeRequestIsForGroup(shade_request_type_t::group_command));
TEST_ASSERT_TRUE(shadeRequestIsForGroup(shade_request_type_t::group_repeat));
TEST_ASSERT_TRUE(shadeRequestIsForGroup(shade_request_type_t::group_sensor));
TEST_ASSERT_FALSE(shadeRequestIsForGroup(shade_request_type_t::shade_sensor));
TEST_ASSERT_FALSE(shadeRequestIsForGroup(shade_request_type_t::unpair));
}
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);
@ -374,6 +396,7 @@ int main(int argc, char **argv) {
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_tilt_alone_on_a_shade_without_one_is_refused);
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);
@ -385,6 +408,8 @@ int main(int argc, char **argv) {
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_moving_shade_is_refused);
RUN_TEST(test_group_requests_take_a_group_id);
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);