mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
feat: command ops for socket protocol 2
shadeCommand, groupCommand, tiltCommand, repeatCommand, setPositions, setSensor, setMyPosition, unpairShade, setRollingCode (types 16-24) are read on async_tcp by ProtoOps and handed to loop() through the LoopQueue, which replies once the request has run: ok, not found or radio unavailable. A bad payload, a paused radio or a full queue is answered straight away. sendRemoteCommand (25) touches nothing of the shade model, so it goes to the radio from async_tcp like its route. ShadeRequests::run() now says how a request went, not only whether the radio took it. An op reads only the fields that were sent, so setPositions can leave the tilt as it is. The shadeCommand event is renamed shadeCommanded, since the op has the route's name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9fe05105ff
commit
0b54264ee3
18 changed files with 812 additions and 50 deletions
|
|
@ -10,8 +10,9 @@ Runs against a real device on your network, by hand, never in CI. It checks sock
|
|||
|---|---|
|
||||
| `--device` | the device's address (required) |
|
||||
| `--password`, `--username` / `--pin` | the credentials of a Secured device |
|
||||
| `--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. 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 it already has. 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.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ def pytest_addoption(parser):
|
|||
parser.addoption("--username", default="", help="the username that goes with --password")
|
||||
parser.addoption("--pin", default=None, help="the device's PIN, when it is Secured with one")
|
||||
parser.addoption("--record", action="store_true", help="save what the device sends to test/fixtures/proto2")
|
||||
parser.addoption("--radio", action="store_true", help="also run the tests that send commands on air")
|
||||
parser.addoption("--shade", type=int, default=None, help="with --radio: the id of the shade the commands may move")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
|
@ -82,3 +84,14 @@ def schema(host, token):
|
|||
@pytest.fixture(scope="session")
|
||||
def recording(request):
|
||||
return request.config.getoption("--record")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def radio_shade(request):
|
||||
"""The shade --radio tests may move, or a skip."""
|
||||
if not request.config.getoption("--radio"):
|
||||
pytest.skip("sends on air: pass --radio --shade <id>")
|
||||
shade = request.config.getoption("--shade")
|
||||
if shade is None:
|
||||
pytest.skip("--radio needs --shade <id>, a shade that may move")
|
||||
return shade
|
||||
|
|
|
|||
112
test/e2e/test_commands.py
Normal file
112
test/e2e/test_commands.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"""The command ops (docs/PROTOCOL.md). Without --radio nothing goes on air:
|
||||
the ops are refused before they reach the radio, or send nothing anyway."""
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from proto_client import run_sync
|
||||
from test_messages import error_name, reply_to
|
||||
|
||||
COMMAND_OPS = ("shadeCommand", "groupCommand", "tiltCommand", "repeatCommand", "setPositions", "setSensor",
|
||||
"setMyPosition", "unpairShade", "setRollingCode", "sendRemoteCommand")
|
||||
|
||||
|
||||
def pack(schema, fields):
|
||||
"""A payload by field names, in short keys."""
|
||||
return {schema.keys[name]: value for name, value in fields.items()}
|
||||
|
||||
|
||||
def synced_model(connect, schema):
|
||||
"""A synced client and the shades and groups it got, by name."""
|
||||
c = connect(schema=schema.hash)
|
||||
_, frames, _ = run_sync(c)
|
||||
shades, groups = [], []
|
||||
for f in frames:
|
||||
shades.extend(schema.expand(s) for s in f["d"].get(schema.keys["shades"], []))
|
||||
groups.extend(schema.expand(g) for g in f["d"].get(schema.keys["groups"], []))
|
||||
return c, shades, groups
|
||||
|
||||
|
||||
def op(c, schema, name, fields):
|
||||
"""Sends the op and returns its reply."""
|
||||
msg_id = c.send(schema.type_numbers[name], pack(schema, fields), with_id=True)
|
||||
return reply_to(c, msg_id)
|
||||
|
||||
|
||||
def is_ok(schema, reply):
|
||||
return reply["d"].get(schema.keys["ok"]) == 1
|
||||
|
||||
|
||||
def test_every_command_op_is_in_the_schema(schema):
|
||||
for name in COMMAND_OPS:
|
||||
assert 16 <= schema.type_numbers[name] <= 127, name
|
||||
|
||||
|
||||
def test_an_op_without_an_id_is_a_bad_request(connect, schema):
|
||||
c, _, _ = synced_model(connect, schema)
|
||||
c.send(schema.type_numbers["shadeCommand"], pack(schema, {"shadeId": 1, "command": "My"}))
|
||||
reply = c.recv_until(lambda f: f["t"] == 2)[-1]
|
||||
assert "id" not in reply
|
||||
assert error_name(schema, reply) == "badRequest"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, fields, why", [
|
||||
("shadeCommand", {}, "shadeId is missing"),
|
||||
("shadeCommand", {"shadeId": 1, "command": "Upp"}, "Not a command"),
|
||||
("shadeCommand", {"shadeId": 1}, "Give command or target"),
|
||||
("shadeCommand", {"shadeId": 1, "target": 101}, "target must be 0-100"),
|
||||
("tiltCommand", {"shadeId": 1, "command": "Up", "target": 5}, "Give command or target"),
|
||||
("groupCommand", {"groupId": 1}, "command is missing"),
|
||||
("repeatCommand", {"command": "Up"}, "Give shadeId or groupId"),
|
||||
("setPositions", {"shadeId": 1}, "Give position, tiltPosition or both"),
|
||||
("setSensor", {"shadeId": 1}, "Give sunny, windy or both"),
|
||||
("setMyPosition", {"shadeId": 1, "myPos": 101}, "myPos must be 0-100"),
|
||||
("unpairShade", {}, "shadeId is missing"),
|
||||
("setRollingCode", {"shadeId": 1}, "lastRollingCode is missing"),
|
||||
("sendRemoteCommand", {"command": "Up", "lastRollingCode": 1}, "remoteAddress is missing"),
|
||||
])
|
||||
def test_a_bad_payload_is_refused_with_why(connect, schema, name, fields, why):
|
||||
c, _, _ = synced_model(connect, schema)
|
||||
reply = op(c, schema, name, fields)
|
||||
assert error_name(schema, reply) == "badRequest"
|
||||
assert reply["d"][schema.keys["desc"]] == why
|
||||
|
||||
|
||||
def test_a_shade_or_group_that_does_not_exist_is_not_found(connect, schema):
|
||||
c, shades, groups = synced_model(connect, schema)
|
||||
shade = next(i for i in range(254, 0, -1) if i not in {s["shadeId"] for s in shades})
|
||||
group = next(i for i in range(254, 0, -1) if i not in {g["groupId"] for g in groups})
|
||||
reply = op(c, schema, "shadeCommand", {"shadeId": shade, "command": "Up"})
|
||||
assert error_name(schema, reply) == "notFound"
|
||||
assert reply["d"][schema.keys["desc"]] == "No shade with that id"
|
||||
reply = op(c, schema, "groupCommand", {"groupId": group, "command": "Up"})
|
||||
assert error_name(schema, reply) == "notFound"
|
||||
assert reply["d"][schema.keys["desc"]] == "No group with that id"
|
||||
|
||||
|
||||
def test_set_positions_where_the_shade_already_is(connect, schema):
|
||||
"""setPositions sends nothing on air. Setting the position the sync gave
|
||||
moves the shade's believed position by less than 1%, its fraction."""
|
||||
c, shades, _ = synced_model(connect, schema)
|
||||
if not shades:
|
||||
pytest.skip("the device has no shades")
|
||||
shade = shades[0]
|
||||
reply = op(c, schema, "setPositions", {"shadeId": shade["shadeId"], "position": shade.get("position", 0)})
|
||||
assert is_ok(schema, reply)
|
||||
|
||||
|
||||
def test_a_command_moves_the_shade_and_replies_ok(connect, schema, radio_shade):
|
||||
"""Moves the --shade a little and back to where it was."""
|
||||
c, shades, _ = synced_model(connect, schema)
|
||||
shade = next((s for s in shades if s["shadeId"] == radio_shade), None)
|
||||
assert shade, "no shade %s on the device" % radio_shade
|
||||
start = shade.get("position", 0)
|
||||
reply = op(c, schema, "shadeCommand", {"shadeId": radio_shade, "command": "Down" if start < 50 else "Up"})
|
||||
assert is_ok(schema, reply)
|
||||
state = schema.type_numbers["shadeState"]
|
||||
moving = c.recv_until(lambda f: f["t"] == state and f["d"].get(schema.keys["shadeId"]) == radio_shade
|
||||
and f["d"].get(schema.keys["direction"], 0) != 0, timeout=10)[-1]
|
||||
assert moving
|
||||
time.sleep(2)
|
||||
reply = op(c, schema, "shadeCommand", {"shadeId": radio_shade, "target": start})
|
||||
assert is_ok(schema, reply)
|
||||
|
|
@ -45,6 +45,11 @@ static void test_type_numbers_are_unique_and_in_their_sender_range() {
|
|||
else TEST_ASSERT_TRUE_MESSAGE(t.number >= 1, t.name);
|
||||
}
|
||||
}
|
||||
static void test_type_names_are_unique() {
|
||||
// Clients look types up by name.
|
||||
std::set<std::string> names;
|
||||
for(uint8_t i = 0; i < protoTypeCount(); i++) TEST_ASSERT_TRUE_MESSAGE(names.insert(protoTypeAt(i).name).second, protoTypeAt(i).name);
|
||||
}
|
||||
static void test_error_and_feed_numbers_are_unique() {
|
||||
std::set<uint8_t> errors, feeds;
|
||||
for(uint8_t i = 0; i < protoErrorCount(); i++) TEST_ASSERT_TRUE(errors.insert(protoErrorAt(i).number).second);
|
||||
|
|
@ -380,6 +385,7 @@ int main(int argc, char **argv) {
|
|||
RUN_TEST(test_field_keys_and_names_are_unique);
|
||||
RUN_TEST(test_every_field_has_a_known_kind);
|
||||
RUN_TEST(test_type_numbers_are_unique_and_in_their_sender_range);
|
||||
RUN_TEST(test_type_names_are_unique);
|
||||
RUN_TEST(test_error_and_feed_numbers_are_unique);
|
||||
RUN_TEST(test_bootstrap_keys_are_fixed);
|
||||
RUN_TEST(test_schema_hash_is_eight_hex_digits_and_stable);
|
||||
|
|
|
|||
246
test/test_protoops/test_protoops.cpp
Normal file
246
test/test_protoops/test_protoops.cpp
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
#include <unity.h>
|
||||
#include <string.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "ProtoOps.h"
|
||||
|
||||
// Socket protocol 2's command ops: what each payload asks for, and what is
|
||||
// refused before anything reaches loop() (docs/PROTOCOL.md).
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static JsonDocument doc;
|
||||
static shade_request_t req;
|
||||
|
||||
static const char *parse(proto_type_t type, const char *payload) {
|
||||
doc.clear();
|
||||
TEST_ASSERT_EQUAL(DeserializationError::Ok, deserializeJson(doc, payload).code());
|
||||
return parseShadeOp(static_cast<uint8_t>(type), doc.as<JsonObjectConst>(), req);
|
||||
}
|
||||
#define TEST_ASSERT_GOOD(why) TEST_ASSERT_NULL_MESSAGE(why, why)
|
||||
|
||||
/*********************************************************************
|
||||
* Which ops are shade requests
|
||||
********************************************************************/
|
||||
static void test_every_command_op_but_the_remote_frame_is_a_shade_request() {
|
||||
const proto_type_t shadeOps[] = {
|
||||
proto_type_t::shadeCommand, proto_type_t::groupCommand, proto_type_t::tiltCommand, proto_type_t::repeatCommand,
|
||||
proto_type_t::setPositions, proto_type_t::setSensor, proto_type_t::setMyPosition, proto_type_t::unpairShade,
|
||||
proto_type_t::setRollingCode
|
||||
};
|
||||
for(proto_type_t t : shadeOps) TEST_ASSERT_TRUE(protoIsShadeOp(static_cast<uint8_t>(t)));
|
||||
TEST_ASSERT_FALSE(protoIsShadeOp(static_cast<uint8_t>(proto_type_t::sendRemoteCommand)));
|
||||
TEST_ASSERT_FALSE(protoIsShadeOp(static_cast<uint8_t>(proto_type_t::subscribe)));
|
||||
TEST_ASSERT_FALSE(protoIsShadeOp(static_cast<uint8_t>(proto_type_t::shadeCommanded)));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Commands
|
||||
********************************************************************/
|
||||
static void test_shade_command_by_name() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::shadeCommand, R"({"s":3,"cm":"Down"})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::shade_command, req.type);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, req.id);
|
||||
TEST_ASSERT_EQUAL(somfy_commands::Down, req.command);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.target);
|
||||
// Left out: the shade's own repeats.
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.repeat);
|
||||
}
|
||||
static void test_shade_command_to_a_target() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::shadeCommand, R"({"s":3,"t":0})"));
|
||||
TEST_ASSERT_EQUAL_INT8(0, req.target);
|
||||
}
|
||||
static void test_shade_command_with_repeat_and_step() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::shadeCommand, R"({"s":3,"cm":"StepUp","re":0,"z":5})"));
|
||||
TEST_ASSERT_EQUAL(somfy_commands::StepUp, req.command);
|
||||
TEST_ASSERT_EQUAL_INT8(0, req.repeat);
|
||||
TEST_ASSERT_EQUAL_UINT8(5, req.stepSize);
|
||||
}
|
||||
static void test_shade_command_needs_a_command_or_a_target_not_both() {
|
||||
TEST_ASSERT_EQUAL_STRING("Give command or target", parse(proto_type_t::shadeCommand, R"({"s":3})"));
|
||||
TEST_ASSERT_EQUAL_STRING("Give command or target", parse(proto_type_t::shadeCommand, R"({"s":3,"cm":"Up","t":10})"));
|
||||
}
|
||||
static void test_shade_command_values_are_checked() {
|
||||
TEST_ASSERT_EQUAL_STRING("shadeId is missing", parse(proto_type_t::shadeCommand, R"({"cm":"Up"})"));
|
||||
TEST_ASSERT_EQUAL_STRING("shadeId must be 0-255", parse(proto_type_t::shadeCommand, R"({"s":"3","cm":"Up"})"));
|
||||
TEST_ASSERT_EQUAL_STRING("target must be 0-100", parse(proto_type_t::shadeCommand, R"({"s":3,"t":101})"));
|
||||
TEST_ASSERT_EQUAL_STRING("Not a command", parse(proto_type_t::shadeCommand, R"({"s":3,"cm":"Upp"})"));
|
||||
TEST_ASSERT_EQUAL_STRING("Not a command", parse(proto_type_t::shadeCommand, R"({"s":3,"cm":2})"));
|
||||
TEST_ASSERT_EQUAL_STRING("repeat must be 0-127", parse(proto_type_t::shadeCommand, R"({"s":3,"cm":"Up","re":-1})"));
|
||||
TEST_ASSERT_EQUAL_STRING("stepSize must be 0-127", parse(proto_type_t::shadeCommand, R"({"s":3,"cm":"Up","z":128})"));
|
||||
}
|
||||
static void test_only_the_first_problem_is_reported() {
|
||||
TEST_ASSERT_EQUAL_STRING("shadeId is missing", parse(proto_type_t::shadeCommand, R"({"t":101,"re":-1})"));
|
||||
}
|
||||
static void test_a_left_out_payload_is_missing_everything() {
|
||||
doc.clear();
|
||||
TEST_ASSERT_EQUAL_STRING("shadeId is missing", parseShadeOp(static_cast<uint8_t>(proto_type_t::unpairShade), JsonObjectConst(), req));
|
||||
}
|
||||
static void test_tilt_command() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::tiltCommand, R"({"s":2,"t":40})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::tilt_command, req.type);
|
||||
TEST_ASSERT_EQUAL_INT8(40, req.target);
|
||||
}
|
||||
static void test_group_command_needs_a_command() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::groupCommand, R"({"g":1,"cm":"my","re":4})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::group_command, req.type);
|
||||
TEST_ASSERT_EQUAL_UINT8(1, req.id);
|
||||
TEST_ASSERT_EQUAL(somfy_commands::My, req.command);
|
||||
TEST_ASSERT_EQUAL_INT8(4, req.repeat);
|
||||
TEST_ASSERT_EQUAL_STRING("command is missing", parse(proto_type_t::groupCommand, R"({"g":1,"t":10})"));
|
||||
}
|
||||
static void test_repeat_takes_a_shade_or_a_group() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::repeatCommand, R"({"s":5,"cm":"Prog"})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::shade_repeat, req.type);
|
||||
TEST_ASSERT_EQUAL_UINT8(5, req.id);
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::repeatCommand, R"({"g":2,"cm":"Up"})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::group_repeat, req.type);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, req.id);
|
||||
TEST_ASSERT_EQUAL_STRING("Give shadeId or groupId", parse(proto_type_t::repeatCommand, R"({"s":5,"g":2,"cm":"Up"})"));
|
||||
TEST_ASSERT_EQUAL_STRING("Give shadeId or groupId", parse(proto_type_t::repeatCommand, R"({"cm":"Up"})"));
|
||||
}
|
||||
static void test_command_names_read_back_as_shade_commanded_reports_them() {
|
||||
somfy_commands cmd;
|
||||
TEST_ASSERT_TRUE(protoCommand("Step Down", cmd));
|
||||
TEST_ASSERT_EQUAL(somfy_commands::StepDown, cmd);
|
||||
TEST_ASSERT_TRUE(protoCommand("My+Up+Down", cmd));
|
||||
TEST_ASSERT_EQUAL(somfy_commands::MyUpDown, cmd);
|
||||
TEST_ASSERT_TRUE(protoCommand("SUNFLAG", cmd));
|
||||
TEST_ASSERT_EQUAL(somfy_commands::SunFlag, cmd);
|
||||
// The HTTP routes' guesses from the first letter are not names.
|
||||
TEST_ASSERT_FALSE(protoCommand("u", cmd));
|
||||
TEST_ASSERT_FALSE(protoCommand("", cmd));
|
||||
TEST_ASSERT_FALSE(protoCommand(nullptr, cmd));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Positions, sensors, favorites, pairing
|
||||
********************************************************************/
|
||||
static void test_set_positions_leaves_out_what_was_not_given() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::setPositions, R"({"s":1,"p":0})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::set_positions, req.type);
|
||||
TEST_ASSERT_EQUAL_INT8(0, req.position);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.tiltPosition);
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::setPositions, R"({"s":1,"q":30})"));
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.position);
|
||||
TEST_ASSERT_EQUAL_INT8(30, req.tiltPosition);
|
||||
TEST_ASSERT_EQUAL_STRING("Give position, tiltPosition or both", parse(proto_type_t::setPositions, R"({"s":1})"));
|
||||
TEST_ASSERT_EQUAL_STRING("position must be 0-100", parse(proto_type_t::setPositions, R"({"s":1,"p":-1})"));
|
||||
}
|
||||
static void test_sensor_leaves_out_what_was_not_given() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::setSensor, R"({"g":3,"sn":1})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::group_sensor, req.type);
|
||||
TEST_ASSERT_EQUAL_INT8(1, req.sunny);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.windy);
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::setSensor, R"({"s":3,"wn":0,"re":2})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::shade_sensor, req.type);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.sunny);
|
||||
TEST_ASSERT_EQUAL_INT8(0, req.windy);
|
||||
TEST_ASSERT_EQUAL_INT8(2, req.repeat);
|
||||
TEST_ASSERT_EQUAL_STRING("Give sunny, windy or both", parse(proto_type_t::setSensor, R"({"s":3})"));
|
||||
// Booleans are 1 and 0.
|
||||
TEST_ASSERT_EQUAL_STRING("sunny must be 0-1", parse(proto_type_t::setSensor, R"({"s":3,"sn":true})"));
|
||||
}
|
||||
static void test_favorite() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::setMyPosition, R"({"s":4,"m":50,"M":20})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::set_favorite, req.type);
|
||||
TEST_ASSERT_EQUAL_INT8(50, req.position);
|
||||
TEST_ASSERT_EQUAL_INT8(20, req.tiltPosition);
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::setMyPosition, R"({"s":4,"M":20})"));
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.position);
|
||||
TEST_ASSERT_EQUAL_STRING("Give myPos, myTiltPos or both", parse(proto_type_t::setMyPosition, R"({"s":4})"));
|
||||
}
|
||||
static void test_unpair_and_rolling_code() {
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::unpairShade, R"({"s":6})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::unpair, req.type);
|
||||
TEST_ASSERT_EQUAL_UINT8(6, req.id);
|
||||
TEST_ASSERT_GOOD(parse(proto_type_t::setRollingCode, R"({"s":6,"c":0})"));
|
||||
TEST_ASSERT_EQUAL(shade_request_type_t::set_rolling_code, req.type);
|
||||
TEST_ASSERT_EQUAL_UINT16(0, req.rollingCode);
|
||||
TEST_ASSERT_EQUAL_STRING("lastRollingCode is missing", parse(proto_type_t::setRollingCode, R"({"s":6})"));
|
||||
TEST_ASSERT_EQUAL_STRING("lastRollingCode must be 0-65535", parse(proto_type_t::setRollingCode, R"({"s":6,"c":65536})"));
|
||||
}
|
||||
static void test_a_type_that_is_not_a_shade_op() {
|
||||
TEST_ASSERT_EQUAL_STRING("Not a shade op", parse(proto_type_t::sendRemoteCommand, R"({})"));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* A frame as a remote
|
||||
********************************************************************/
|
||||
static void test_remote_command() {
|
||||
doc.clear();
|
||||
deserializeJson(doc, R"({"a":1193046,"cm":"Up","c":42,"ek":160,"rp":3})");
|
||||
somfy_frame_t frame;
|
||||
uint8_t repeats = 9;
|
||||
TEST_ASSERT_GOOD(parseRemoteCommandOp(doc.as<JsonObjectConst>(), frame, repeats));
|
||||
TEST_ASSERT_EQUAL_UINT32(0x123456, frame.remoteAddress);
|
||||
TEST_ASSERT_EQUAL(somfy_commands::Up, frame.cmd);
|
||||
TEST_ASSERT_EQUAL_UINT16(42, frame.rollingCode);
|
||||
TEST_ASSERT_EQUAL_UINT8(160, frame.encKey);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, repeats);
|
||||
}
|
||||
static void test_remote_command_defaults() {
|
||||
doc.clear();
|
||||
deserializeJson(doc, R"({"a":1,"cm":"My","c":1})");
|
||||
somfy_frame_t frame;
|
||||
uint8_t repeats = 9;
|
||||
TEST_ASSERT_GOOD(parseRemoteCommandOp(doc.as<JsonObjectConst>(), frame, repeats));
|
||||
TEST_ASSERT_EQUAL_UINT8(0xA7, frame.encKey);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, repeats);
|
||||
}
|
||||
static void test_remote_command_needs_an_address_and_a_rolling_code() {
|
||||
somfy_frame_t frame;
|
||||
uint8_t repeats;
|
||||
doc.clear();
|
||||
deserializeJson(doc, R"({"cm":"Up","c":1})");
|
||||
TEST_ASSERT_EQUAL_STRING("remoteAddress is missing", parseRemoteCommandOp(doc.as<JsonObjectConst>(), frame, repeats));
|
||||
doc.clear();
|
||||
deserializeJson(doc, R"({"a":0,"cm":"Up","c":1})");
|
||||
TEST_ASSERT_EQUAL_STRING("remoteAddress must be 1-16777215", parseRemoteCommandOp(doc.as<JsonObjectConst>(), frame, repeats));
|
||||
doc.clear();
|
||||
deserializeJson(doc, R"({"a":5,"cm":"Up","c":0})");
|
||||
TEST_ASSERT_EQUAL_STRING("lastRollingCode must be 1-65535", parseRemoteCommandOp(doc.as<JsonObjectConst>(), frame, repeats));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Replies once loop() has run a request
|
||||
********************************************************************/
|
||||
static void test_request_results_become_replies() {
|
||||
shade_request_t shade;
|
||||
shade_request_t group;
|
||||
group.type = shade_request_type_t::group_sensor;
|
||||
TEST_ASSERT_EQUAL(proto_error_t::none, protoRequestError(shade_request_result_t::done));
|
||||
TEST_ASSERT_NULL(protoRequestErrorDesc(shade, shade_request_result_t::done));
|
||||
TEST_ASSERT_EQUAL(proto_error_t::notFound, protoRequestError(shade_request_result_t::not_found));
|
||||
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));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_every_command_op_but_the_remote_frame_is_a_shade_request);
|
||||
RUN_TEST(test_shade_command_by_name);
|
||||
RUN_TEST(test_shade_command_to_a_target);
|
||||
RUN_TEST(test_shade_command_with_repeat_and_step);
|
||||
RUN_TEST(test_shade_command_needs_a_command_or_a_target_not_both);
|
||||
RUN_TEST(test_shade_command_values_are_checked);
|
||||
RUN_TEST(test_only_the_first_problem_is_reported);
|
||||
RUN_TEST(test_a_left_out_payload_is_missing_everything);
|
||||
RUN_TEST(test_tilt_command);
|
||||
RUN_TEST(test_group_command_needs_a_command);
|
||||
RUN_TEST(test_repeat_takes_a_shade_or_a_group);
|
||||
RUN_TEST(test_command_names_read_back_as_shade_commanded_reports_them);
|
||||
RUN_TEST(test_set_positions_leaves_out_what_was_not_given);
|
||||
RUN_TEST(test_sensor_leaves_out_what_was_not_given);
|
||||
RUN_TEST(test_favorite);
|
||||
RUN_TEST(test_unpair_and_rolling_code);
|
||||
RUN_TEST(test_a_type_that_is_not_a_shade_op);
|
||||
RUN_TEST(test_remote_command);
|
||||
RUN_TEST(test_remote_command_defaults);
|
||||
RUN_TEST(test_remote_command_needs_an_address_and_a_rolling_code);
|
||||
RUN_TEST(test_request_results_become_replies);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
@ -100,19 +100,19 @@ void tearDown() {}
|
|||
/*********************************************************************
|
||||
* Finding the shade or group
|
||||
********************************************************************/
|
||||
static void test_a_missing_shade_is_dropped() {
|
||||
static void test_a_missing_shade_is_not_found() {
|
||||
shade_request_t req = request(shade_request_type_t::shade_command);
|
||||
req.target = 30;
|
||||
TEST_ASSERT_TRUE(requests.run(req));
|
||||
TEST_ASSERT_EQUAL(shade_request_result_t::not_found, 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)));
|
||||
static void test_a_missing_group_is_not_found() {
|
||||
TEST_ASSERT_EQUAL(shade_request_result_t::not_found, 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(shade_request_result_t::not_found, requests.run(request(shade_request_type_t::group_command, 2)));
|
||||
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ static void test_set_positions_with_nothing_in_range_does_nothing() {
|
|||
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(shade_request_result_t::done, requests.run(req));
|
||||
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
|
||||
}
|
||||
static void test_client_values_are_checked_on_the_way_in() {
|
||||
|
|
@ -338,14 +338,14 @@ static void test_unpair_holds_prog_longer_on_a_56_bit_motor() {
|
|||
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(shade_request_result_t::radio_refused, 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(shade_request_result_t::done, requests.run(req));
|
||||
TEST_ASSERT_EQUAL_STRING("setRollingCode 4 1234", ops.calls.c_str());
|
||||
}
|
||||
|
||||
|
|
@ -357,15 +357,15 @@ static void test_a_refused_command_is_reported() {
|
|||
ops.radioTakes = false;
|
||||
shade_request_t req = request(shade_request_type_t::shade_command);
|
||||
req.target = 50;
|
||||
TEST_ASSERT_FALSE(requests.run(req));
|
||||
TEST_ASSERT_EQUAL(shade_request_result_t::radio_refused, 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_missing_shade_is_not_found);
|
||||
RUN_TEST(test_a_missing_group_is_not_found);
|
||||
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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue