mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +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
|
|
@ -8,6 +8,7 @@
|
|||
#include "SomfySnapshot.h"
|
||||
#include "SomfyEditor.h"
|
||||
#include "Utils.h"
|
||||
#include "outputs/ProtoSocket.h"
|
||||
|
||||
static const char *TAG = "LoopQueue";
|
||||
|
||||
|
|
@ -50,10 +51,13 @@ void LoopQueue::drain() {
|
|||
// loop() is the only consumer so this is the item we peeked.
|
||||
xQueueReceive(q, &item, 0);
|
||||
switch(item.type) {
|
||||
case loop_item_type_t::shade_request:
|
||||
if(!shadeRequests.run(item.request))
|
||||
case loop_item_type_t::shade_request: {
|
||||
const shade_request_result_t result = shadeRequests.run(item.request);
|
||||
if(result == shade_request_result_t::radio_refused)
|
||||
ESP_LOGW(TAG, "Radio didn't take shade request %u", static_cast<uint8_t>(item.request.type));
|
||||
if(item.replyClient) protoSocket.replyRequest(item.replyClient, item.replyId, item.request, result);
|
||||
break;
|
||||
}
|
||||
case loop_item_type_t::restore_shades: {
|
||||
restore_options_t opts = restoreOptions(item.restoreParts);
|
||||
const bool restored = ShadeConfigFile::restore(&somfy, "/shades.tmp", opts);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ struct loop_item_t {
|
|||
uint8_t restoreParts = 0; // restore_shades: RESTORE_* bits
|
||||
uint8_t slot = 0; // snapshot_fill: the SnapshotSlots slot
|
||||
model_edit_t *edit = nullptr; // model_edit: loop() runs and frees it
|
||||
// shade_request from a protocol 2 client: loop() replies to this socket
|
||||
// client with replyId once the request has run. 0 for no reply.
|
||||
uint32_t replyClient = 0;
|
||||
uint32_t replyId = 0;
|
||||
};
|
||||
|
||||
// Whether the item sends anything on air.
|
||||
|
|
|
|||
247
src/ProtoOps.cpp
Normal file
247
src/ProtoOps.cpp
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
#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;
|
||||
}
|
||||
};
|
||||
// What the reply's desc points at once parseShadeOp has returned.
|
||||
static char lastError[64];
|
||||
static const char *result(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;
|
||||
}
|
||||
|
||||
bool protoIsShadeOp(uint8_t type) {
|
||||
return type >= static_cast<uint8_t>(proto_type_t::shadeCommand) && type <= static_cast<uint8_t>(proto_type_t::setRollingCode);
|
||||
}
|
||||
|
||||
// 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 readShade(OpReader &r, shade_request_t &req) {
|
||||
int32_t n;
|
||||
if(r.need(proto_field_t::shadeId, 0, 255, n)) req.id = static_cast<uint8_t>(n);
|
||||
}
|
||||
// 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;
|
||||
int32_t n;
|
||||
const bool isShade = r.has(proto_field_t::shadeId);
|
||||
req.type = isShade ? shade : group;
|
||||
if(r.need(isShade ? proto_field_t::shadeId : proto_field_t::groupId, 0, 255, n)) req.id = static_cast<uint8_t>(n);
|
||||
}
|
||||
// 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 positions 0-100 of which at least one is given; -1 for the other.
|
||||
static void readPositions(OpReader &r, proto_field_t lift, proto_field_t tilt, int8_t &liftOut, int8_t &tiltOut) {
|
||||
if(!r.has(lift) && !r.has(tilt)) {
|
||||
r.fail("Give %s, %s or both", protoField(lift).name, protoField(tilt).name);
|
||||
return;
|
||||
}
|
||||
int32_t n;
|
||||
if(r.get(lift, 0, 100, n)) liftOut = static_cast<int8_t>(n);
|
||||
if(r.get(tilt, 0, 100, n)) tiltOut = 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;
|
||||
if(r.need(proto_field_t::groupId, 0, 255, n)) req.id = static_cast<uint8_t>(n);
|
||||
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);
|
||||
readPositions(r, proto_field_t::position, proto_field_t::tiltPosition, 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);
|
||||
if(!r.has(proto_field_t::sunny) && !r.has(proto_field_t::windy)) r.fail("Give sunny, windy or both");
|
||||
if(r.get(proto_field_t::sunny, 0, 1, n)) req.sunny = static_cast<int8_t>(n);
|
||||
if(r.get(proto_field_t::windy, 0, 1, n)) req.windy = static_cast<int8_t>(n);
|
||||
readRepeat(r, req);
|
||||
break;
|
||||
case proto_type_t::setMyPosition:
|
||||
req.type = shade_request_type_t::set_favorite;
|
||||
readShade(r, req);
|
||||
readPositions(r, proto_field_t::myPos, proto_field_t::myTiltPos, 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 result(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 result(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;
|
||||
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:
|
||||
switch(req.type) {
|
||||
case shade_request_type_t::group_command:
|
||||
case shade_request_type_t::group_repeat:
|
||||
case shade_request_type_t::group_sensor:
|
||||
return "No group with that id";
|
||||
default:
|
||||
return "No shade with that id";
|
||||
}
|
||||
case shade_request_result_t::radio_refused: return "The radio didn't take the command";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
34
src/ProtoOps.h
Normal file
34
src/ProtoOps.h
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef PROTO_OPS_H
|
||||
#define PROTO_OPS_H
|
||||
#include <stdint.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "ProtoSchema.h"
|
||||
#include "ShadeRequests.h"
|
||||
#include "SomfyFrame.h"
|
||||
|
||||
// Socket protocol 2's command ops (docs/PROTOCOL.md): what each one asks for,
|
||||
// read from its payload. Kept free of the shade model and the web server so
|
||||
// it can be unit tested on the PC (pio test -e native).
|
||||
//
|
||||
// The readers are strict where the HTTP routes guess: a field of the wrong
|
||||
// kind, a value out of range or a command name that isn't one is a bad
|
||||
// request, not a default.
|
||||
|
||||
// Every command op except sendRemoteCommand becomes a shade request that
|
||||
// loop() runs.
|
||||
bool protoIsShadeOp(uint8_t type);
|
||||
// nullptr when the payload is good and req is filled in; otherwise what is
|
||||
// wrong with it, for a bad request reply.
|
||||
const char *parseShadeOp(uint8_t type, JsonObjectConst d, shade_request_t &req);
|
||||
// sendRemoteCommand: a frame as a remote that isn't one of the shades, so it
|
||||
// touches nothing of the shade model. The same as parseShadeOp otherwise.
|
||||
const char *parseRemoteCommandOp(JsonObjectConst d, somfy_frame_t &frame, uint8_t &repeats);
|
||||
// A command by its name, as shadeCommanded reports it ("Up", "StepDown"),
|
||||
// in any case. False for a name that isn't a command.
|
||||
bool protoCommand(const char *name, somfy_commands &cmd);
|
||||
|
||||
// The reply to a shade request that loop() has run: none when it was done.
|
||||
proto_error_t protoRequestError(shade_request_result_t result);
|
||||
// What that reply says.
|
||||
const char *protoRequestErrorDesc(const shade_request_t &req, shade_request_result_t result);
|
||||
#endif
|
||||
|
|
@ -24,6 +24,16 @@
|
|||
TYPE(syncEnd, 5, 'd') \
|
||||
TYPE(subscribe, 6, 'c') \
|
||||
TYPE(unsubscribe, 7, 'c') \
|
||||
TYPE(shadeCommand, 16, 'c') \
|
||||
TYPE(groupCommand, 17, 'c') \
|
||||
TYPE(tiltCommand, 18, 'c') \
|
||||
TYPE(repeatCommand, 19, 'c') \
|
||||
TYPE(setPositions, 20, 'c') \
|
||||
TYPE(setSensor, 21, 'c') \
|
||||
TYPE(setMyPosition, 22, 'c') \
|
||||
TYPE(unpairShade, 23, 'c') \
|
||||
TYPE(setRollingCode, 24, 'c') \
|
||||
TYPE(sendRemoteCommand, 25, 'c') \
|
||||
TYPE(shadeState, 128, 'd') \
|
||||
TYPE(shade, 129, 'd') \
|
||||
TYPE(shadeRemoved, 130, 'd') \
|
||||
|
|
@ -35,7 +45,7 @@
|
|||
TYPE(roomOrder, 136, 'd') \
|
||||
TYPE(shadeOrder, 137, 'd') \
|
||||
TYPE(groupOrder, 138, 'd') \
|
||||
TYPE(shadeCommand, 139, 'd') \
|
||||
TYPE(shadeCommanded, 139, 'd') \
|
||||
TYPE(memory, 140, 'd') \
|
||||
TYPE(wifi, 141, 'd') \
|
||||
TYPE(ethernet, 142, 'd') \
|
||||
|
|
@ -107,10 +117,13 @@
|
|||
FIELD(gpioLLTrigger, "gl", 'b', 0) \
|
||||
FIELD(linkedRemotes, "lk", 'l', 0) \
|
||||
FIELD(linkedShades, "ls", 'l', 0) \
|
||||
/* shadeCommand */ \
|
||||
/* the command ops and shadeCommanded */ \
|
||||
FIELD(command, "cm", 's', 0) \
|
||||
FIELD(source, "so", 's', 0) \
|
||||
FIELD(sourceAddress, "sr", 'i', 0) \
|
||||
FIELD(repeat, "re", 'i', 0) \
|
||||
FIELD(sunny, "sn", 'b', 0) \
|
||||
FIELD(windy, "wn", 'b', 0) \
|
||||
/* the header */ \
|
||||
FIELD(maxRooms, "xr", 'i', 0) \
|
||||
FIELD(maxShades, "xs", 'i', 0) \
|
||||
|
|
|
|||
|
|
@ -21,69 +21,71 @@ static bool inRange(int8_t pos) { return pos >= 0 && pos <= 100; }
|
|||
static int8_t storedPosition(int8_t pos, bool flip) { return displayPosition(static_cast<float>(pos), flip); }
|
||||
static uint8_t repeatsOr(int8_t repeat, uint8_t own) { return repeat >= 0 ? static_cast<uint8_t>(repeat) : own; }
|
||||
|
||||
bool ShadeRequests::run(const shade_request_t &req) {
|
||||
static shade_request_result_t sent(bool taken) { return taken ? shade_request_result_t::done : shade_request_result_t::radio_refused; }
|
||||
|
||||
shade_request_result_t ShadeRequests::run(const shade_request_t &req) {
|
||||
shade_info_t shade;
|
||||
uint8_t groupRepeats = 0;
|
||||
switch(req.type) {
|
||||
case shade_request_type_t::group_command:
|
||||
case shade_request_type_t::group_repeat:
|
||||
case shade_request_type_t::group_sensor:
|
||||
if(!this->ops.findGroup(req.id, groupRepeats)) return true;
|
||||
if(!this->ops.findGroup(req.id, groupRepeats)) return shade_request_result_t::not_found;
|
||||
break;
|
||||
default:
|
||||
if(!this->ops.findShade(req.id, shade)) return true;
|
||||
if(!this->ops.findShade(req.id, shade)) return shade_request_result_t::not_found;
|
||||
break;
|
||||
}
|
||||
const bool flip = shade.flipPosition;
|
||||
switch(req.type) {
|
||||
case shade_request_type_t::shade_command:
|
||||
if(inRange(req.target)) return this->ops.moveToTarget(req.id, storedPosition(req.target, flip));
|
||||
return this->ops.sendCommand(req.id, req.command, repeatsOr(req.repeat, shade.repeats), req.stepSize);
|
||||
if(inRange(req.target)) return sent(this->ops.moveToTarget(req.id, storedPosition(req.target, flip)));
|
||||
return sent(this->ops.sendCommand(req.id, req.command, repeatsOr(req.repeat, shade.repeats), req.stepSize));
|
||||
case shade_request_type_t::group_command:
|
||||
return this->ops.sendGroupCommand(req.id, req.command, repeatsOr(req.repeat, groupRepeats), req.stepSize);
|
||||
return sent(this->ops.sendGroupCommand(req.id, req.command, repeatsOr(req.repeat, groupRepeats), req.stepSize));
|
||||
case shade_request_type_t::tilt_command:
|
||||
if(inRange(req.target)) return this->ops.moveToTiltTarget(req.id, storedPosition(req.target, flip));
|
||||
return this->ops.sendTiltCommand(req.id, req.command);
|
||||
if(inRange(req.target)) return sent(this->ops.moveToTiltTarget(req.id, storedPosition(req.target, flip)));
|
||||
return sent(this->ops.sendTiltCommand(req.id, req.command));
|
||||
case shade_request_type_t::shade_repeat: {
|
||||
// A single button garage door has no Prog, its button is Toggle.
|
||||
somfy_commands cmd = req.command;
|
||||
if(shade.shadeType == shade_types::garage1 && cmd == somfy_commands::Prog) cmd = somfy_commands::Toggle;
|
||||
const uint8_t repeat = repeatsOr(req.repeat, shade.repeats);
|
||||
if(this->ops.isLastCommand(req.id, cmd)) return this->ops.repeatFrame(req.id, repeat);
|
||||
return this->ops.sendCommand(req.id, cmd, repeat, req.stepSize);
|
||||
if(this->ops.isLastCommand(req.id, cmd)) return sent(this->ops.repeatFrame(req.id, repeat));
|
||||
return sent(this->ops.sendCommand(req.id, cmd, repeat, req.stepSize));
|
||||
}
|
||||
case shade_request_type_t::group_repeat: {
|
||||
const uint8_t repeat = repeatsOr(req.repeat, groupRepeats);
|
||||
if(this->ops.isLastGroupCommand(req.id, req.command)) return this->ops.repeatGroupFrame(req.id, repeat);
|
||||
return this->ops.sendGroupCommand(req.id, req.command, repeat, req.stepSize);
|
||||
if(this->ops.isLastGroupCommand(req.id, req.command)) return sent(this->ops.repeatGroupFrame(req.id, repeat));
|
||||
return sent(this->ops.sendGroupCommand(req.id, req.command, repeat, req.stepSize));
|
||||
}
|
||||
case shade_request_type_t::set_positions: {
|
||||
const float pos = inRange(req.position) ? storedPosition(req.position, flip) : -1.0f;
|
||||
const float tilt = inRange(req.tiltPosition) && shade.tiltType != tilt_types::none ? storedPosition(req.tiltPosition, flip) : -1.0f;
|
||||
if(pos >= 0.0f || tilt >= 0.0f) this->ops.setPositions(req.id, pos, tilt);
|
||||
return true;
|
||||
return shade_request_result_t::done;
|
||||
}
|
||||
case shade_request_type_t::shade_sensor:
|
||||
return this->ops.sendSensorCommand(req.id, req.windy, req.sunny, repeatsOr(req.repeat, shade.repeats));
|
||||
return sent(this->ops.sendSensorCommand(req.id, req.windy, req.sunny, repeatsOr(req.repeat, shade.repeats)));
|
||||
case shade_request_type_t::group_sensor:
|
||||
return this->ops.sendGroupSensorCommand(req.id, req.windy, req.sunny, repeatsOr(req.repeat, groupRepeats));
|
||||
return sent(this->ops.sendGroupSensorCommand(req.id, req.windy, req.sunny, repeatsOr(req.repeat, groupRepeats)));
|
||||
case shade_request_type_t::set_favorite: {
|
||||
const int8_t tilt = requestedFavoriteTilt(req.tiltPosition, shade.tiltType, shade.myTiltPos, flip);
|
||||
// Without a lift position only the tilt changes: the lift favorite stays,
|
||||
// or is set where the shade is when it has none.
|
||||
int8_t pos = req.position;
|
||||
if(!inRange(pos)) pos = displayPosition(shade.myPos >= 0.0f ? shade.myPos : shade.currentPos, flip);
|
||||
return this->ops.setMyPosition(req.id, storedPosition(pos, flip), storedPosition(tilt, flip));
|
||||
return sent(this->ops.setMyPosition(req.id, storedPosition(pos, flip), storedPosition(tilt, flip)));
|
||||
}
|
||||
case shade_request_type_t::unpair: {
|
||||
// A 56-bit motor needs a longer Prog press than an 80-bit one.
|
||||
const bool sent = this->ops.sendCommand(req.id, somfy_commands::Prog, shade.bitLength == 56 ? 7 : 1, 0);
|
||||
if(sent) this->ops.setUnpaired(req.id);
|
||||
return sent;
|
||||
const bool taken = this->ops.sendCommand(req.id, somfy_commands::Prog, shade.bitLength == 56 ? 7 : 1, 0);
|
||||
if(taken) this->ops.setUnpaired(req.id);
|
||||
return sent(taken);
|
||||
}
|
||||
case shade_request_type_t::set_rolling_code:
|
||||
this->ops.setRollingCode(req.id, req.rollingCode);
|
||||
return true;
|
||||
return shade_request_result_t::done;
|
||||
}
|
||||
return true;
|
||||
return shade_request_result_t::done;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,16 +85,20 @@ class ShadeOps {
|
|||
virtual bool sendGroupSensorCommand(uint8_t groupId, int8_t windy, int8_t sunny, uint8_t repeat) = 0;
|
||||
};
|
||||
|
||||
// How a request went. One with nothing in range to do is done.
|
||||
enum class shade_request_result_t : uint8_t {
|
||||
done,
|
||||
not_found, // no shade or group with that id; nothing was done
|
||||
radio_refused // the radio didn't take a command it sent
|
||||
};
|
||||
|
||||
// Carries out shade requests, whichever entry point they came from: finds
|
||||
// the shade or group, turns client positions into stored ones, checks
|
||||
// ranges and fills in default repeats. Runs on loop() only.
|
||||
class ShadeRequests {
|
||||
public:
|
||||
explicit ShadeRequests(ShadeOps &ops) : ops(ops) {}
|
||||
// False when the radio refused a command it sent. A request for a shade
|
||||
// or group that doesn't exist, or with nothing in range to do, is dropped
|
||||
// and returns true.
|
||||
bool run(const shade_request_t &req);
|
||||
shade_request_result_t run(const shade_request_t &req);
|
||||
private:
|
||||
ShadeOps &ops;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
#include "ProtoFrame.h"
|
||||
#include "ProtoParse.h"
|
||||
#include "ProtoSlots.h"
|
||||
#include "ProtoOps.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "SomfySnapshot.h"
|
||||
#include "Somfy.h"
|
||||
#include "ConfigSettings.h"
|
||||
|
|
@ -26,6 +28,7 @@ extern ESPNetwork net;
|
|||
extern Web webServer;
|
||||
extern LoopStats loopStats;
|
||||
extern SocketEmitter sockEmit;
|
||||
extern LoopQueue loopQueue;
|
||||
|
||||
ProtoSocket protoSocket;
|
||||
static AsyncWebSocket ws("/ws");
|
||||
|
|
@ -144,6 +147,34 @@ static void replyOk(AsyncWebSocketClient *client, uint32_t id) {
|
|||
if(id == 0) return;
|
||||
sendFixed(client, [](JsonFormatter &json, void *ctx) { writeOkReply(json, *static_cast<uint32_t *>(ctx)); }, &id);
|
||||
}
|
||||
// A command op that becomes a shade request: loop() runs it and replies.
|
||||
static void queueShadeOp(AsyncWebSocketClient *client, const proto_message_t &req, JsonObjectConst d) {
|
||||
loop_item_t item;
|
||||
const char *bad = parseShadeOp(req.type, d, item.request);
|
||||
if(bad) { replyError(client, req.id, proto_error_t::badRequest, bad); return; }
|
||||
// Nothing goes on air during a frequency scan, so say so now rather than
|
||||
// once it reaches the radio.
|
||||
if(loopItemUsesRadio(item) && somfy.transceiver.txSuspended()) {
|
||||
replyError(client, req.id, proto_error_t::radioUnavailable, "The radio is paused");
|
||||
return;
|
||||
}
|
||||
item.replyClient = client->id();
|
||||
item.replyId = req.id;
|
||||
if(!loopQueue.submit(item)) replyError(client, req.id, proto_error_t::busy, "Command queue full");
|
||||
}
|
||||
// A frame as a remote that isn't one of the shades. It touches nothing of the
|
||||
// shade model and the radio takes jobs from any task, so it goes from here.
|
||||
static void sendRemoteCommand(AsyncWebSocketClient *client, const proto_message_t &req, JsonObjectConst d) {
|
||||
somfy_frame_t frame;
|
||||
uint8_t repeats;
|
||||
const char *bad = parseRemoteCommandOp(d, frame, repeats);
|
||||
if(bad) { replyError(client, req.id, proto_error_t::badRequest, bad); return; }
|
||||
if(!somfy.sendFrame(frame, repeats)) {
|
||||
replyError(client, req.id, proto_error_t::radioUnavailable, "The radio didn't take the command");
|
||||
return;
|
||||
}
|
||||
replyOk(client, req.id);
|
||||
}
|
||||
static bool authorized(AsyncWebServerRequest *request) {
|
||||
if(settings.Security.type == security_types::None) return true;
|
||||
const AsyncWebParameter *key = request->getParam("apikey");
|
||||
|
|
@ -210,8 +241,15 @@ static void onFrame(AsyncWebSocketClient *client, AwsFrameInfo *info, uint8_t *d
|
|||
replyOk(client, msg.id);
|
||||
return;
|
||||
}
|
||||
case proto_type_t::sendRemoteCommand:
|
||||
sendRemoteCommand(client, msg, doc["d"]);
|
||||
return;
|
||||
default:
|
||||
// The ops arrive with the protocol core's later PRs.
|
||||
if(protoIsShadeOp(msg.type)) {
|
||||
queueShadeOp(client, msg, doc["d"]);
|
||||
return;
|
||||
}
|
||||
// The edit and settings ops arrive with the protocol core's later PRs.
|
||||
replyError(client, msg.id, proto_error_t::unknownOp, "Unknown message type");
|
||||
return;
|
||||
}
|
||||
|
|
@ -271,6 +309,18 @@ void ProtoSocket::loop() {
|
|||
ws.cleanupClients(PROTO_MAX_CLIENTS);
|
||||
}
|
||||
|
||||
void ProtoSocket::replyRequest(uint32_t client, uint32_t id, const shade_request_t &req, shade_request_result_t result) {
|
||||
if(!sockEmit.onLoopTask() || id == 0) return;
|
||||
char buf[192];
|
||||
BufferedJsonFormatter json;
|
||||
json.setBuffer(buf, sizeof(buf));
|
||||
const proto_error_t code = protoRequestError(result);
|
||||
if(code == proto_error_t::none) writeOkReply(json, id);
|
||||
else writeErrorReply(json, id, code, protoRequestErrorDesc(req, result));
|
||||
// A client that has gone is skipped.
|
||||
if(!json.overflowed()) ws.text(client, buf, json.length());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* loop(): events
|
||||
********************************************************************/
|
||||
|
|
@ -373,9 +423,9 @@ void ProtoSocket::repeaters() {
|
|||
w.endList();
|
||||
sendEvent();
|
||||
}
|
||||
void ProtoSocket::shadeCommand(SomfyShade &shade, const char *command, const char *source, uint32_t sourceAddress) {
|
||||
void ProtoSocket::shadeCommanded(SomfyShade &shade, const char *command, const char *source, uint32_t sourceAddress) {
|
||||
if(!this->sending()) return;
|
||||
ProtoWriter w(beginEvent(proto_type_t::shadeCommand));
|
||||
ProtoWriter w(beginEvent(proto_type_t::shadeCommanded));
|
||||
w.add(proto_field_t::shadeId, shade.getShadeId());
|
||||
w.add(proto_field_t::remoteAddress, shade.getRemoteAddress());
|
||||
w.add(proto_field_t::command, command);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <ESPAsyncWebServer.h>
|
||||
#include "ProtoSchema.h"
|
||||
#include "ModelEditor.h"
|
||||
#include "ShadeRequests.h"
|
||||
|
||||
class SomfyShade;
|
||||
class SomfyGroup;
|
||||
|
|
@ -23,6 +24,8 @@ class ProtoSocket {
|
|||
// Moves each client's sync on. loop() only.
|
||||
void loop();
|
||||
bool hasClients() const;
|
||||
// The reply to a command op once loop() has run its shade request.
|
||||
void replyRequest(uint32_t client, uint32_t id, const shade_request_t &req, shade_request_result_t result);
|
||||
// Events, loop() only.
|
||||
void shadeState(SomfyShade &shade);
|
||||
void shade(SomfyShade &shade);
|
||||
|
|
@ -35,7 +38,7 @@ class ProtoSocket {
|
|||
// roomOrder, shadeOrder or groupOrder: the list's ids, in sort order.
|
||||
void order(sort_list_t list);
|
||||
void repeaters();
|
||||
void shadeCommand(SomfyShade &shade, const char *command, const char *source, uint32_t sourceAddress);
|
||||
void shadeCommanded(SomfyShade &shade, const char *command, const char *source, uint32_t sourceAddress);
|
||||
void memory();
|
||||
void wifi();
|
||||
void ethernet();
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ void SomfyShade::emitCommand(uint8_t num, somfy_commands cmd, const char *source
|
|||
json->addElem("sourceAddress", (uint32_t)sourceAddress);
|
||||
json->endObject();
|
||||
sockEmit.endEmit(num);
|
||||
if(num == 255) protoSocket.shadeCommand(*this, translateSomfyCommand(cmd).c_str(), source, sourceAddress);
|
||||
if(num == 255) protoSocket.shadeCommanded(*this, translateSomfyCommand(cmd).c_str(), source, sourceAddress);
|
||||
if(mqtt.connected()) {
|
||||
this->publish("cmdSource", source);
|
||||
this->publish("cmdAddress", sourceAddress);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue