mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
- A group's sunSensor and a shade's flags are refused by name: the routes don't set them. - setPaired needs paired, and without a shade id it is a bad request. - groupOptions' payload is parsed in ProtoEdits (parseReadPayload). - The assembler's mutex is static, so creating it can't fail. - protoIsReadOp and protoIsShadeOp are gone; the tests use protoOpKind. - Native tests for missing(), offeredRemoteAddress and the edit ops against the TYPE table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
240 lines
9.6 KiB
C++
240 lines
9.6 KiB
C++
#include <stdio.h>
|
|
#include <strings.h>
|
|
#include "ProtoOps.h"
|
|
|
|
// An op reads only the fields that were sent: unlike what the device sends, a
|
|
// left-out field is not its schema default but not given, so a client can
|
|
// leave a shade's tilt or its sun flag as they are.
|
|
class OpReader {
|
|
public:
|
|
explicit OpReader(JsonObjectConst d) : d(d) {}
|
|
bool has(proto_field_t field) const { return !this->d[protoField(field).key].isNull(); }
|
|
// The field when it was sent, as an integer in lo..hi. False when it
|
|
// wasn't; error is set when it was but isn't one.
|
|
bool get(proto_field_t field, int32_t lo, int32_t hi, int32_t &out) {
|
|
JsonVariantConst v = this->d[protoField(field).key];
|
|
if(v.isNull()) return false;
|
|
if(!v.is<int32_t>() || v.as<int32_t>() < lo || v.as<int32_t>() > hi) {
|
|
this->fail("%s must be %ld-%ld", protoField(field).name, (long)lo, (long)hi);
|
|
return false;
|
|
}
|
|
out = v.as<int32_t>();
|
|
return true;
|
|
}
|
|
// The same for a field the op can't do without.
|
|
bool need(proto_field_t field, int32_t lo, int32_t hi, int32_t &out) {
|
|
if(!this->has(field)) {
|
|
this->fail("%s is missing", protoField(field).name);
|
|
return false;
|
|
}
|
|
return this->get(field, lo, hi, out);
|
|
}
|
|
bool command(somfy_commands &cmd) {
|
|
if(!this->has(proto_field_t::command)) {
|
|
this->fail("command is missing");
|
|
return false;
|
|
}
|
|
if(!protoCommand(protoString(proto_field_t::command), cmd)) {
|
|
this->fail("Not a command");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
// Exactly one of the two.
|
|
bool either(proto_field_t a, proto_field_t b) {
|
|
if(this->has(a) == this->has(b)) {
|
|
this->fail("Give %s or %s", protoField(a).name, protoField(b).name);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
template<typename... Args>
|
|
void fail(const char *fmt, Args... args) {
|
|
// Only the first problem is reported.
|
|
if(this->error) return;
|
|
snprintf(this->why, sizeof(this->why), fmt, args...);
|
|
this->error = this->why;
|
|
}
|
|
const char *error = nullptr;
|
|
private:
|
|
JsonObjectConst d;
|
|
char why[64];
|
|
const char *protoString(proto_field_t field) const {
|
|
JsonVariantConst v = this->d[protoField(field).key];
|
|
return v.is<const char *>() ? v.as<const char *>() : nullptr;
|
|
}
|
|
};
|
|
// The reader's problem, kept once the reader has gone so the reply can send
|
|
// it. One buffer for every parse: they all run on async_tcp, one at a time.
|
|
static char lastError[64];
|
|
static const char *errorOf(OpReader &r) {
|
|
if(!r.error) return nullptr;
|
|
snprintf(lastError, sizeof(lastError), "%s", r.error);
|
|
return lastError;
|
|
}
|
|
|
|
struct command_name_t {
|
|
const char *name;
|
|
somfy_commands cmd;
|
|
};
|
|
// The names, and the spellings shadeCommanded reports, so they read back.
|
|
static const command_name_t COMMANDS[] = {
|
|
{"My", somfy_commands::My}, {"Up", somfy_commands::Up}, {"Down", somfy_commands::Down},
|
|
{"MyUp", somfy_commands::MyUp}, {"My+Up", somfy_commands::MyUp},
|
|
{"MyDown", somfy_commands::MyDown}, {"My+Down", somfy_commands::MyDown},
|
|
{"UpDown", somfy_commands::UpDown}, {"Up+Down", somfy_commands::UpDown},
|
|
{"MyUpDown", somfy_commands::MyUpDown}, {"My+Up+Down", somfy_commands::MyUpDown},
|
|
{"Prog", somfy_commands::Prog},
|
|
{"SunFlag", somfy_commands::SunFlag}, {"Sun Flag", somfy_commands::SunFlag},
|
|
{"Flag", somfy_commands::Flag},
|
|
{"StepUp", somfy_commands::StepUp}, {"Step Up", somfy_commands::StepUp},
|
|
{"StepDown", somfy_commands::StepDown}, {"Step Down", somfy_commands::StepDown},
|
|
{"Toggle", somfy_commands::Toggle}, {"Sensor", somfy_commands::Sensor},
|
|
{"Favorite", somfy_commands::Favorite}, {"Stop", somfy_commands::Stop}
|
|
};
|
|
bool protoCommand(const char *name, somfy_commands &cmd) {
|
|
if(!name) return false;
|
|
for(const command_name_t &c : COMMANDS) {
|
|
if(strcasecmp(c.name, name) == 0) {
|
|
cmd = c.cmd;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// The repeat count and step size a command may carry.
|
|
static void readRepeat(OpReader &r, shade_request_t &req) {
|
|
int32_t n;
|
|
if(r.get(proto_field_t::repeat, 0, INT8_MAX, n)) req.repeat = static_cast<int8_t>(n);
|
|
}
|
|
static void readStep(OpReader &r, shade_request_t &req) {
|
|
int32_t n;
|
|
// Steps are 7 bits on air.
|
|
if(r.get(proto_field_t::stepSize, 0, 127, n)) req.stepSize = static_cast<uint8_t>(n);
|
|
}
|
|
static void readId(OpReader &r, proto_field_t field, shade_request_t &req) {
|
|
int32_t n;
|
|
if(r.need(field, 0, 255, n)) req.id = static_cast<uint8_t>(n);
|
|
}
|
|
static void readShade(OpReader &r, shade_request_t &req) { readId(r, proto_field_t::shadeId, req); }
|
|
// A shade or a group; req.type becomes the one that was given.
|
|
static void readShadeOrGroup(OpReader &r, shade_request_t &req, shade_request_type_t shade, shade_request_type_t group) {
|
|
if(!r.either(proto_field_t::shadeId, proto_field_t::groupId)) return;
|
|
const bool isShade = r.has(proto_field_t::shadeId);
|
|
req.type = isShade ? shade : group;
|
|
readId(r, isShade ? proto_field_t::shadeId : proto_field_t::groupId, req);
|
|
}
|
|
// A command, or a position 0-100 to move to.
|
|
static void readCommandOrTarget(OpReader &r, shade_request_t &req) {
|
|
if(!r.either(proto_field_t::command, proto_field_t::target)) return;
|
|
int32_t n;
|
|
if(r.has(proto_field_t::command)) r.command(req.command);
|
|
else if(r.get(proto_field_t::target, 0, 100, n)) req.target = static_cast<int8_t>(n);
|
|
}
|
|
// Two values in lo..hi of which at least one is given; the other is left as
|
|
// it was.
|
|
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) {
|
|
if(!r.has(a) && !r.has(b)) {
|
|
r.fail("Give %s, %s or both", protoField(a).name, protoField(b).name);
|
|
return;
|
|
}
|
|
int32_t n;
|
|
if(r.get(a, lo, hi, n)) aOut = static_cast<int8_t>(n);
|
|
if(r.get(b, lo, hi, n)) bOut = static_cast<int8_t>(n);
|
|
}
|
|
|
|
const char *parseShadeOp(uint8_t type, JsonObjectConst d, shade_request_t &req) {
|
|
req = shade_request_t();
|
|
OpReader r(d);
|
|
int32_t n;
|
|
switch(static_cast<proto_type_t>(type)) {
|
|
case proto_type_t::shadeCommand:
|
|
req.type = shade_request_type_t::shade_command;
|
|
readShade(r, req);
|
|
readCommandOrTarget(r, req);
|
|
readRepeat(r, req);
|
|
readStep(r, req);
|
|
break;
|
|
case proto_type_t::tiltCommand:
|
|
req.type = shade_request_type_t::tilt_command;
|
|
readShade(r, req);
|
|
readCommandOrTarget(r, req);
|
|
break;
|
|
case proto_type_t::groupCommand:
|
|
req.type = shade_request_type_t::group_command;
|
|
readId(r, proto_field_t::groupId, req);
|
|
r.command(req.command);
|
|
readRepeat(r, req);
|
|
readStep(r, req);
|
|
break;
|
|
case proto_type_t::repeatCommand:
|
|
readShadeOrGroup(r, req, shade_request_type_t::shade_repeat, shade_request_type_t::group_repeat);
|
|
r.command(req.command);
|
|
readRepeat(r, req);
|
|
readStep(r, req);
|
|
break;
|
|
case proto_type_t::setPositions:
|
|
req.type = shade_request_type_t::set_positions;
|
|
readShade(r, req);
|
|
readOneOrBoth(r, proto_field_t::position, proto_field_t::tiltPosition, 0, 100, req.position, req.tiltPosition);
|
|
break;
|
|
case proto_type_t::setSensor:
|
|
readShadeOrGroup(r, req, shade_request_type_t::shade_sensor, shade_request_type_t::group_sensor);
|
|
readOneOrBoth(r, proto_field_t::sunny, proto_field_t::windy, 0, 1, req.sunny, req.windy);
|
|
readRepeat(r, req);
|
|
break;
|
|
case proto_type_t::setMyPosition:
|
|
req.type = shade_request_type_t::set_favorite;
|
|
readShade(r, req);
|
|
readOneOrBoth(r, proto_field_t::myPos, proto_field_t::myTiltPos, 0, 100, req.position, req.tiltPosition);
|
|
break;
|
|
case proto_type_t::unpairShade:
|
|
req.type = shade_request_type_t::unpair;
|
|
readShade(r, req);
|
|
break;
|
|
case proto_type_t::setRollingCode:
|
|
req.type = shade_request_type_t::set_rolling_code;
|
|
readShade(r, req);
|
|
if(r.need(proto_field_t::lastRollingCode, 0, UINT16_MAX, n)) req.rollingCode = static_cast<uint16_t>(n);
|
|
break;
|
|
default:
|
|
r.fail("Not a shade op");
|
|
break;
|
|
}
|
|
return errorOf(r);
|
|
}
|
|
|
|
const char *parseRemoteCommandOp(JsonObjectConst d, somfy_frame_t &frame, uint8_t &repeats) {
|
|
frame = somfy_frame_t();
|
|
repeats = 0;
|
|
OpReader r(d);
|
|
int32_t n;
|
|
// A remote address is 24 bits, and 0 is no remote.
|
|
if(r.need(proto_field_t::remoteAddress, 1, 0xFFFFFF, n)) frame.remoteAddress = static_cast<uint32_t>(n);
|
|
r.command(frame.cmd);
|
|
if(r.need(proto_field_t::lastRollingCode, 1, UINT16_MAX, n)) frame.rollingCode = static_cast<uint16_t>(n);
|
|
if(r.get(proto_field_t::encKey, 0, UINT8_MAX, n)) frame.encKey = static_cast<uint8_t>(n);
|
|
if(r.get(proto_field_t::repeats, 0, UINT8_MAX, n)) repeats = static_cast<uint8_t>(n);
|
|
return errorOf(r);
|
|
}
|
|
|
|
proto_error_t protoRequestError(shade_request_result_t result) {
|
|
switch(result) {
|
|
case shade_request_result_t::not_found: return proto_error_t::notFound;
|
|
case shade_request_result_t::radio_refused: return proto_error_t::radioUnavailable;
|
|
// At rest it would take it: the client may try again.
|
|
case shade_request_result_t::shade_moving: return proto_error_t::busy;
|
|
case shade_request_result_t::no_tilt: return proto_error_t::badRequest;
|
|
default: return proto_error_t::none;
|
|
}
|
|
}
|
|
const char *protoRequestErrorDesc(const shade_request_t &req, shade_request_result_t result) {
|
|
switch(result) {
|
|
case shade_request_result_t::not_found: return shadeRequestIsForGroup(req.type) ? "No group with that id" : "No shade with that id";
|
|
case shade_request_result_t::radio_refused: return PROTO_RADIO_REFUSED;
|
|
case shade_request_result_t::shade_moving: return "The shade is moving";
|
|
case shade_request_result_t::no_tilt: return "The shade has no tilt";
|
|
default: return nullptr;
|
|
}
|
|
}
|