src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
164 / 164
100.0%
Branches:
148 / 206
71.8%
Functions:
24 / 24
100.0%

ProtoOps.cpp
Line Branch Exec Source
1 #include <stdio.h>
2 #include <strings.h>
3 #include "ProtoOps.h"
4
5 // An op reads only the fields that were sent: unlike what the device sends, a
6 // left-out field is not its schema default but not given, so a client can
7 // leave a shade's tilt or its sun flag as they are.
8 class OpReader {
9 public:
10 42 explicit OpReader(JsonObjectConst d) : d(d) {}
11
2/4
✓ Branch 1 taken 145 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 145 times.
✗ Branch 5 not taken.
145 bool has(proto_field_t field) const { return !this->d[protoField(field).key].isNull(); }
12 // The field when it was sent, as an integer in lo..hi. False when it
13 // wasn't; error is set when it was but isn't one.
14 114 bool get(proto_field_t field, int32_t lo, int32_t hi, int32_t &out) {
15
2/4
✓ Branch 1 taken 114 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 114 times.
✗ Branch 5 not taken.
114 JsonVariantConst v = this->d[protoField(field).key];
16
2/2
✓ Branch 1 taken 50 times.
✓ Branch 2 taken 64 times.
114 if(v.isNull()) return false;
17
11/14
✓ Branch 1 taken 64 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 62 times.
✓ Branch 4 taken 2 times.
✓ Branch 6 taken 62 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 57 times.
✓ Branch 9 taken 5 times.
✓ Branch 11 taken 57 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 4 times.
✓ Branch 14 taken 53 times.
✓ Branch 15 taken 11 times.
✓ Branch 16 taken 53 times.
64 if(!v.is<int32_t>() || v.as<int32_t>() < lo || v.as<int32_t>() > hi) {
18
1/2
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
11 this->fail("%s must be %ld-%ld", protoField(field).name, (long)lo, (long)hi);
19 11 return false;
20 }
21
1/2
✓ Branch 1 taken 53 times.
✗ Branch 2 not taken.
53 out = v.as<int32_t>();
22 53 return true;
23 }
24 // The same for a field the op can't do without.
25 47 bool need(proto_field_t field, int32_t lo, int32_t hi, int32_t &out) {
26
2/2
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 42 times.
47 if(!this->has(field)) {
27 5 this->fail("%s is missing", protoField(field).name);
28 5 return false;
29 }
30 42 return this->get(field, lo, hi, out);
31 }
32 19 bool command(somfy_commands &cmd) {
33
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 18 times.
19 if(!this->has(proto_field_t::command)) {
34 1 this->fail("command is missing");
35 1 return false;
36 }
37
2/2
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 16 times.
18 if(!protoCommand(protoString(proto_field_t::command), cmd)) {
38 2 this->fail("Not a command");
39 2 return false;
40 }
41 16 return true;
42 }
43 // Exactly one of the two.
44 22 bool either(proto_field_t a, proto_field_t b) {
45
2/2
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 18 times.
22 if(this->has(a) == this->has(b)) {
46 4 this->fail("Give %s or %s", protoField(a).name, protoField(b).name);
47 4 return false;
48 }
49 18 return true;
50 }
51 template<typename... Args>
52 27 void fail(const char *fmt, Args... args) {
53 // Only the first problem is reported.
54
5/8
void OpReader::fail<>(char const*):
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
void OpReader::fail<char const*, char const*>(char const*, char const*, char const*):
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
void OpReader::fail<char const*, long, long>(char const*, char const*, long, long):
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9 times.
void OpReader::fail<char const*>(char const*, char const*):
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
27 if(this->error) return;
55 25 snprintf(this->why, sizeof(this->why), fmt, args...);
56 25 this->error = this->why;
57 }
58 const char *error = nullptr;
59 private:
60 JsonObjectConst d;
61 char why[64];
62 18 const char *protoString(proto_field_t field) const {
63
2/4
✓ Branch 1 taken 18 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
18 JsonVariantConst v = this->d[protoField(field).key];
64
4/6
✓ Branch 1 taken 18 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 17 times.
✓ Branch 4 taken 1 time.
✓ Branch 6 taken 17 times.
✗ Branch 7 not taken.
36 return v.is<const char *>() ? v.as<const char *>() : nullptr;
65 }
66 };
67 // The reader's problem, kept once the reader has gone so the reply can send
68 // it. One buffer for every parse: they all run on async_tcp, one at a time.
69 static char lastError[64];
70 42 static const char *errorOf(OpReader &r) {
71
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 25 times.
42 if(!r.error) return nullptr;
72 25 snprintf(lastError, sizeof(lastError), "%s", r.error);
73 25 return lastError;
74 }
75
76 struct command_name_t {
77 const char *name;
78 somfy_commands cmd;
79 };
80 // The names, and the spellings shadeCommanded reports, so they read back.
81 static const command_name_t COMMANDS[] = {
82 {"My", somfy_commands::My}, {"Up", somfy_commands::Up}, {"Down", somfy_commands::Down},
83 {"MyUp", somfy_commands::MyUp}, {"My+Up", somfy_commands::MyUp},
84 {"MyDown", somfy_commands::MyDown}, {"My+Down", somfy_commands::MyDown},
85 {"UpDown", somfy_commands::UpDown}, {"Up+Down", somfy_commands::UpDown},
86 {"MyUpDown", somfy_commands::MyUpDown}, {"My+Up+Down", somfy_commands::MyUpDown},
87 {"Prog", somfy_commands::Prog},
88 {"SunFlag", somfy_commands::SunFlag}, {"Sun Flag", somfy_commands::SunFlag},
89 {"Flag", somfy_commands::Flag},
90 {"StepUp", somfy_commands::StepUp}, {"Step Up", somfy_commands::StepUp},
91 {"StepDown", somfy_commands::StepDown}, {"Step Down", somfy_commands::StepDown},
92 {"Toggle", somfy_commands::Toggle}, {"Sensor", somfy_commands::Sensor},
93 {"Favorite", somfy_commands::Favorite}, {"Stop", somfy_commands::Stop}
94 };
95 24 bool protoCommand(const char *name, somfy_commands &cmd) {
96
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 22 times.
24 if(!name) return false;
97
2/2
✓ Branch 0 taken 167 times.
✓ Branch 1 taken 3 times.
170 for(const command_name_t &c : COMMANDS) {
98
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 148 times.
167 if(strcasecmp(c.name, name) == 0) {
99 19 cmd = c.cmd;
100 19 return true;
101 }
102 }
103 3 return false;
104 }
105
106 // The repeat count and step size a command may carry.
107 23 static void readRepeat(OpReader &r, shade_request_t &req) {
108 int32_t n;
109
3/4
✓ Branch 1 taken 23 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 20 times.
23 if(r.get(proto_field_t::repeat, 0, INT8_MAX, n)) req.repeat = static_cast<int8_t>(n);
110 23 }
111 19 static void readStep(OpReader &r, shade_request_t &req) {
112 int32_t n;
113 // Steps are 7 bits on air.
114
3/4
✓ Branch 1 taken 19 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 18 times.
19 if(r.get(proto_field_t::stepSize, 0, 127, n)) req.stepSize = static_cast<uint8_t>(n);
115 19 }
116 34 static void readId(OpReader &r, proto_field_t field, shade_request_t &req) {
117 int32_t n;
118
3/4
✓ Branch 1 taken 34 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 30 times.
✓ Branch 4 taken 4 times.
34 if(r.need(field, 0, 255, n)) req.id = static_cast<uint8_t>(n);
119 34 }
120 26 static void readShade(OpReader &r, shade_request_t &req) { readId(r, proto_field_t::shadeId, req); }
121 // A shade or a group; req.type becomes the one that was given.
122 8 static void readShadeOrGroup(OpReader &r, shade_request_t &req, shade_request_type_t shade, shade_request_type_t group) {
123
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 6 times.
8 if(!r.either(proto_field_t::shadeId, proto_field_t::groupId)) return;
124 6 const bool isShade = r.has(proto_field_t::shadeId);
125
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
6 req.type = isShade ? shade : group;
126
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 2 times.
6 readId(r, isShade ? proto_field_t::shadeId : proto_field_t::groupId, req);
127 }
128 // A command, or a position 0-100 to move to.
129 14 static void readCommandOrTarget(OpReader &r, shade_request_t &req) {
130
3/4
✓ Branch 1 taken 14 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 12 times.
14 if(!r.either(proto_field_t::command, proto_field_t::target)) return;
131 int32_t n;
132
4/6
✓ Branch 1 taken 12 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
✓ Branch 4 taken 4 times.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
12 if(r.has(proto_field_t::command)) r.command(req.command);
133
3/4
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 2 times.
4 else if(r.get(proto_field_t::target, 0, 100, n)) req.target = static_cast<int8_t>(n);
134 }
135 // Two values in lo..hi of which at least one is given; the other is left as
136 // it was.
137 11 static void readOneOrBoth(OpReader &r, proto_field_t a, proto_field_t b, int32_t lo, int32_t hi, int8_t &aOut, int8_t &bOut) {
138
8/10
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
✓ Branch 4 taken 5 times.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 3 times.
✓ Branch 10 taken 3 times.
✓ Branch 11 taken 8 times.
11 if(!r.has(a) && !r.has(b)) {
139
2/4
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 r.fail("Give %s, %s or both", protoField(a).name, protoField(b).name);
140 3 return;
141 }
142 int32_t n;
143
3/4
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 5 times.
8 if(r.get(a, lo, hi, n)) aOut = static_cast<int8_t>(n);
144
3/4
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 4 times.
8 if(r.get(b, lo, hi, n)) bOut = static_cast<int8_t>(n);
145 }
146
147 37 const char *parseShadeOp(uint8_t type, JsonObjectConst d, shade_request_t &req) {
148 37 req = shade_request_t();
149 37 OpReader r(d);
150 int32_t n;
151
10/10
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 4 times.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 1 time.
37 switch(static_cast<proto_type_t>(type)) {
152 13 case proto_type_t::shadeCommand:
153 13 req.type = shade_request_type_t::shade_command;
154
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 readShade(r, req);
155
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 readCommandOrTarget(r, req);
156
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 readRepeat(r, req);
157
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 readStep(r, req);
158 13 break;
159 1 case proto_type_t::tiltCommand:
160 1 req.type = shade_request_type_t::tilt_command;
161
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
1 readShade(r, req);
162
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
1 readCommandOrTarget(r, req);
163 1 break;
164 2 case proto_type_t::groupCommand:
165 2 req.type = shade_request_type_t::group_command;
166
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 readId(r, proto_field_t::groupId, req);
167
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 r.command(req.command);
168
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 readRepeat(r, req);
169
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 readStep(r, req);
170 2 break;
171 4 case proto_type_t::repeatCommand:
172
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 readShadeOrGroup(r, req, shade_request_type_t::shade_repeat, shade_request_type_t::group_repeat);
173
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 r.command(req.command);
174
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 readRepeat(r, req);
175
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 readStep(r, req);
176 4 break;
177 4 case proto_type_t::setPositions:
178 4 req.type = shade_request_type_t::set_positions;
179
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 readShade(r, req);
180
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 readOneOrBoth(r, proto_field_t::position, proto_field_t::tiltPosition, 0, 100, req.position, req.tiltPosition);
181 4 break;
182 4 case proto_type_t::setSensor:
183
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 readShadeOrGroup(r, req, shade_request_type_t::shade_sensor, shade_request_type_t::group_sensor);
184
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 readOneOrBoth(r, proto_field_t::sunny, proto_field_t::windy, 0, 1, req.sunny, req.windy);
185
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 readRepeat(r, req);
186 4 break;
187 3 case proto_type_t::setMyPosition:
188 3 req.type = shade_request_type_t::set_favorite;
189
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 readShade(r, req);
190
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 readOneOrBoth(r, proto_field_t::myPos, proto_field_t::myTiltPos, 0, 100, req.position, req.tiltPosition);
191 3 break;
192 2 case proto_type_t::unpairShade:
193 2 req.type = shade_request_type_t::unpair;
194
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 readShade(r, req);
195 2 break;
196 3 case proto_type_t::setRollingCode:
197 3 req.type = shade_request_type_t::set_rolling_code;
198
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 readShade(r, req);
199
3/4
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 2 times.
3 if(r.need(proto_field_t::lastRollingCode, 0, UINT16_MAX, n)) req.rollingCode = static_cast<uint16_t>(n);
200 3 break;
201 1 default:
202 1 r.fail("Not a shade op");
203 1 break;
204 }
205 74 return errorOf(r);
206 }
207
208 5 const char *parseRemoteCommandOp(JsonObjectConst d, somfy_frame_t &frame, uint8_t &repeats) {
209 5 frame = somfy_frame_t();
210 5 repeats = 0;
211 5 OpReader r(d);
212 int32_t n;
213 // A remote address is 24 bits, and 0 is no remote.
214
3/4
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 2 times.
5 if(r.need(proto_field_t::remoteAddress, 1, 0xFFFFFF, n)) frame.remoteAddress = static_cast<uint32_t>(n);
215
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 r.command(frame.cmd);
216
3/4
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 1 time.
5 if(r.need(proto_field_t::lastRollingCode, 1, UINT16_MAX, n)) frame.rollingCode = static_cast<uint16_t>(n);
217
3/4
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 4 times.
5 if(r.get(proto_field_t::encKey, 0, UINT8_MAX, n)) frame.encKey = static_cast<uint8_t>(n);
218
3/4
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 4 times.
5 if(r.get(proto_field_t::repeats, 0, UINT8_MAX, n)) repeats = static_cast<uint8_t>(n);
219 10 return errorOf(r);
220 }
221
222 5 proto_error_t protoRequestError(shade_request_result_t result) {
223
5/5
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
5 switch(result) {
224 1 case shade_request_result_t::not_found: return proto_error_t::notFound;
225 1 case shade_request_result_t::radio_refused: return proto_error_t::radioUnavailable;
226 // At rest it would take it: the client may try again.
227 1 case shade_request_result_t::shade_moving: return proto_error_t::busy;
228 1 case shade_request_result_t::no_tilt: return proto_error_t::badRequest;
229 1 default: return proto_error_t::none;
230 }
231 }
232 6 const char *protoRequestErrorDesc(const shade_request_t &req, shade_request_result_t result) {
233
5/5
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
6 switch(result) {
234
2/2
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 1 time.
2 case shade_request_result_t::not_found: return shadeRequestIsForGroup(req.type) ? "No group with that id" : "No shade with that id";
235 1 case shade_request_result_t::radio_refused: return PROTO_RADIO_REFUSED;
236 1 case shade_request_result_t::shade_moving: return "The shade is moving";
237 1 case shade_request_result_t::no_tilt: return "The shade has no tilt";
238 1 default: return nullptr;
239 }
240 }
241