mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
refactor: carry out shade requests from one module behind a LoopQueue
HTTP routes and MQTT commands now both build a shade request and submit it to the LoopQueue, which loop() drains into ShadeRequests. ShadeRequests finds the shade or group, turns client positions into stored ones, checks ranges and fills in default repeats, and reaches the shades through the ShadeOps seam: SomfyShadeOps on the device, a recording fake in the native tests. MqttCommands turns a .../set topic into a shade request. Behaviour that changes: - MQTT tiltTarget, mypos, myTiltPos and tiltPosition, and /setPositions, now honour flipPosition like the other routes. - MQTT mypos keeps the favorite tilt (was set to 0) and myTiltPos keeps the favorite lift. - repeat 0 on /shadeCommand means no repeats, as it did for groups; a JSON repeat over 127 is capped instead of wrapping to the default. - Position corrections go through the setters, so MQTT publishes them. - MQTT commands wait while the radio is full instead of being refused. - The queue is drained every loop() pass and no longer from inside a GitHub firmware download. WebCommand.h is replaced by LoopQueue.h and ShadeRequests.h. 45 new native tests, 271 in all. Flash: esp32devdbg +976 bytes, esp32dev +1,380 bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5fd5feda28
commit
395cb2d1c7
20 changed files with 1354 additions and 569 deletions
66
src/LoopQueue.cpp
Normal file
66
src/LoopQueue.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
#include "esp_log.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "ConfigFile.h"
|
||||
#include "Somfy.h"
|
||||
#include "Utils.h"
|
||||
|
||||
static const char *TAG = "LoopQueue";
|
||||
|
||||
extern SomfyShadeController somfy;
|
||||
extern ShadeRequests shadeRequests;
|
||||
extern rebootDelay_t rebootDelay;
|
||||
|
||||
static restore_options_t restoreOptions(uint8_t parts) {
|
||||
restore_options_t opts;
|
||||
opts.settings = parts & RESTORE_SETTINGS;
|
||||
opts.shades = parts & RESTORE_SHADES;
|
||||
opts.network = parts & RESTORE_NETWORK;
|
||||
opts.transceiver = parts & RESTORE_TRANSCEIVER;
|
||||
opts.repeaters = parts & RESTORE_REPEATERS;
|
||||
opts.mqtt = parts & RESTORE_MQTT;
|
||||
return opts;
|
||||
}
|
||||
|
||||
void LoopQueue::begin() {
|
||||
if(!this->queue) this->queue = xQueueCreate(LOOP_QUEUE_SIZE, sizeof(loop_item_t));
|
||||
}
|
||||
bool LoopQueue::submit(const loop_item_t &item) {
|
||||
if(!this->queue) return false;
|
||||
// async_tcp also services the sockets, so it must never wait on loop() or
|
||||
// the radio.
|
||||
if(xQueueSend(static_cast<QueueHandle_t>(this->queue), &item, 0) != pdTRUE) {
|
||||
ESP_LOGE(TAG, "Loop queue full, dropping item");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void LoopQueue::drain() {
|
||||
if(!this->queue) return;
|
||||
QueueHandle_t q = static_cast<QueueHandle_t>(this->queue);
|
||||
loop_item_t item;
|
||||
// Items run in order, so only the one at the head can be held back.
|
||||
while(xQueuePeek(q, &item, 0) == pdTRUE) {
|
||||
if(holdLoopItem(item, somfy.transceiver.txSuspended(), somfy.transceiver.txHasRoom())) break;
|
||||
// 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))
|
||||
ESP_LOGW(TAG, "Radio didn't take shade request %u", static_cast<uint8_t>(item.request.type));
|
||||
break;
|
||||
case loop_item_type_t::restore_shades: {
|
||||
restore_options_t opts = restoreOptions(item.restoreParts);
|
||||
ShadeConfigFile::restore(&somfy, "/shades.tmp", opts);
|
||||
ESP_LOGI(TAG, "Rebooting ESP for restored settings...");
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 1000;
|
||||
break;
|
||||
}
|
||||
case loop_item_type_t::load_shades:
|
||||
somfy.loadShadesFile("/shades.tmp");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/LoopQueue.h
Normal file
58
src/LoopQueue.h
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#ifndef LOOP_QUEUE_H
|
||||
#define LOOP_QUEUE_H
|
||||
#include <stdint.h>
|
||||
#include "ShadeRequests.h"
|
||||
|
||||
// The one queue through which other tasks hand work to loop(), which alone
|
||||
// changes the shade model (docs/adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md).
|
||||
// The items and the hold rule are kept free of FreeRTOS so they can be unit
|
||||
// tested on the PC (pio test -e native).
|
||||
|
||||
// Big enough for Home Assistant firing a command at every shade at once.
|
||||
#define LOOP_QUEUE_SIZE 32
|
||||
|
||||
// What a restore_shades item restores, as bits of restoreParts.
|
||||
#define RESTORE_SETTINGS 0x01
|
||||
#define RESTORE_SHADES 0x02
|
||||
#define RESTORE_NETWORK 0x04
|
||||
#define RESTORE_TRANSCEIVER 0x08
|
||||
#define RESTORE_REPEATERS 0x10
|
||||
#define RESTORE_MQTT 0x20
|
||||
|
||||
enum class loop_item_type_t : uint8_t {
|
||||
shade_request, // run by ShadeRequests
|
||||
restore_shades, // restore from /shades.tmp, then reboot
|
||||
load_shades // replace the shades with /shades.tmp
|
||||
};
|
||||
struct loop_item_t {
|
||||
loop_item_type_t type = loop_item_type_t::shade_request;
|
||||
shade_request_t request; // shade_request
|
||||
uint8_t restoreParts = 0; // restore_shades: RESTORE_* bits
|
||||
};
|
||||
|
||||
// Whether the item sends anything on air.
|
||||
inline bool loopItemUsesRadio(const loop_item_t &item) {
|
||||
return item.type == loop_item_type_t::shade_request && shadeRequestUsesRadio(item.request.type);
|
||||
}
|
||||
// Whether the item at the head of the queue waits for the radio. The client
|
||||
// was already told it was accepted so it stays queued until the radio has room
|
||||
// rather than have the radio drop it. While TX is suspended nothing gets
|
||||
// through anyway so nothing is held back then, except an unpair: the client was
|
||||
// told the shade is unpaired, so it waits until TX resumes.
|
||||
inline bool holdLoopItem(const loop_item_t &item, bool txSuspended, bool txHasRoom) {
|
||||
if(txSuspended) return item.type == loop_item_type_t::shade_request && item.request.type == shade_request_type_t::unpair;
|
||||
return loopItemUsesRadio(item) && !txHasRoom;
|
||||
}
|
||||
|
||||
class LoopQueue {
|
||||
public:
|
||||
void begin();
|
||||
// Hands the item to loop() without waiting, so it is safe from any task,
|
||||
// including async_tcp. False when the queue is full.
|
||||
bool submit(const loop_item_t &item);
|
||||
// Runs the waiting items in order. Only from loop().
|
||||
void drain();
|
||||
private:
|
||||
void *queue = nullptr;
|
||||
};
|
||||
#endif
|
||||
89
src/ShadeRequests.cpp
Normal file
89
src/ShadeRequests.cpp
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#include "ShadeRequests.h"
|
||||
#include "ShadeMotion.h"
|
||||
|
||||
bool shadeRequestUsesRadio(shade_request_type_t type) {
|
||||
switch(type) {
|
||||
case shade_request_type_t::set_positions:
|
||||
case shade_request_type_t::set_rolling_code:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
int8_t requestRepeat(int32_t repeat) {
|
||||
if(repeat < 0) return -1;
|
||||
return repeat > INT8_MAX ? INT8_MAX : static_cast<int8_t>(repeat);
|
||||
}
|
||||
int8_t requestTarget(int32_t target) { return target >= 0 && target <= 100 ? static_cast<int8_t>(target) : -1; }
|
||||
|
||||
static bool inRange(int8_t pos) { return pos >= 0 && pos <= 100; }
|
||||
// Turning a client position round is the same sum as turning a stored one.
|
||||
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) {
|
||||
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;
|
||||
break;
|
||||
default:
|
||||
if(!this->ops.findShade(req.id, shade)) return true;
|
||||
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);
|
||||
case shade_request_type_t::group_command:
|
||||
return 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);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
case shade_request_type_t::shade_sensor:
|
||||
return 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));
|
||||
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));
|
||||
}
|
||||
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;
|
||||
}
|
||||
case shade_request_type_t::set_rolling_code:
|
||||
this->ops.setRollingCode(req.id, req.rollingCode);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
101
src/ShadeRequests.h
Normal file
101
src/ShadeRequests.h
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#ifndef SHADE_REQUESTS_H
|
||||
#define SHADE_REQUESTS_H
|
||||
#include <stdint.h>
|
||||
#include "SomfyFrame.h"
|
||||
#include "ShadeTypes.h"
|
||||
|
||||
// Shade requests from the entry points (HTTP and MQTT) and the domain module
|
||||
// that carries them out on loop(). Kept free of the shade model so it can be
|
||||
// unit tested on the PC (pio test -e native).
|
||||
|
||||
enum class shade_request_type_t : uint8_t {
|
||||
shade_command, // move the shade to target, or send command
|
||||
group_command, // send command to the group
|
||||
tilt_command, // move the tilt to target, or send command to the tilt
|
||||
shade_repeat, // send command, or repeat it when it was the last one sent
|
||||
group_repeat, // the same for a group
|
||||
set_positions, // correct where the shade is thought to be; sends nothing
|
||||
shade_sensor, // send a sensor frame as the shade
|
||||
group_sensor, // send a sensor frame as the group
|
||||
set_favorite, // set or clear the shade's favorite
|
||||
unpair, // send Prog and clear paired
|
||||
set_rolling_code // set the shade's rolling code; sends nothing
|
||||
};
|
||||
|
||||
// Positions are as the client sees them: 0-100, counted from the other end on
|
||||
// a shade with flipPosition, and -1 when not given.
|
||||
struct shade_request_t {
|
||||
shade_request_type_t type = shade_request_type_t::shade_command;
|
||||
uint8_t id = 255; // shade id, or group id for group_*
|
||||
int8_t target = -1; // shade/tilt_command: -1 sends command instead
|
||||
somfy_commands command = somfy_commands::My;
|
||||
int8_t repeat = -1; // -1 for the shade's or group's own repeats
|
||||
uint8_t stepSize = 0;
|
||||
int8_t position = -1; // set_positions, set_favorite
|
||||
int8_t tiltPosition = -1; // set_positions, set_favorite
|
||||
int8_t sunny = -1; // *_sensor: -1 leaves it as it is
|
||||
int8_t windy = -1;
|
||||
uint16_t rollingCode = 0; // set_rolling_code
|
||||
};
|
||||
|
||||
// Whether the request sends anything on air.
|
||||
bool shadeRequestUsesRadio(shade_request_type_t type);
|
||||
// A repeat count from a client: negative for the default, capped at what fits.
|
||||
int8_t requestRepeat(int32_t repeat);
|
||||
// A target from a client: -1 unless it is 0-100.
|
||||
int8_t requestTarget(int32_t target);
|
||||
|
||||
// What ShadeRequests needs to know about a shade. Positions are stored ones.
|
||||
struct shade_info_t {
|
||||
bool flipPosition = false;
|
||||
shade_types shadeType = shade_types::roller;
|
||||
tilt_types tiltType = tilt_types::none;
|
||||
uint8_t repeats = 1;
|
||||
uint8_t bitLength = 56;
|
||||
float currentPos = 0.0f;
|
||||
float myPos = -1.0f;
|
||||
float myTiltPos = -1.0f;
|
||||
};
|
||||
|
||||
// The seam between ShadeRequests and the shades and groups it acts on. On the
|
||||
// device it is the shade model (SomfyShadeOps); the tests record the calls.
|
||||
// Positions are stored ones, so already turned round for a flipped shade.
|
||||
class ShadeOps {
|
||||
public:
|
||||
virtual ~ShadeOps() {}
|
||||
virtual bool findShade(uint8_t shadeId, shade_info_t &info) = 0;
|
||||
// repeats is the group's own repeat count.
|
||||
virtual bool findGroup(uint8_t groupId, uint8_t &repeats) = 0;
|
||||
// The bool ones return false when the radio refused a command they sent.
|
||||
virtual bool moveToTarget(uint8_t shadeId, float pos) = 0;
|
||||
virtual bool moveToTiltTarget(uint8_t shadeId, float tilt) = 0;
|
||||
virtual bool sendCommand(uint8_t shadeId, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) = 0;
|
||||
virtual bool sendTiltCommand(uint8_t shadeId, somfy_commands cmd) = 0;
|
||||
virtual bool isLastCommand(uint8_t shadeId, somfy_commands cmd) = 0;
|
||||
virtual bool repeatFrame(uint8_t shadeId, uint8_t repeat) = 0;
|
||||
virtual bool sendSensorCommand(uint8_t shadeId, int8_t windy, int8_t sunny, uint8_t repeat) = 0;
|
||||
virtual bool setMyPosition(uint8_t shadeId, int8_t pos, int8_t tilt) = 0;
|
||||
// A negative position leaves that one as it is.
|
||||
virtual void setPositions(uint8_t shadeId, float pos, float tilt) = 0;
|
||||
virtual void setUnpaired(uint8_t shadeId) = 0;
|
||||
virtual void setRollingCode(uint8_t shadeId, uint16_t code) = 0;
|
||||
virtual bool sendGroupCommand(uint8_t groupId, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) = 0;
|
||||
virtual bool isLastGroupCommand(uint8_t groupId, somfy_commands cmd) = 0;
|
||||
virtual bool repeatGroupFrame(uint8_t groupId, uint8_t repeat) = 0;
|
||||
virtual bool sendGroupSensorCommand(uint8_t groupId, int8_t windy, int8_t sunny, uint8_t repeat) = 0;
|
||||
};
|
||||
|
||||
// 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);
|
||||
private:
|
||||
ShadeOps &ops;
|
||||
};
|
||||
#endif
|
||||
|
|
@ -9,6 +9,9 @@
|
|||
#include "Utils.h"
|
||||
#include "Somfy.h"
|
||||
#include "entryPoints/MQTT.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "ShadeRequests.h"
|
||||
#include "SomfyShadeOps.h"
|
||||
#include "GitOTA.h"
|
||||
#include "esp_core_dump.h"
|
||||
#include "esp_system.h"
|
||||
|
|
@ -33,6 +36,9 @@ rebootDelay_t rebootDelay;
|
|||
SomfyShadeController somfy;
|
||||
MQTTClass mqtt;
|
||||
GitUpdater git;
|
||||
SomfyShadeOps shadeOps;
|
||||
ShadeRequests shadeRequests(shadeOps);
|
||||
LoopQueue loopQueue;
|
||||
|
||||
uint32_t oldheap = 0;
|
||||
|
||||
|
|
@ -112,6 +118,8 @@ void setup() {
|
|||
settings.begin();
|
||||
if(WiFi.status() == WL_CONNECTED) WiFi.disconnect(true);
|
||||
delay(10);
|
||||
// Before anything that can submit to it: the web servers and MQTT.
|
||||
loopQueue.begin();
|
||||
webServer.startup();
|
||||
webServer.begin();
|
||||
delay(1000);
|
||||
|
|
@ -145,6 +153,9 @@ void loop() {
|
|||
somfy.loop();
|
||||
logIfSlow(TAG, "Loop somfy", timing.lap(), LOOP_SLOW_MS);
|
||||
esp_task_wdt_reset();
|
||||
loopQueue.drain();
|
||||
logIfSlow(TAG, "Loop queue", timing.lap(), LOOP_SLOW_MS);
|
||||
esp_task_wdt_reset();
|
||||
if(net.connected() || net.softAPOpened) {
|
||||
if(!rebootDelay.reboot && net.connected() && !net.softAPOpened) {
|
||||
git.loop();
|
||||
|
|
|
|||
70
src/SomfyShadeOps.cpp
Normal file
70
src/SomfyShadeOps.cpp
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#include "SomfyShadeOps.h"
|
||||
#include "Somfy.h"
|
||||
|
||||
extern SomfyShadeController somfy;
|
||||
|
||||
// ShadeRequests only calls these for a shade or group it has just found.
|
||||
bool SomfyShadeOps::findShade(uint8_t shadeId, shade_info_t &info) {
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) return false;
|
||||
info.flipPosition = shade->flipPosition;
|
||||
info.shadeType = shade->shadeType;
|
||||
info.tiltType = shade->tiltType;
|
||||
info.repeats = shade->repeats;
|
||||
info.bitLength = shade->bitLength;
|
||||
info.currentPos = shade->currentPos;
|
||||
info.myPos = shade->myPos;
|
||||
info.myTiltPos = shade->myTiltPos;
|
||||
return true;
|
||||
}
|
||||
bool SomfyShadeOps::findGroup(uint8_t groupId, uint8_t &repeats) {
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(!group) return false;
|
||||
repeats = group->repeats;
|
||||
return true;
|
||||
}
|
||||
bool SomfyShadeOps::moveToTarget(uint8_t shadeId, float pos) { return somfy.getShadeById(shadeId)->moveToTarget(pos); }
|
||||
bool SomfyShadeOps::moveToTiltTarget(uint8_t shadeId, float tilt) { return somfy.getShadeById(shadeId)->moveToTiltTarget(tilt); }
|
||||
bool SomfyShadeOps::sendCommand(uint8_t shadeId, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) {
|
||||
return somfy.getShadeById(shadeId)->sendCommand(cmd, repeat, stepSize);
|
||||
}
|
||||
bool SomfyShadeOps::sendTiltCommand(uint8_t shadeId, somfy_commands cmd) { return somfy.getShadeById(shadeId)->sendTiltCommand(cmd); }
|
||||
bool SomfyShadeOps::isLastCommand(uint8_t shadeId, somfy_commands cmd) { return somfy.getShadeById(shadeId)->isLastCommand(cmd); }
|
||||
bool SomfyShadeOps::repeatFrame(uint8_t shadeId, uint8_t repeat) { return somfy.getShadeById(shadeId)->repeatFrame(repeat); }
|
||||
bool SomfyShadeOps::sendSensorCommand(uint8_t shadeId, int8_t windy, int8_t sunny, uint8_t repeat) {
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
const bool sent = shade->sendSensorCommand(windy, sunny, repeat);
|
||||
shade->emitState();
|
||||
return sent;
|
||||
}
|
||||
bool SomfyShadeOps::setMyPosition(uint8_t shadeId, int8_t pos, int8_t tilt) { return somfy.getShadeById(shadeId)->setMyPosition(pos, tilt); }
|
||||
void SomfyShadeOps::setPositions(uint8_t shadeId, float pos, float tilt) {
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
// Through the setters, so MQTT hears about it too.
|
||||
if(pos >= 0.0f) {
|
||||
shade->p_target(pos);
|
||||
shade->p_currentPos(pos);
|
||||
}
|
||||
if(tilt >= 0.0f) {
|
||||
shade->p_tiltTarget(tilt);
|
||||
shade->p_currentTiltPos(tilt);
|
||||
}
|
||||
shade->emitState();
|
||||
}
|
||||
void SomfyShadeOps::setUnpaired(uint8_t shadeId) {
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
shade->paired = false;
|
||||
shade->save();
|
||||
}
|
||||
void SomfyShadeOps::setRollingCode(uint8_t shadeId, uint16_t code) { somfy.getShadeById(shadeId)->setRollingCode(code); }
|
||||
bool SomfyShadeOps::sendGroupCommand(uint8_t groupId, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) {
|
||||
return somfy.getGroupById(groupId)->sendCommand(cmd, repeat, stepSize);
|
||||
}
|
||||
bool SomfyShadeOps::isLastGroupCommand(uint8_t groupId, somfy_commands cmd) { return somfy.getGroupById(groupId)->isLastCommand(cmd); }
|
||||
bool SomfyShadeOps::repeatGroupFrame(uint8_t groupId, uint8_t repeat) { return somfy.getGroupById(groupId)->repeatFrame(repeat); }
|
||||
bool SomfyShadeOps::sendGroupSensorCommand(uint8_t groupId, int8_t windy, int8_t sunny, uint8_t repeat) {
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
const bool sent = group->sendSensorCommand(windy, sunny, repeat);
|
||||
group->emitState();
|
||||
return sent;
|
||||
}
|
||||
27
src/SomfyShadeOps.h
Normal file
27
src/SomfyShadeOps.h
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#ifndef SOMFY_SHADE_OPS_H
|
||||
#define SOMFY_SHADE_OPS_H
|
||||
#include "ShadeRequests.h"
|
||||
|
||||
// ShadeOps on the device: forwards to the shades and groups of the shade
|
||||
// model. Runs on loop() only.
|
||||
class SomfyShadeOps : public ShadeOps {
|
||||
public:
|
||||
bool findShade(uint8_t shadeId, shade_info_t &info) override;
|
||||
bool findGroup(uint8_t groupId, uint8_t &repeats) override;
|
||||
bool moveToTarget(uint8_t shadeId, float pos) override;
|
||||
bool moveToTiltTarget(uint8_t shadeId, float tilt) override;
|
||||
bool sendCommand(uint8_t shadeId, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) override;
|
||||
bool sendTiltCommand(uint8_t shadeId, somfy_commands cmd) override;
|
||||
bool isLastCommand(uint8_t shadeId, somfy_commands cmd) override;
|
||||
bool repeatFrame(uint8_t shadeId, uint8_t repeat) override;
|
||||
bool sendSensorCommand(uint8_t shadeId, int8_t windy, int8_t sunny, uint8_t repeat) override;
|
||||
bool setMyPosition(uint8_t shadeId, int8_t pos, int8_t tilt) override;
|
||||
void setPositions(uint8_t shadeId, float pos, float tilt) override;
|
||||
void setUnpaired(uint8_t shadeId) override;
|
||||
void setRollingCode(uint8_t shadeId, uint16_t code) override;
|
||||
bool sendGroupCommand(uint8_t groupId, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) override;
|
||||
bool isLastGroupCommand(uint8_t groupId, somfy_commands cmd) override;
|
||||
bool repeatGroupFrame(uint8_t groupId, uint8_t repeat) override;
|
||||
bool sendGroupSensorCommand(uint8_t groupId, int8_t windy, int8_t sunny, uint8_t repeat) override;
|
||||
};
|
||||
#endif
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
#ifndef WEB_COMMAND_H
|
||||
#define WEB_COMMAND_H
|
||||
#include <stdint.h>
|
||||
#include "SomfyFrame.h"
|
||||
|
||||
// Commands the web servers hand to the main loop (src/entryPoints/Web.cpp). Kept free of
|
||||
// the web server and FreeRTOS so it can be unit tested on the PC
|
||||
// (pio test -e native).
|
||||
|
||||
// Commands are queued by the async web server and drained by the main loop.
|
||||
// Big enough for Home Assistant firing a command at every shade at once.
|
||||
#define WEB_CMD_QUEUE_SIZE 32
|
||||
|
||||
enum class web_cmd_t : uint8_t {
|
||||
shade_command, // moveToTarget or sendCommand
|
||||
group_command, // group sendCommand
|
||||
tilt_command, // moveToTiltTarget or sendTiltCommand
|
||||
shade_repeat, // shade sendCommand/repeatFrame
|
||||
group_repeat, // group sendCommand/repeatFrame
|
||||
set_positions, // set shade position directly
|
||||
shade_sensor, // shade sensor command
|
||||
group_sensor, // group sensor command
|
||||
set_my_position, // shade setMyPosition (position/tiltPosition)
|
||||
unpair_shade, // shade unpair (Prog) and clear paired flag
|
||||
set_rolling_code, // set a shade's rolling code
|
||||
restore_shades, // restore from /shades.tmp, then reboot
|
||||
load_shades, // replace the shades with /shades.tmp
|
||||
};
|
||||
|
||||
// What a restore_shades command restores, as bits of restoreParts.
|
||||
#define RESTORE_SETTINGS 0x01
|
||||
#define RESTORE_SHADES 0x02
|
||||
#define RESTORE_NETWORK 0x04
|
||||
#define RESTORE_TRANSCEIVER 0x08
|
||||
#define RESTORE_REPEATERS 0x10
|
||||
#define RESTORE_MQTT 0x20
|
||||
|
||||
struct web_command_t {
|
||||
web_cmd_t type;
|
||||
uint8_t shadeId;
|
||||
uint8_t groupId;
|
||||
uint8_t target; // 0-100 or 255 (none)
|
||||
somfy_commands command;
|
||||
int8_t repeat;
|
||||
uint8_t stepSize;
|
||||
int8_t position; // for setPositions
|
||||
int8_t tiltPosition; // for setPositions/tilt
|
||||
int8_t sunny; // for sensor
|
||||
int8_t windy; // for sensor
|
||||
uint16_t rollingCode; // for set_rolling_code
|
||||
uint8_t restoreParts; // for restore_shades: RESTORE_* bits
|
||||
};
|
||||
|
||||
// Whether the command sends anything on air.
|
||||
inline bool webCommandUsesRadio(web_cmd_t type) {
|
||||
switch(type) {
|
||||
case web_cmd_t::set_positions:
|
||||
case web_cmd_t::set_rolling_code:
|
||||
case web_cmd_t::restore_shades:
|
||||
case web_cmd_t::load_shades:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Whether the command at the head of the queue waits for the radio. The
|
||||
// client was already told it was accepted so it stays queued until the radio
|
||||
// has room rather than have the radio drop it. While TX is suspended nothing
|
||||
// gets through anyway so nothing is held back then, except an unpair: the
|
||||
// client was told the shade is unpaired, so it waits until TX resumes.
|
||||
inline bool holdWebCommand(web_cmd_t type, bool txSuspended, bool txHasRoom) {
|
||||
if(txSuspended) return type == web_cmd_t::unpair_shade;
|
||||
return webCommandUsesRadio(type) && !txHasRoom;
|
||||
}
|
||||
#endif
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
#include "esp_log.h"
|
||||
#include "ConfigSettings.h"
|
||||
#include "MQTT.h"
|
||||
#include "MqttCommands.h"
|
||||
#include "Somfy.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "ESPNetwork.h"
|
||||
#include "Utils.h"
|
||||
|
||||
|
|
@ -21,6 +23,7 @@ extern ConfigSettings settings;
|
|||
extern SomfyShadeController somfy;
|
||||
extern ESPNetwork net;
|
||||
extern rebootDelay_t rebootDelay;
|
||||
extern LoopQueue loopQueue;
|
||||
|
||||
|
||||
bool MQTTClass::begin() {
|
||||
|
|
@ -49,131 +52,14 @@ bool MQTTClass::loop() {
|
|||
void MQTTClass::receive(const char *topic, byte*payload, uint32_t length) {
|
||||
esp_task_wdt_reset(); // Make sure we do not reboot here.
|
||||
ESP_LOGD(TAG, "MQTT Topic:%s payload:%.*s", topic, (int)length, (const char *)payload);
|
||||
|
||||
// We need to start at the last slash in the data
|
||||
uint8_t len = strlen(topic);
|
||||
|
||||
uint8_t slashes = 0;
|
||||
uint16_t ndx = strlen(topic) - 1;
|
||||
while(ndx > 0) {
|
||||
if(topic[ndx] == '/') slashes++;
|
||||
if(slashes == 4) break;
|
||||
ndx--;
|
||||
// This already runs on loop(), but going through the loop queue keeps MQTT
|
||||
// and web requests in order and holds them while the radio is full.
|
||||
loop_item_t item;
|
||||
if(!parseMqttCommand(topic, payload, length, item.request)) {
|
||||
ESP_LOGW(TAG, "MQTT command not understood: %s", topic);
|
||||
return;
|
||||
}
|
||||
char entityId[4];
|
||||
char command[32];
|
||||
char entityType[7];
|
||||
char value[10];
|
||||
memset(command, 0x00, sizeof(command));
|
||||
memset(entityId, 0x00, sizeof(entityId));
|
||||
memset(entityType, 0x00, sizeof(entityType));
|
||||
memset(value, 0x00, sizeof(value));
|
||||
uint8_t i = 0;
|
||||
while(topic[ndx] == '/' && ndx < len) ndx++;
|
||||
while(ndx < len) {
|
||||
if(topic[ndx] != '/' && i < sizeof(entityType))
|
||||
entityType[i++] = topic[ndx];
|
||||
ndx++;
|
||||
if(topic[ndx] == '/') break;
|
||||
}
|
||||
i = 0;
|
||||
while(topic[ndx] == '/' && ndx < len) ndx++;
|
||||
while(ndx < len) {
|
||||
if(topic[ndx] != '/' && i < sizeof(entityId))
|
||||
entityId[i++] = topic[ndx];
|
||||
ndx++;
|
||||
if(topic[ndx] == '/') break;
|
||||
}
|
||||
i = 0;
|
||||
while(topic[ndx] == '/' && ndx < len) ndx++;
|
||||
while(ndx < len) {
|
||||
if(topic[ndx] != '/' && i < sizeof(command))
|
||||
command[i++] = topic[ndx];
|
||||
ndx++;
|
||||
if(topic[ndx] == '/') break;
|
||||
}
|
||||
for(uint8_t j = 0; j < length && j < sizeof(value); j++)
|
||||
value[j] = payload[j];
|
||||
|
||||
ESP_LOGD(TAG, "MQTT type:[%s] command:[%s] entityId:%s value:%s", entityType, command, entityId, value);
|
||||
if(strncmp(entityType, "shades", sizeof(entityType)) == 0) {
|
||||
SomfyShade* shade = somfy.getShadeById(atoi(entityId));
|
||||
if (shade) {
|
||||
int val = atoi(value);
|
||||
if(strncmp(command, "target", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100) {
|
||||
ESP_LOGI(TAG, "MQTT shade %s target=%d", entityId, val);
|
||||
shade->moveToTarget(shade->transformPosition(atoi(value)));
|
||||
}
|
||||
}
|
||||
if(strncmp(command, "tiltTarget", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->moveToTiltTarget(atoi(value));
|
||||
}
|
||||
else if(strncmp(command, "direction", sizeof(command)) == 0) {
|
||||
if(val < 0)
|
||||
shade->sendCommand(somfy_commands::Up);
|
||||
else if(val > 0)
|
||||
shade->sendCommand(somfy_commands::Down);
|
||||
else
|
||||
shade->sendCommand(somfy_commands::My);
|
||||
}
|
||||
else if(strncmp(command, "mypos", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->setMyPosition(val);
|
||||
}
|
||||
else if(strncmp(command, "myTiltPos", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100)
|
||||
shade->setMyPosition(shade->myPos, val);
|
||||
}
|
||||
else if(strncmp(command, "sunFlag", sizeof(command)) == 0) {
|
||||
shade->sendCommand(sunFlagCommand(val > 0));
|
||||
}
|
||||
else if(strncmp(command, "position", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100) {
|
||||
shade->target = shade->currentPos = shade->transformPosition((float)val);
|
||||
shade->emitState();
|
||||
}
|
||||
}
|
||||
else if(strncmp(command, "tiltPosition", sizeof(command)) == 0) {
|
||||
if(val >= 0 && val <= 100) {
|
||||
shade->tiltTarget = shade->currentTiltPos = (float)val;
|
||||
shade->emitState();
|
||||
}
|
||||
}
|
||||
else if(strncmp(command, "sunny", sizeof(command)) == 0) {
|
||||
if(val >= 0) shade->sendSensorCommand(-1, val, shade->repeats);
|
||||
}
|
||||
else if(strncmp(command, "windy", sizeof(command)) == 0) {
|
||||
if(val >= 0) shade->sendSensorCommand(val, -1, shade->repeats);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(strncmp(entityType, "groups", sizeof(entityType)) == 0) {
|
||||
SomfyGroup* group = somfy.getGroupById(atoi(entityId));
|
||||
if (group) {
|
||||
int val = atoi(value);
|
||||
ESP_LOGI(TAG, "MQTT group %s command=%s value=%d", entityId, command, val);
|
||||
if(strncmp(command, "direction", sizeof(command)) == 0) {
|
||||
if(val < 0)
|
||||
group->sendCommand(somfy_commands::Up);
|
||||
else if(val > 0)
|
||||
group->sendCommand(somfy_commands::Down);
|
||||
else
|
||||
group->sendCommand(somfy_commands::My);
|
||||
}
|
||||
else if(strncmp(command, "sunFlag", sizeof(command)) == 0) {
|
||||
group->sendCommand(sunFlagCommand(val > 0));
|
||||
}
|
||||
else if(strncmp(command, "sunny", sizeof(command)) == 0) {
|
||||
if(val >= 0) group->sendSensorCommand(-1, val, group->repeats);
|
||||
}
|
||||
else if(strncmp(command, "windy", sizeof(command)) == 0) {
|
||||
if(val >= 0) group->sendSensorCommand(val, -1, group->repeats);
|
||||
}
|
||||
}
|
||||
}
|
||||
esp_task_wdt_reset(); // Make sure we do not reboot here.
|
||||
loopQueue.submit(item);
|
||||
}
|
||||
bool MQTTClass::connect() {
|
||||
esp_task_wdt_reset(); // Make sure we do not reboot here.
|
||||
|
|
|
|||
126
src/entryPoints/MqttCommands.cpp
Normal file
126
src/entryPoints/MqttCommands.cpp
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "MqttCommands.h"
|
||||
|
||||
namespace {
|
||||
struct segment_t {
|
||||
const char *s = "";
|
||||
size_t len = 0;
|
||||
bool is(const char *word) const { return this->len == strlen(word) && strncmp(this->s, word, this->len) == 0; }
|
||||
};
|
||||
bool inRange(int val) { return val >= 0 && val <= 100; }
|
||||
somfy_commands directionCommand(int val) {
|
||||
if(val < 0) return somfy_commands::Up;
|
||||
if(val > 0) return somfy_commands::Down;
|
||||
return somfy_commands::My;
|
||||
}
|
||||
}
|
||||
|
||||
bool parseMqttCommand(const char *topic, const uint8_t *payload, size_t length, shade_request_t &req) {
|
||||
// Only the last four segments count, so the root topic may hold slashes or
|
||||
// be empty: type, id, command and "set".
|
||||
segment_t seg[4];
|
||||
size_t end = strlen(topic);
|
||||
for(int i = 3; i >= 0; i--) {
|
||||
size_t start = end;
|
||||
while(start > 0 && topic[start - 1] != '/') start--;
|
||||
seg[i].s = topic + start;
|
||||
seg[i].len = end - start;
|
||||
if(i > 0) {
|
||||
if(start == 0) return false;
|
||||
end = start - 1;
|
||||
}
|
||||
}
|
||||
if(!seg[3].is("set")) return false;
|
||||
if(seg[1].len == 0 || seg[1].len > 3) return false;
|
||||
int id = 0;
|
||||
for(size_t i = 0; i < seg[1].len; i++) {
|
||||
if(seg[1].s[i] < '0' || seg[1].s[i] > '9') return false;
|
||||
id = id * 10 + (seg[1].s[i] - '0');
|
||||
}
|
||||
if(id > 255) return false;
|
||||
char value[16];
|
||||
const size_t n = length < sizeof(value) - 1 ? length : sizeof(value) - 1;
|
||||
memcpy(value, payload, n);
|
||||
value[n] = '\0';
|
||||
const int val = atoi(value);
|
||||
const segment_t &cmd = seg[2];
|
||||
|
||||
shade_request_t r;
|
||||
r.id = static_cast<uint8_t>(id);
|
||||
if(seg[0].is("shades")) {
|
||||
if(cmd.is("target")) {
|
||||
if(!inRange(val)) return false;
|
||||
r.type = shade_request_type_t::shade_command;
|
||||
r.target = static_cast<int8_t>(val);
|
||||
}
|
||||
else if(cmd.is("tiltTarget")) {
|
||||
if(!inRange(val)) return false;
|
||||
r.type = shade_request_type_t::tilt_command;
|
||||
r.target = static_cast<int8_t>(val);
|
||||
}
|
||||
else if(cmd.is("direction")) {
|
||||
r.type = shade_request_type_t::shade_command;
|
||||
r.command = directionCommand(val);
|
||||
}
|
||||
else if(cmd.is("mypos")) {
|
||||
if(!inRange(val)) return false;
|
||||
r.type = shade_request_type_t::set_favorite;
|
||||
r.position = static_cast<int8_t>(val);
|
||||
}
|
||||
else if(cmd.is("myTiltPos")) {
|
||||
if(!inRange(val)) return false;
|
||||
r.type = shade_request_type_t::set_favorite;
|
||||
r.tiltPosition = static_cast<int8_t>(val);
|
||||
}
|
||||
else if(cmd.is("sunFlag")) {
|
||||
r.type = shade_request_type_t::shade_command;
|
||||
r.command = sunFlagCommand(val > 0);
|
||||
}
|
||||
else if(cmd.is("sunny")) {
|
||||
if(val < 0) return false;
|
||||
r.type = shade_request_type_t::shade_sensor;
|
||||
r.sunny = val > 0 ? 1 : 0;
|
||||
}
|
||||
else if(cmd.is("windy")) {
|
||||
if(val < 0) return false;
|
||||
r.type = shade_request_type_t::shade_sensor;
|
||||
r.windy = val > 0 ? 1 : 0;
|
||||
}
|
||||
else if(cmd.is("position")) {
|
||||
if(!inRange(val)) return false;
|
||||
r.type = shade_request_type_t::set_positions;
|
||||
r.position = static_cast<int8_t>(val);
|
||||
}
|
||||
else if(cmd.is("tiltPosition")) {
|
||||
if(!inRange(val)) return false;
|
||||
r.type = shade_request_type_t::set_positions;
|
||||
r.tiltPosition = static_cast<int8_t>(val);
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
else if(seg[0].is("groups")) {
|
||||
if(cmd.is("direction")) {
|
||||
r.type = shade_request_type_t::group_command;
|
||||
r.command = directionCommand(val);
|
||||
}
|
||||
else if(cmd.is("sunFlag")) {
|
||||
r.type = shade_request_type_t::group_command;
|
||||
r.command = sunFlagCommand(val > 0);
|
||||
}
|
||||
else if(cmd.is("sunny")) {
|
||||
if(val < 0) return false;
|
||||
r.type = shade_request_type_t::group_sensor;
|
||||
r.sunny = val > 0 ? 1 : 0;
|
||||
}
|
||||
else if(cmd.is("windy")) {
|
||||
if(val < 0) return false;
|
||||
r.type = shade_request_type_t::group_sensor;
|
||||
r.windy = val > 0 ? 1 : 0;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
else return false;
|
||||
req = r;
|
||||
return true;
|
||||
}
|
||||
14
src/entryPoints/MqttCommands.h
Normal file
14
src/entryPoints/MqttCommands.h
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#ifndef MQTT_COMMANDS_H
|
||||
#define MQTT_COMMANDS_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "ShadeRequests.h"
|
||||
|
||||
// The MQTT command topics, <root>/shades/<id>/<command>/set and
|
||||
// <root>/groups/<id>/<command>/set. Kept free of the MQTT client so it can be
|
||||
// unit tested on the PC (pio test -e native).
|
||||
|
||||
// Turns a message on a command topic into a shade request. False for a topic
|
||||
// or value it doesn't take, which is dropped.
|
||||
bool parseMqttCommand(const char *topic, const uint8_t *payload, size_t length, shade_request_t &req);
|
||||
#endif
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
#include "SSDP.h"
|
||||
#include "Somfy.h"
|
||||
#include "WResp.h"
|
||||
#include "LoopQueue.h"
|
||||
#include "Web.h"
|
||||
#include "MQTT.h"
|
||||
#include "GitOTA.h"
|
||||
|
|
@ -28,6 +29,7 @@ extern Web webServer;
|
|||
extern MQTTClass mqtt;
|
||||
extern GitUpdater git;
|
||||
extern ESPNetwork net;
|
||||
extern LoopQueue loopQueue;
|
||||
|
||||
//#define WEB_MAX_RESPONSE 34768
|
||||
#define WEB_MAX_RESPONSE 4096
|
||||
|
|
@ -47,61 +49,53 @@ static const char _encoding_json[] = "application/json";
|
|||
|
||||
static const char *TAG = "Web";
|
||||
|
||||
static QueueHandle_t webCmdQueue = nullptr;
|
||||
|
||||
static void sendQueueFull(AsyncWebServerRequest *request);
|
||||
static void sendRadioUnavailable(AsyncWebServerRequest *request);
|
||||
// Hands a command to the main loop, which owns the shades. When it can't be
|
||||
// the request is answered with the reason and false is returned.
|
||||
static bool queueLoopCommand(AsyncWebServerRequest *request, const web_command_t &cmd) {
|
||||
// Hands an item to loop(), which owns the shades. When it can't be the
|
||||
// request is answered with the reason and false is returned.
|
||||
static bool queueLoopItem(AsyncWebServerRequest *request, const loop_item_t &item) {
|
||||
// Nothing can go on air during a frequency scan so tell the client now rather
|
||||
// than dropping the command once it reaches the radio.
|
||||
if(webCommandUsesRadio(cmd.type) && somfy.transceiver.txSuspended()) { sendRadioUnavailable(request); return false; }
|
||||
if(!webServer.queueCommand(cmd)) { sendQueueFull(request); return false; }
|
||||
// than dropping the request once it reaches the radio.
|
||||
if(loopItemUsesRadio(item) && somfy.transceiver.txSuspended()) { sendRadioUnavailable(request); return false; }
|
||||
if(!loopQueue.submit(item)) { sendQueueFull(request); return false; }
|
||||
return true;
|
||||
}
|
||||
// Radio work for these handlers runs on the main loop via the command queue.
|
||||
static bool queueShadeRequest(AsyncWebServerRequest *request, const shade_request_t &req) {
|
||||
loop_item_t item;
|
||||
item.request = req;
|
||||
return queueLoopItem(request, item);
|
||||
}
|
||||
static bool queueSetMyPosition(AsyncWebServerRequest *request, SomfyShade *shade, int8_t pos, int8_t tilt) {
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::set_my_position;
|
||||
cmd.shadeId = shade->getShadeId();
|
||||
cmd.position = pos;
|
||||
cmd.tiltPosition = tilt;
|
||||
return queueLoopCommand(request, cmd);
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::set_favorite;
|
||||
req.id = shade->getShadeId();
|
||||
req.position = pos;
|
||||
req.tiltPosition = tilt;
|
||||
return queueShadeRequest(request, req);
|
||||
}
|
||||
// The main loop clears paired and saves it once the Prog is queued. The
|
||||
// shade belongs to the main loop so it isn't changed here.
|
||||
// loop() clears paired and saves it once the Prog is queued. The shade
|
||||
// belongs to loop() so it isn't changed here.
|
||||
static bool queueUnpairShade(AsyncWebServerRequest *request, SomfyShade *shade) {
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::unpair_shade;
|
||||
cmd.shadeId = shade->getShadeId();
|
||||
return queueLoopCommand(request, cmd);
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::unpair;
|
||||
req.id = shade->getShadeId();
|
||||
return queueShadeRequest(request, req);
|
||||
}
|
||||
// Sending a command reads and commits the rolling code on the main loop, so
|
||||
// a new code is set there too.
|
||||
// Sending a command reads and commits the rolling code on loop(), so a new
|
||||
// code is set there too.
|
||||
static bool queueSetRollingCode(AsyncWebServerRequest *request, SomfyShade *shade, uint16_t rollingCode) {
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::set_rolling_code;
|
||||
cmd.shadeId = shade->getShadeId();
|
||||
cmd.rollingCode = rollingCode;
|
||||
return queueLoopCommand(request, cmd);
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::set_rolling_code;
|
||||
req.id = shade->getShadeId();
|
||||
req.rollingCode = rollingCode;
|
||||
return queueShadeRequest(request, req);
|
||||
}
|
||||
// A restore's options travel through the command queue as RESTORE_* bits.
|
||||
// A restore's options travel through the loop queue as RESTORE_* bits.
|
||||
static uint8_t restoreParts(const restore_options_t &opts) {
|
||||
return (opts.settings ? RESTORE_SETTINGS : 0) | (opts.shades ? RESTORE_SHADES : 0) |
|
||||
(opts.network ? RESTORE_NETWORK : 0) | (opts.transceiver ? RESTORE_TRANSCEIVER : 0) |
|
||||
(opts.repeaters ? RESTORE_REPEATERS : 0) | (opts.mqtt ? RESTORE_MQTT : 0);
|
||||
}
|
||||
static restore_options_t restoreOptions(uint8_t parts) {
|
||||
restore_options_t opts;
|
||||
opts.settings = parts & RESTORE_SETTINGS;
|
||||
opts.shades = parts & RESTORE_SHADES;
|
||||
opts.network = parts & RESTORE_NETWORK;
|
||||
opts.transceiver = parts & RESTORE_TRANSCEIVER;
|
||||
opts.repeaters = parts & RESTORE_REPEATERS;
|
||||
opts.mqtt = parts & RESTORE_MQTT;
|
||||
return opts;
|
||||
}
|
||||
// The radio and MQTT are shut down once an upload starts and only a reboot
|
||||
// brings them back. The completion handler schedules it but never runs if the
|
||||
// client goes away mid-upload, so make sure the device reboots then too.
|
||||
|
|
@ -125,7 +119,6 @@ AsyncWebServer asyncServer(80);
|
|||
AsyncWebServer asyncApiServer(8081);
|
||||
void Web::startup() {
|
||||
ESP_LOGI(TAG, "Launching web server...");
|
||||
if(!webCmdQueue) webCmdQueue = xQueueCreate(WEB_CMD_QUEUE_SIZE, sizeof(web_command_t));
|
||||
|
||||
asyncServer.on("/loginContext", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
AsyncJsonResponse *response = new AsyncJsonResponse();
|
||||
|
|
@ -143,128 +136,8 @@ void Web::startup() {
|
|||
ESP_LOGI(TAG, "Async API server started on port 8081");
|
||||
}
|
||||
void Web::loop() {
|
||||
this->processQueue();
|
||||
delay(1);
|
||||
}
|
||||
bool Web::queueCommand(const web_command_t &cmd) {
|
||||
if(!webCmdQueue) return false;
|
||||
// This runs on the async_tcp task which also services the sockets so it must
|
||||
// never wait on the main loop or the radio.
|
||||
if(xQueueSend(webCmdQueue, &cmd, 0) != pdTRUE) {
|
||||
ESP_LOGE(TAG, "Command queue full, dropping command");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void Web::processQueue() {
|
||||
if(!webCmdQueue) return;
|
||||
web_command_t cmd;
|
||||
// Commands run in order, so only the one at the head can be held back.
|
||||
while(xQueuePeek(webCmdQueue, &cmd, 0) == pdTRUE) {
|
||||
if(holdWebCommand(cmd.type, somfy.transceiver.txSuspended(), somfy.transceiver.txHasRoom())) break;
|
||||
// loop() is the only consumer so this is the command we peeked.
|
||||
xQueueReceive(webCmdQueue, &cmd, 0);
|
||||
bool sent = true;
|
||||
switch(cmd.type) {
|
||||
case web_cmd_t::shade_command: {
|
||||
SomfyShade *shade = somfy.getShadeById(cmd.shadeId);
|
||||
if(shade) {
|
||||
if(cmd.target <= 100) sent = shade->moveToTarget(shade->transformPosition(cmd.target));
|
||||
else sent = shade->sendCommand(cmd.command, cmd.repeat > 0 ? cmd.repeat : shade->repeats, cmd.stepSize);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::group_command: {
|
||||
SomfyGroup *group = somfy.getGroupById(cmd.groupId);
|
||||
if(group) sent = group->sendCommand(cmd.command, cmd.repeat >= 0 ? cmd.repeat : group->repeats, cmd.stepSize);
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::tilt_command: {
|
||||
SomfyShade *shade = somfy.getShadeById(cmd.shadeId);
|
||||
if(shade) {
|
||||
if(cmd.target <= 100) sent = shade->moveToTiltTarget(shade->transformPosition(cmd.target));
|
||||
else sent = shade->sendTiltCommand(cmd.command);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::shade_repeat: {
|
||||
SomfyShade *shade = somfy.getShadeById(cmd.shadeId);
|
||||
if(shade) {
|
||||
if(shade->shadeType == shade_types::garage1 && cmd.command == somfy_commands::Prog) cmd.command = somfy_commands::Toggle;
|
||||
if(!shade->isLastCommand(cmd.command)) sent = shade->sendCommand(cmd.command, cmd.repeat >= 0 ? cmd.repeat : shade->repeats, cmd.stepSize);
|
||||
else sent = shade->repeatFrame(cmd.repeat >= 0 ? cmd.repeat : shade->repeats);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::group_repeat: {
|
||||
SomfyGroup *group = somfy.getGroupById(cmd.groupId);
|
||||
if(group) {
|
||||
if(!group->isLastCommand(cmd.command)) sent = group->sendCommand(cmd.command, cmd.repeat >= 0 ? cmd.repeat : group->repeats, cmd.stepSize);
|
||||
else sent = group->repeatFrame(cmd.repeat >= 0 ? cmd.repeat : group->repeats);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::set_positions: {
|
||||
SomfyShade *shade = somfy.getShadeById(cmd.shadeId);
|
||||
if(shade) {
|
||||
if(cmd.position >= 0) shade->target = shade->currentPos = cmd.position;
|
||||
if(cmd.tiltPosition >= 0 && shade->tiltType != tilt_types::none) shade->tiltTarget = shade->currentTiltPos = cmd.tiltPosition;
|
||||
shade->emitState();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::shade_sensor: {
|
||||
SomfyShade *shade = somfy.getShadeById(cmd.shadeId);
|
||||
if(shade) {
|
||||
sent = shade->sendSensorCommand(cmd.windy, cmd.sunny, cmd.repeat >= 0 ? (uint8_t)cmd.repeat : shade->repeats);
|
||||
shade->emitState();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::group_sensor: {
|
||||
SomfyGroup *group = somfy.getGroupById(cmd.groupId);
|
||||
if(group) {
|
||||
sent = group->sendSensorCommand(cmd.windy, cmd.sunny, cmd.repeat >= 0 ? (uint8_t)cmd.repeat : group->repeats);
|
||||
group->emitState();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::set_my_position: {
|
||||
SomfyShade *shade = somfy.getShadeById(cmd.shadeId);
|
||||
if(shade) sent = shade->setMyPosition(cmd.position, cmd.tiltPosition);
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::unpair_shade: {
|
||||
SomfyShade *shade = somfy.getShadeById(cmd.shadeId);
|
||||
if(shade) {
|
||||
sent = shade->sendCommand(somfy_commands::Prog, shade->bitLength == 56 ? 7 : 1);
|
||||
if(sent) {
|
||||
shade->paired = false;
|
||||
shade->save();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::set_rolling_code: {
|
||||
SomfyShade *shade = somfy.getShadeById(cmd.shadeId);
|
||||
if(shade) shade->setRollingCode(cmd.rollingCode);
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::restore_shades: {
|
||||
restore_options_t opts = restoreOptions(cmd.restoreParts);
|
||||
ShadeConfigFile::restore(&somfy, "/shades.tmp", opts);
|
||||
ESP_LOGI(TAG, "Rebooting ESP for restored settings...");
|
||||
rebootDelay.reboot = true;
|
||||
rebootDelay.rebootTime = millis() + 1000;
|
||||
break;
|
||||
}
|
||||
case web_cmd_t::load_shades:
|
||||
somfy.loadShadesFile("/shades.tmp");
|
||||
break;
|
||||
}
|
||||
if(!sent) ESP_LOGW(TAG, "Radio didn't take web command %u", static_cast<uint8_t>(cmd.type));
|
||||
}
|
||||
}
|
||||
bool Web::isAuthenticated(AsyncWebServerRequest *request, bool cfg) {
|
||||
ESP_LOGD(TAG, "Checking async authentication");
|
||||
if(settings.Security.type == security_types::None) return true;
|
||||
|
|
@ -353,7 +226,7 @@ static void serializeShadeRef(SomfyShade *shade, JsonFormatter &json) {
|
|||
}
|
||||
// paired and lastRollingCode are passed in for a response that reports the
|
||||
// change the main loop is about to make. An unpair is held until it goes on
|
||||
// air (holdWebCommand), so the answer comes true.
|
||||
// air (holdLoopItem), so the answer comes true.
|
||||
static void serializeShade(SomfyShade *shade, JsonFormatter &json, bool paired, uint16_t lastRollingCode) {
|
||||
json.addElem("shadeId", shade->getShadeId());
|
||||
json.addElem("roomId", shade->roomId);
|
||||
|
|
@ -898,40 +771,30 @@ void Web::handleLogin(AsyncWebServerRequest *request, JsonVariant &json) {
|
|||
void Web::handleShadeCommand(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
uint8_t shadeId = 255;
|
||||
uint8_t target = 255;
|
||||
uint8_t stepSize = 0;
|
||||
int8_t repeat = -1;
|
||||
somfy_commands command = somfy_commands::My;
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::shade_command;
|
||||
// Try query params
|
||||
if(asyncHasParam(request, "shadeId")) {
|
||||
shadeId = asyncParam(request, "shadeId").toInt();
|
||||
if(asyncHasParam(request, "command")) command = translateSomfyCommand(asyncParam(request, "command"));
|
||||
else if(asyncHasParam(request, "target")) target = asyncParam(request, "target").toInt();
|
||||
if(asyncHasParam(request, "repeat")) repeat = asyncParam(request, "repeat").toInt();
|
||||
if(asyncHasParam(request, "stepSize")) stepSize = asyncParam(request, "stepSize").toInt();
|
||||
req.id = asyncParam(request, "shadeId").toInt();
|
||||
if(asyncHasParam(request, "command")) req.command = translateSomfyCommand(asyncParam(request, "command"));
|
||||
else if(asyncHasParam(request, "target")) req.target = requestTarget(asyncParam(request, "target").toInt());
|
||||
if(asyncHasParam(request, "repeat")) req.repeat = requestRepeat(asyncParam(request, "repeat").toInt());
|
||||
if(asyncHasParam(request, "stepSize")) req.stepSize = asyncParam(request, "stepSize").toInt();
|
||||
}
|
||||
else if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
if(!obj["shadeId"].isNull()) req.id = obj["shadeId"];
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; }
|
||||
if(!obj["command"].isNull()) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); }
|
||||
else if(!obj["target"].isNull()) target = obj["target"].as<uint8_t>();
|
||||
if(!obj["repeat"].isNull()) repeat = obj["repeat"].as<uint8_t>();
|
||||
if(!obj["stepSize"].isNull()) stepSize = obj["stepSize"].as<uint8_t>();
|
||||
if(!obj["command"].isNull()) { String scmd = obj["command"]; req.command = translateSomfyCommand(scmd); }
|
||||
else if(!obj["target"].isNull()) req.target = requestTarget(obj["target"].as<int>());
|
||||
if(!obj["repeat"].isNull()) req.repeat = requestRepeat(obj["repeat"].as<int>());
|
||||
if(!obj["stepSize"].isNull()) req.stepSize = obj["stepSize"].as<uint8_t>();
|
||||
}
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; }
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
SomfyShade *shade = somfy.getShadeById(req.id);
|
||||
if(shade) {
|
||||
ESP_LOGI(TAG, "handleShadeCommand shade=%u target=%u command=%s", shadeId, target, translateSomfyCommand(command).c_str());
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::shade_command;
|
||||
cmd.shadeId = shadeId;
|
||||
cmd.target = target;
|
||||
cmd.command = command;
|
||||
cmd.repeat = repeat;
|
||||
cmd.stepSize = stepSize;
|
||||
if(!queueLoopCommand(request, cmd)) return;
|
||||
ESP_LOGI(TAG, "handleShadeCommand shade=%u target=%d command=%s", req.id, req.target, translateSomfyCommand(req.command).c_str());
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
|
|
@ -944,35 +807,27 @@ void Web::handleShadeCommand(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
void Web::handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
uint8_t groupId = 255;
|
||||
uint8_t stepSize = 0;
|
||||
int8_t repeat = -1;
|
||||
somfy_commands command = somfy_commands::My;
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::group_command;
|
||||
if(asyncHasParam(request, "groupId")) {
|
||||
groupId = asyncParam(request, "groupId").toInt();
|
||||
if(asyncHasParam(request, "command")) command = translateSomfyCommand(asyncParam(request, "command"));
|
||||
if(asyncHasParam(request, "repeat")) repeat = asyncParam(request, "repeat").toInt();
|
||||
if(asyncHasParam(request, "stepSize")) stepSize = asyncParam(request, "stepSize").toInt();
|
||||
req.id = asyncParam(request, "groupId").toInt();
|
||||
if(asyncHasParam(request, "command")) req.command = translateSomfyCommand(asyncParam(request, "command"));
|
||||
if(asyncHasParam(request, "repeat")) req.repeat = requestRepeat(asyncParam(request, "repeat").toInt());
|
||||
if(asyncHasParam(request, "stepSize")) req.stepSize = asyncParam(request, "stepSize").toInt();
|
||||
}
|
||||
else if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["groupId"].isNull()) groupId = obj["groupId"];
|
||||
if(!obj["groupId"].isNull()) req.id = obj["groupId"];
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group id was supplied.\"}")); return; }
|
||||
if(!obj["command"].isNull()) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); }
|
||||
if(!obj["repeat"].isNull()) repeat = obj["repeat"].as<uint8_t>();
|
||||
if(!obj["stepSize"].isNull()) stepSize = obj["stepSize"].as<uint8_t>();
|
||||
if(!obj["command"].isNull()) { String scmd = obj["command"]; req.command = translateSomfyCommand(scmd); }
|
||||
if(!obj["repeat"].isNull()) req.repeat = requestRepeat(obj["repeat"].as<int>());
|
||||
if(!obj["stepSize"].isNull()) req.stepSize = obj["stepSize"].as<uint8_t>();
|
||||
}
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No group object supplied.\"}")); return; }
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
SomfyGroup *group = somfy.getGroupById(req.id);
|
||||
if(group) {
|
||||
ESP_LOGI(TAG, "handleGroupCommand group=%u command=%s", groupId, translateSomfyCommand(command).c_str());
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::group_command;
|
||||
cmd.groupId = groupId;
|
||||
cmd.command = command;
|
||||
cmd.repeat = repeat;
|
||||
cmd.stepSize = stepSize;
|
||||
if(!queueLoopCommand(request, cmd)) return;
|
||||
ESP_LOGI(TAG, "handleGroupCommand group=%u command=%s", req.id, translateSomfyCommand(req.command).c_str());
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
|
|
@ -985,31 +840,25 @@ void Web::handleGroupCommand(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
void Web::handleTiltCommand(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
uint8_t shadeId = 255;
|
||||
uint8_t target = 255;
|
||||
somfy_commands command = somfy_commands::My;
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::tilt_command;
|
||||
if(asyncHasParam(request, "shadeId")) {
|
||||
shadeId = asyncParam(request, "shadeId").toInt();
|
||||
if(asyncHasParam(request, "command")) command = translateSomfyCommand(asyncParam(request, "command"));
|
||||
else if(asyncHasParam(request, "target")) target = asyncParam(request, "target").toInt();
|
||||
req.id = asyncParam(request, "shadeId").toInt();
|
||||
if(asyncHasParam(request, "command")) req.command = translateSomfyCommand(asyncParam(request, "command"));
|
||||
else if(asyncHasParam(request, "target")) req.target = requestTarget(asyncParam(request, "target").toInt());
|
||||
}
|
||||
else if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
if(!obj["shadeId"].isNull()) req.id = obj["shadeId"];
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade id was supplied.\"}")); return; }
|
||||
if(!obj["command"].isNull()) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); }
|
||||
else if(!obj["target"].isNull()) target = obj["target"].as<uint8_t>();
|
||||
if(!obj["command"].isNull()) { String scmd = obj["command"]; req.command = translateSomfyCommand(scmd); }
|
||||
else if(!obj["target"].isNull()) req.target = requestTarget(obj["target"].as<int>());
|
||||
}
|
||||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; }
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
SomfyShade *shade = somfy.getShadeById(req.id);
|
||||
if(shade) {
|
||||
ESP_LOGI(TAG, "handleTiltCommand shade=%u target=%u command=%s", shadeId, target, translateSomfyCommand(command).c_str());
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::tilt_command;
|
||||
cmd.shadeId = shadeId;
|
||||
cmd.target = target;
|
||||
cmd.command = command;
|
||||
if(!queueLoopCommand(request, cmd)) return;
|
||||
ESP_LOGI(TAG, "handleTiltCommand shade=%u target=%d command=%s", req.id, req.target, translateSomfyCommand(req.command).c_str());
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
|
|
@ -1024,33 +873,27 @@ void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
if(!this->isAuthenticated(request)) return;
|
||||
uint8_t shadeId = 255;
|
||||
uint8_t groupId = 255;
|
||||
uint8_t stepSize = 0;
|
||||
int8_t repeat = -1;
|
||||
somfy_commands command = somfy_commands::My;
|
||||
shade_request_t req;
|
||||
if(asyncHasParam(request, "shadeId")) shadeId = asyncParam(request, "shadeId").toInt();
|
||||
else if(asyncHasParam(request, "groupId")) groupId = asyncParam(request, "groupId").toInt();
|
||||
if(asyncHasParam(request, "command")) command = translateSomfyCommand(asyncParam(request, "command"));
|
||||
if(asyncHasParam(request, "repeat")) repeat = asyncParam(request, "repeat").toInt();
|
||||
if(asyncHasParam(request, "stepSize")) stepSize = asyncParam(request, "stepSize").toInt();
|
||||
if(asyncHasParam(request, "command")) req.command = translateSomfyCommand(asyncParam(request, "command"));
|
||||
if(asyncHasParam(request, "repeat")) req.repeat = requestRepeat(asyncParam(request, "repeat").toInt());
|
||||
if(asyncHasParam(request, "stepSize")) req.stepSize = asyncParam(request, "stepSize").toInt();
|
||||
if(shadeId == 255 && groupId == 255 && !json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
if(!obj["groupId"].isNull()) groupId = obj["groupId"];
|
||||
if(!obj["stepSize"].isNull()) stepSize = obj["stepSize"];
|
||||
if(!obj["command"].isNull()) { String scmd = obj["command"]; command = translateSomfyCommand(scmd); }
|
||||
if(!obj["repeat"].isNull()) repeat = obj["repeat"].as<uint8_t>();
|
||||
if(!obj["stepSize"].isNull()) req.stepSize = obj["stepSize"];
|
||||
if(!obj["command"].isNull()) { String scmd = obj["command"]; req.command = translateSomfyCommand(scmd); }
|
||||
if(!obj["repeat"].isNull()) req.repeat = requestRepeat(obj["repeat"].as<int>());
|
||||
}
|
||||
if(shadeId != 255) {
|
||||
ESP_LOGI(TAG, "handleRepeatCommand shade=%u command=%s", shadeId, translateSomfyCommand(command).c_str());
|
||||
ESP_LOGI(TAG, "handleRepeatCommand shade=%u command=%s", shadeId, translateSomfyCommand(req.command).c_str());
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(!shade) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Shade reference could not be found.\"}")); return; }
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::shade_repeat;
|
||||
cmd.shadeId = shadeId;
|
||||
cmd.command = command;
|
||||
cmd.repeat = repeat;
|
||||
cmd.stepSize = stepSize;
|
||||
if(!queueLoopCommand(request, cmd)) return;
|
||||
req.type = shade_request_type_t::shade_repeat;
|
||||
req.id = shadeId;
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
|
|
@ -1059,16 +902,12 @@ void Web::handleRepeatCommand(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
resp.endResponse();
|
||||
}
|
||||
else if(groupId != 255) {
|
||||
ESP_LOGI(TAG, "handleRepeatCommand group=%u command=%s", groupId, translateSomfyCommand(command).c_str());
|
||||
ESP_LOGI(TAG, "handleRepeatCommand group=%u command=%s", groupId, translateSomfyCommand(req.command).c_str());
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(!group) { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"Group reference could not be found.\"}")); return; }
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::group_repeat;
|
||||
cmd.groupId = groupId;
|
||||
cmd.command = command;
|
||||
cmd.repeat = repeat;
|
||||
cmd.stepSize = stepSize;
|
||||
if(!queueLoopCommand(request, cmd)) return;
|
||||
req.type = shade_request_type_t::group_repeat;
|
||||
req.id = groupId;
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
|
|
@ -1144,27 +983,24 @@ void Web::handleGroup(AsyncWebServerRequest *request) {
|
|||
void Web::handleSetPositions(AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
if(request->method() == HTTP_OPTIONS) { request->send(200); return; }
|
||||
if(!this->isAuthenticated(request)) return;
|
||||
uint8_t shadeId = asyncHasParam(request, "shadeId") ? asyncParam(request, "shadeId").toInt() : 255;
|
||||
int8_t pos = asyncHasParam(request, "position") ? asyncParam(request, "position").toInt() : -1;
|
||||
int8_t tiltPos = asyncHasParam(request, "tiltPosition") ? asyncParam(request, "tiltPosition").toInt() : -1;
|
||||
shade_request_t req;
|
||||
req.type = shade_request_type_t::set_positions;
|
||||
if(asyncHasParam(request, "shadeId")) req.id = asyncParam(request, "shadeId").toInt();
|
||||
if(asyncHasParam(request, "position")) req.position = requestTarget(asyncParam(request, "position").toInt());
|
||||
if(asyncHasParam(request, "tiltPosition")) req.tiltPosition = requestTarget(asyncParam(request, "tiltPosition").toInt());
|
||||
if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"];
|
||||
if(!obj["position"].isNull()) pos = obj["position"];
|
||||
if(!obj["tiltPosition"].isNull()) tiltPos = obj["tiltPosition"];
|
||||
if(!obj["shadeId"].isNull()) req.id = obj["shadeId"];
|
||||
if(!obj["position"].isNull()) req.position = requestTarget(obj["position"].as<int>());
|
||||
if(!obj["tiltPosition"].isNull()) req.tiltPosition = requestTarget(obj["tiltPosition"].as<int>());
|
||||
}
|
||||
if(shadeId != 255) {
|
||||
ESP_LOGI(TAG, "handleSetPositions shade=%u pos=%d tiltPos=%d", shadeId, pos, tiltPos);
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(req.id != 255) {
|
||||
ESP_LOGI(TAG, "handleSetPositions shade=%u pos=%d tiltPos=%d", req.id, req.position, req.tiltPosition);
|
||||
SomfyShade *shade = somfy.getShadeById(req.id);
|
||||
if(shade) {
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::set_positions;
|
||||
cmd.shadeId = shadeId;
|
||||
cmd.position = pos;
|
||||
cmd.tiltPosition = tiltPos;
|
||||
if(!this->queueCommand(cmd)) { sendQueueFull(request); return; }
|
||||
// The main loop applies the command after we answer so this is the state
|
||||
// before it. Clients get the result from the shadeState socket event.
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
// loop() applies the request after we answer so this is the state before
|
||||
// it. Clients get the result from the shadeState socket event.
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
|
|
@ -1181,33 +1017,30 @@ void Web::handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json) {
|
|||
if(!this->isAuthenticated(request)) return;
|
||||
uint8_t shadeId = asyncHasParam(request, "shadeId") ? asyncParam(request, "shadeId").toInt() : 255;
|
||||
uint8_t groupId = asyncHasParam(request, "groupId") ? asyncParam(request, "groupId").toInt() : 255;
|
||||
int8_t sunny = asyncHasParam(request, "sunny") ? (toBoolean(asyncParam(request, "sunny").c_str(), false) ? 1 : 0) : -1;
|
||||
int8_t windy = asyncHasParam(request, "windy") ? asyncParam(request, "windy").toInt() : -1;
|
||||
int8_t repeat = asyncHasParam(request, "repeat") ? asyncParam(request, "repeat").toInt() : -1;
|
||||
shade_request_t req;
|
||||
if(asyncHasParam(request, "sunny")) req.sunny = toBoolean(asyncParam(request, "sunny").c_str(), false) ? 1 : 0;
|
||||
if(asyncHasParam(request, "windy")) req.windy = asyncParam(request, "windy").toInt();
|
||||
if(asyncHasParam(request, "repeat")) req.repeat = requestRepeat(asyncParam(request, "repeat").toInt());
|
||||
if(!json.isNull()) {
|
||||
JsonObject obj = json.as<JsonObject>();
|
||||
if(!obj["shadeId"].isNull()) shadeId = obj["shadeId"].as<uint8_t>();
|
||||
if(!obj["groupId"].isNull()) groupId = obj["groupId"].as<uint8_t>();
|
||||
if(!obj["sunny"].isNull()) {
|
||||
if(obj["sunny"].is<bool>()) sunny = obj["sunny"].as<bool>() ? 1 : 0;
|
||||
else sunny = obj["sunny"].as<int8_t>();
|
||||
if(obj["sunny"].is<bool>()) req.sunny = obj["sunny"].as<bool>() ? 1 : 0;
|
||||
else req.sunny = obj["sunny"].as<int8_t>();
|
||||
}
|
||||
if(!obj["windy"].isNull()) {
|
||||
if(obj["windy"].is<bool>()) windy = obj["windy"].as<bool>() ? 1 : 0;
|
||||
else windy = obj["windy"].as<int8_t>();
|
||||
if(obj["windy"].is<bool>()) req.windy = obj["windy"].as<bool>() ? 1 : 0;
|
||||
else req.windy = obj["windy"].as<int8_t>();
|
||||
}
|
||||
if(!obj["repeat"].isNull()) repeat = obj["repeat"].as<uint8_t>();
|
||||
if(!obj["repeat"].isNull()) req.repeat = requestRepeat(obj["repeat"].as<int>());
|
||||
}
|
||||
if(shadeId != 255) {
|
||||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
if(shade) {
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::shade_sensor;
|
||||
cmd.shadeId = shadeId;
|
||||
cmd.sunny = sunny;
|
||||
cmd.windy = windy;
|
||||
cmd.repeat = repeat;
|
||||
if(!queueLoopCommand(request, cmd)) return;
|
||||
req.type = shade_request_type_t::shade_sensor;
|
||||
req.id = shadeId;
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
// The main loop applies the command after we answer so this is the state
|
||||
// before it. Clients get the result from the shadeState socket event.
|
||||
AsyncJsonResp resp;
|
||||
|
|
@ -1222,13 +1055,9 @@ void Web::handleSetSensor(AsyncWebServerRequest *request, JsonVariant &json) {
|
|||
else if(groupId != 255) {
|
||||
SomfyGroup *group = somfy.getGroupById(groupId);
|
||||
if(group) {
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::group_sensor;
|
||||
cmd.groupId = groupId;
|
||||
cmd.sunny = sunny;
|
||||
cmd.windy = windy;
|
||||
cmd.repeat = repeat;
|
||||
if(!queueLoopCommand(request, cmd)) return;
|
||||
req.type = shade_request_type_t::group_sensor;
|
||||
req.id = groupId;
|
||||
if(!queueShadeRequest(request, req)) return;
|
||||
// As above, this is the state before the command and groupState follows.
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
|
|
@ -1511,10 +1340,10 @@ void Web::begin() {
|
|||
}
|
||||
// The main loop restores, since it owns the shades and their rolling
|
||||
// codes, and then reboots.
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::restore_shades;
|
||||
cmd.restoreParts = restoreParts(opts);
|
||||
if(!queueLoopCommand(request, cmd)) return;
|
||||
loop_item_t item;
|
||||
item.type = loop_item_type_t::restore_shades;
|
||||
item.restoreParts = restoreParts(opts);
|
||||
if(!queueLoopItem(request, item)) return;
|
||||
request->send(200, _encoding_json, "{\"status\":\"Success\",\"desc\":\"Restoring Shade settings\"}");
|
||||
}
|
||||
},
|
||||
|
|
@ -1776,9 +1605,8 @@ void Web::begin() {
|
|||
}
|
||||
SomfyShade* shade = somfy.getShadeById(shadeId);
|
||||
if(shade) {
|
||||
tilt = requestedFavoriteTilt(tilt, shade->tiltType, shade->myTiltPos, shade->flipPosition);
|
||||
if(pos >= 0 && pos <= 100)
|
||||
if(!queueSetMyPosition(request, shade, shade->transformPosition(pos), shade->transformPosition(tilt))) return;
|
||||
if(!queueSetMyPosition(request, shade, pos, requestTarget(tilt))) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
|
|
@ -1802,9 +1630,8 @@ void Web::begin() {
|
|||
else { request->send(500, _encoding_json, F("{\"status\":\"ERROR\",\"desc\":\"No shade object supplied.\"}")); return; }
|
||||
SomfyShade* shade = somfy.getShadeById(shadeId);
|
||||
if(shade) {
|
||||
tilt = requestedFavoriteTilt(tilt, shade->tiltType, shade->myTiltPos, shade->flipPosition);
|
||||
if(pos >= 0 && pos <= 100)
|
||||
if(!queueSetMyPosition(request, shade, shade->transformPosition(pos), shade->transformPosition(tilt))) return;
|
||||
if(!queueSetMyPosition(request, shade, pos, requestTarget(tilt))) return;
|
||||
AsyncJsonResp resp;
|
||||
resp.beginResponse(request, g_async_content, sizeof(g_async_content));
|
||||
resp.beginObject();
|
||||
|
|
@ -2326,9 +2153,9 @@ void Web::begin() {
|
|||
}
|
||||
if(final) {
|
||||
// The main loop loads it, since it owns the shades.
|
||||
web_command_t cmd = {};
|
||||
cmd.type = web_cmd_t::load_shades;
|
||||
webServer.queueCommand(cmd);
|
||||
loop_item_t item;
|
||||
item.type = loop_item_type_t::load_shades;
|
||||
loopQueue.submit(item);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncJson.h>
|
||||
#include <freertos/queue.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include "Somfy.h"
|
||||
#include "WebCommand.h"
|
||||
#ifndef webserver_h
|
||||
#define webserver_h
|
||||
|
||||
|
|
@ -40,8 +37,5 @@ class Web {
|
|||
void handleBackup(AsyncWebServerRequest *request);
|
||||
void handleReboot(AsyncWebServerRequest *request);
|
||||
void handleNotFound(AsyncWebServerRequest *request);
|
||||
bool queueCommand(const web_command_t &cmd);
|
||||
private:
|
||||
void processQueue();
|
||||
};
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue