mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
Merge pull request #40 from cjkas/refactor/shade-requests
refactor: carry out shade requests from one module behind a LoopQueue
This commit is contained in:
commit
ddc5e3b941
20 changed files with 1354 additions and 569 deletions
|
|
@ -36,7 +36,7 @@ The new `ControllerChunker` reads state as the response drains. On a slow link o
|
|||
- `/linkToGroup` and `/unlinkFromGroup` change a group's linked shades.
|
||||
- `/setPaired` sets `paired`.
|
||||
|
||||
Shade commands, set-My, unpair, `/setRollingCode`, a shades restore and `/updateShadeConfig` already go through the web command queue to `loop()`.
|
||||
Shade requests (shade and group commands, set-My, unpair, `/setPositions`, `/setRollingCode`, and every MQTT command), a shades restore and `/updateShadeConfig` already go through the LoopQueue to `loop()`.
|
||||
- `/controller` leaves out a room, shade, group or linked shade that doesn't fit in its chunk buffer, see [Oversized `/controller` items are left out](#oversized-controller-items-are-left-out).
|
||||
|
||||
## Oversized `/controller` items are left out
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Plan: domain modules behind the LoopQueue
|
||||
|
||||
**Status:** planned, nothing built yet.
|
||||
**Status:** steps 0 and 1 built (PRs #39 and #40); steps 2–5 planned.
|
||||
**Written:** 2026-10-15, against commit `e624a41` on `refactor/entry-points-outputs`. Terms are defined in [GLOSSARY.md](../GLOSSARY.md); the threading rule is [ADR-0002](adr/0002-loop-task-is-the-only-writer-of-the-shade-model.md).
|
||||
|
||||
## Why
|
||||
|
|
@ -118,6 +118,18 @@ Each step is a PR of its own that can be tested on the device. Each one runs `pi
|
|||
| 4 | Change fan-out with dirty bits, replacing publishing from the `p_*` setters. | writes that skip MQTT, `position` stuck at 50 |
|
||||
| 5 | Split `Web.cpp` into WebUiRoutes and ApiRoutes, and `MQTT.cpp`/`SomfyMqtt.cpp` into MqttCommands, an MQTT connection and MqttPublisher. Edit parsing stays in `SomfyJson.cpp`. | the glossary names land in the code |
|
||||
|
||||
### Step 1: what changed in behaviour
|
||||
- **Flip handling is the same for every entry point.** MQTT `tiltTarget`, `mypos`, `myTiltPos` and `tiltPosition`, and `/setPositions` (which Home Assistant calls), now turn the position round for a shade with `flipPosition`, as the other routes always did.
|
||||
- **Setting the favorite over MQTT keeps the other half.** `mypos` on a shade with tilt keeps the favorite tilt; before, it set it to 0. `myTiltPos` keeps the favorite lift, or uses where the shade is when it has none.
|
||||
- **`repeat` means the same everywhere.** Left out or negative is the shade's or group's own repeats; 0 is no repeats. `/shadeCommand` used to read 0 as "the shade's own". A JSON `repeat` over 127 is capped; it used to wrap round to "the shade's own".
|
||||
- **Position corrections reach MQTT.** `/setPositions` and MQTT `position`/`tiltPosition` go through the setters, so they are published.
|
||||
- **MQTT commands go through the LoopQueue.** While the radio is full they wait instead of being refused, and they run in order with web requests. An MQTT command topic that isn't understood is logged.
|
||||
- **The LoopQueue is drained on every `loop()` pass**, with or without a network. It is no longer drained from inside a GitHub firmware download, so a restore can't reboot the device in the middle of one.
|
||||
|
||||
`WebCommand.h` became `LoopQueue.h` (the item and the hold rule) and `ShadeRequests.h` (the request, the `ShadeOps` seam and the module). `SomfyShadeOps` is the device side of the seam. `entryPoints/MqttCommands` holds the topic parsing. There are 45 new native tests (271 in all).
|
||||
|
||||
Flash: `esp32devdbg` 1,546,405 → 1,547,381 bytes, `esp32dev` 1,396,957 → 1,398,337 bytes.
|
||||
|
||||
### Step 0 on a device
|
||||
NVS stays at `0x9000`, so the rolling codes survive. The file system partition shrinks from `0xE0000` to `0xD0000`, and the existing LittleFS won't mount, because `LittleFS.begin()` doesn't format. So:
|
||||
1. Take a backup through `/backup`.
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ lib_deps =
|
|||
platform = native
|
||||
test_framework = unity
|
||||
test_build_src = yes
|
||||
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp>
|
||||
build_src_filter = -<*> +<Utils.cpp> +<SomfyFrame.cpp> +<JsonFormatter.cpp> +<ShadeMotion.cpp> +<ShadeSensors.cpp> +<PulseDecoder.cpp> +<TxBuffer.cpp> +<RadioScheduler.cpp> +<ShadeRequests.cpp> +<entryPoints/MqttCommands.cpp>
|
||||
build_flags =
|
||||
-std=gnu++17
|
||||
-Isrc
|
||||
|
|
|
|||
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
|
||||
|
|
|
|||
78
test/test_loopqueue/test_loopqueue.cpp
Normal file
78
test/test_loopqueue/test_loopqueue.cpp
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#include <unity.h>
|
||||
#include "LoopQueue.h"
|
||||
|
||||
static loop_item_t request(shade_request_type_t type) {
|
||||
loop_item_t item;
|
||||
item.request.type = type;
|
||||
return item;
|
||||
}
|
||||
static loop_item_t item(loop_item_type_t type) {
|
||||
loop_item_t item;
|
||||
item.type = type;
|
||||
return item;
|
||||
}
|
||||
|
||||
static const shade_request_type_t RADIO_REQUESTS[] = {
|
||||
shade_request_type_t::shade_command, shade_request_type_t::group_command, shade_request_type_t::tilt_command,
|
||||
shade_request_type_t::shade_repeat, shade_request_type_t::group_repeat, shade_request_type_t::shade_sensor,
|
||||
shade_request_type_t::group_sensor, shade_request_type_t::set_favorite, shade_request_type_t::unpair
|
||||
};
|
||||
static const shade_request_type_t OFF_RADIO_REQUESTS[] = {
|
||||
shade_request_type_t::set_positions, shade_request_type_t::set_rolling_code
|
||||
};
|
||||
static const loop_item_type_t OTHER_ITEMS[] = { loop_item_type_t::restore_shades, loop_item_type_t::load_shades };
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static void test_only_position_and_rolling_code_requests_stay_off_the_radio() {
|
||||
int offRadio = 0;
|
||||
for(uint8_t t = 0; t <= static_cast<uint8_t>(shade_request_type_t::set_rolling_code); t++) {
|
||||
if(!shadeRequestUsesRadio(static_cast<shade_request_type_t>(t))) offRadio++;
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(2, offRadio);
|
||||
for(shade_request_type_t t : OFF_RADIO_REQUESTS) TEST_ASSERT_FALSE(loopItemUsesRadio(request(t)));
|
||||
for(loop_item_type_t t : OTHER_ITEMS) TEST_ASSERT_FALSE(loopItemUsesRadio(item(t)));
|
||||
}
|
||||
static void test_off_radio_items_are_never_held() {
|
||||
// Nothing goes on air, so a full or suspended radio doesn't matter.
|
||||
for(shade_request_type_t t : OFF_RADIO_REQUESTS) {
|
||||
TEST_ASSERT_FALSE(holdLoopItem(request(t), false, false));
|
||||
TEST_ASSERT_FALSE(holdLoopItem(request(t), true, false));
|
||||
}
|
||||
for(loop_item_type_t t : OTHER_ITEMS) {
|
||||
TEST_ASSERT_FALSE(holdLoopItem(item(t), false, false));
|
||||
TEST_ASSERT_FALSE(holdLoopItem(item(t), true, false));
|
||||
}
|
||||
}
|
||||
static void test_radio_request_waits_for_room() {
|
||||
for(shade_request_type_t t : RADIO_REQUESTS) {
|
||||
TEST_ASSERT_TRUE(holdLoopItem(request(t), false, false));
|
||||
TEST_ASSERT_FALSE(holdLoopItem(request(t), false, true));
|
||||
}
|
||||
}
|
||||
static void test_requests_are_not_held_while_suspended() {
|
||||
// Nothing goes on air during a frequency scan or OTA anyway.
|
||||
for(shade_request_type_t t : RADIO_REQUESTS) {
|
||||
if(t == shade_request_type_t::unpair) continue;
|
||||
TEST_ASSERT_FALSE(holdLoopItem(request(t), true, false));
|
||||
TEST_ASSERT_FALSE(holdLoopItem(request(t), true, true));
|
||||
}
|
||||
}
|
||||
static void test_unpair_waits_out_a_suspend() {
|
||||
// The client was already told the shade is unpaired.
|
||||
TEST_ASSERT_TRUE(holdLoopItem(request(shade_request_type_t::unpair), true, false));
|
||||
TEST_ASSERT_TRUE(holdLoopItem(request(shade_request_type_t::unpair), true, true));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_only_position_and_rolling_code_requests_stay_off_the_radio);
|
||||
RUN_TEST(test_off_radio_items_are_never_held);
|
||||
RUN_TEST(test_radio_request_waits_for_room);
|
||||
RUN_TEST(test_requests_are_not_held_while_suspended);
|
||||
RUN_TEST(test_unpair_waits_out_a_suspend);
|
||||
return UNITY_END();
|
||||
}
|
||||
165
test/test_mqttcommands/test_mqttcommands.cpp
Normal file
165
test/test_mqttcommands/test_mqttcommands.cpp
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
#include <unity.h>
|
||||
#include <string.h>
|
||||
#include "entryPoints/MqttCommands.h"
|
||||
|
||||
static shade_request_t req;
|
||||
|
||||
static bool parse(const char *topic, const char *value) {
|
||||
req = shade_request_t();
|
||||
return parseMqttCommand(topic, reinterpret_cast<const uint8_t *>(value), strlen(value), req);
|
||||
}
|
||||
static void assertType(shade_request_type_t expected) {
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(expected), static_cast<uint8_t>(req.type));
|
||||
}
|
||||
static void assertCommand(somfy_commands expected) {
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(expected), static_cast<uint8_t>(req.command));
|
||||
}
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
/*********************************************************************
|
||||
* Topics
|
||||
********************************************************************/
|
||||
static void test_the_last_four_segments_name_the_command() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/target/set", "30"));
|
||||
assertType(shade_request_type_t::shade_command);
|
||||
TEST_ASSERT_EQUAL_UINT8(4, req.id);
|
||||
TEST_ASSERT_EQUAL_INT8(30, req.target);
|
||||
}
|
||||
static void test_the_root_topic_may_be_empty_or_have_slashes() {
|
||||
TEST_ASSERT_TRUE(parse("shades/12/target/set", "30"));
|
||||
TEST_ASSERT_EQUAL_UINT8(12, req.id);
|
||||
TEST_ASSERT_TRUE(parse("home/somfy/rts/shades/255/target/set", "30"));
|
||||
TEST_ASSERT_EQUAL_UINT8(255, req.id);
|
||||
}
|
||||
static void test_topics_it_does_not_take() {
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/target", "30")); // not a set topic
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/target/get", "30"));
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/rooms/4/target/set", "30")); // no such entity
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/name/set", "30")); // no such command
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/x4/target/set", "30")); // not an id
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/256/target/set", "30"));
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades//target/set", "30"));
|
||||
TEST_ASSERT_FALSE(parse("target/set", "30"));
|
||||
TEST_ASSERT_FALSE(parse("", "30"));
|
||||
}
|
||||
static void test_the_payload_need_not_be_terminated() {
|
||||
const char payload[] = { '4', '2', 'x', 'y' };
|
||||
req = shade_request_t();
|
||||
TEST_ASSERT_TRUE(parseMqttCommand("espsomfy/shades/4/target/set", reinterpret_cast<const uint8_t *>(payload), 2, req));
|
||||
TEST_ASSERT_EQUAL_INT8(42, req.target);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Shades
|
||||
********************************************************************/
|
||||
static void test_targets_must_be_in_range() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/target/set", "100"));
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/target/set", "101"));
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/target/set", "-1"));
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/tiltTarget/set", "101"));
|
||||
}
|
||||
static void test_tilt_target() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/tiltTarget/set", "30"));
|
||||
assertType(shade_request_type_t::tilt_command);
|
||||
TEST_ASSERT_EQUAL_INT8(30, req.target);
|
||||
}
|
||||
static void test_direction_sends_up_down_or_my() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/direction/set", "-1"));
|
||||
assertType(shade_request_type_t::shade_command);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.target);
|
||||
assertCommand(somfy_commands::Up);
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/direction/set", "1"));
|
||||
assertCommand(somfy_commands::Down);
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/direction/set", "0"));
|
||||
assertCommand(somfy_commands::My);
|
||||
}
|
||||
static void test_favorite_lift_and_tilt() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/mypos/set", "30"));
|
||||
assertType(shade_request_type_t::set_favorite);
|
||||
TEST_ASSERT_EQUAL_INT8(30, req.position);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.tiltPosition);
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/myTiltPos/set", "60"));
|
||||
assertType(shade_request_type_t::set_favorite);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.position);
|
||||
TEST_ASSERT_EQUAL_INT8(60, req.tiltPosition);
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/mypos/set", "120"));
|
||||
}
|
||||
static void test_sun_flag() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/sunFlag/set", "1"));
|
||||
assertType(shade_request_type_t::shade_command);
|
||||
assertCommand(somfy_commands::SunFlag);
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/sunFlag/set", "0"));
|
||||
assertCommand(somfy_commands::Flag);
|
||||
}
|
||||
static void test_sunny_and_windy_leave_the_other_alone() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/sunny/set", "1"));
|
||||
assertType(shade_request_type_t::shade_sensor);
|
||||
TEST_ASSERT_EQUAL_INT8(1, req.sunny);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.windy);
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/windy/set", "0"));
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.sunny);
|
||||
TEST_ASSERT_EQUAL_INT8(0, req.windy);
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/shades/4/windy/set", "-1"));
|
||||
}
|
||||
static void test_position_and_tilt_position_correct_the_shade() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/position/set", "30"));
|
||||
assertType(shade_request_type_t::set_positions);
|
||||
TEST_ASSERT_EQUAL_INT8(30, req.position);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.tiltPosition);
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/tiltPosition/set", "60"));
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.position);
|
||||
TEST_ASSERT_EQUAL_INT8(60, req.tiltPosition);
|
||||
}
|
||||
static void test_shade_requests_use_the_default_repeats() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/shades/4/direction/set", "1"));
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.repeat);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Groups
|
||||
********************************************************************/
|
||||
static void test_group_direction_and_sun_flag() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/groups/2/direction/set", "-1"));
|
||||
assertType(shade_request_type_t::group_command);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, req.id);
|
||||
assertCommand(somfy_commands::Up);
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/groups/2/sunFlag/set", "1"));
|
||||
assertType(shade_request_type_t::group_command);
|
||||
assertCommand(somfy_commands::SunFlag);
|
||||
}
|
||||
static void test_group_sunny_and_windy() {
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/groups/2/sunny/set", "0"));
|
||||
assertType(shade_request_type_t::group_sensor);
|
||||
TEST_ASSERT_EQUAL_INT8(0, req.sunny);
|
||||
TEST_ASSERT_EQUAL_INT8(-1, req.windy);
|
||||
TEST_ASSERT_TRUE(parse("espsomfy/groups/2/windy/set", "1"));
|
||||
TEST_ASSERT_EQUAL_INT8(1, req.windy);
|
||||
}
|
||||
static void test_groups_only_take_their_commands() {
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/groups/2/target/set", "30"));
|
||||
TEST_ASSERT_FALSE(parse("espsomfy/groups/2/mypos/set", "30"));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_the_last_four_segments_name_the_command);
|
||||
RUN_TEST(test_the_root_topic_may_be_empty_or_have_slashes);
|
||||
RUN_TEST(test_topics_it_does_not_take);
|
||||
RUN_TEST(test_the_payload_need_not_be_terminated);
|
||||
RUN_TEST(test_targets_must_be_in_range);
|
||||
RUN_TEST(test_tilt_target);
|
||||
RUN_TEST(test_direction_sends_up_down_or_my);
|
||||
RUN_TEST(test_favorite_lift_and_tilt);
|
||||
RUN_TEST(test_sun_flag);
|
||||
RUN_TEST(test_sunny_and_windy_leave_the_other_alone);
|
||||
RUN_TEST(test_position_and_tilt_position_correct_the_shade);
|
||||
RUN_TEST(test_shade_requests_use_the_default_repeats);
|
||||
RUN_TEST(test_group_direction_and_sun_flag);
|
||||
RUN_TEST(test_group_sunny_and_windy);
|
||||
RUN_TEST(test_groups_only_take_their_commands);
|
||||
return UNITY_END();
|
||||
}
|
||||
398
test/test_shaderequests/test_shaderequests.cpp
Normal file
398
test/test_shaderequests/test_shaderequests.cpp
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
#include <unity.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include "ShadeRequests.h"
|
||||
|
||||
// Records every call as a line so a test reads as "this request, these calls".
|
||||
class FakeShadeOps : public ShadeOps {
|
||||
public:
|
||||
std::map<uint8_t, shade_info_t> shades;
|
||||
std::map<uint8_t, uint8_t> groups; // group id -> its repeats
|
||||
bool radioTakes = true; // false: the radio refuses everything
|
||||
bool lastCommand = false; // what isLastCommand answers
|
||||
std::string calls;
|
||||
|
||||
bool findShade(uint8_t id, shade_info_t &info) override {
|
||||
auto it = shades.find(id);
|
||||
if(it == shades.end()) return false;
|
||||
info = it->second;
|
||||
return true;
|
||||
}
|
||||
bool findGroup(uint8_t id, uint8_t &repeats) override {
|
||||
auto it = groups.find(id);
|
||||
if(it == groups.end()) return false;
|
||||
repeats = it->second;
|
||||
return true;
|
||||
}
|
||||
bool moveToTarget(uint8_t id, float pos) override { return log("moveToTarget %u %g", id, pos); }
|
||||
bool moveToTiltTarget(uint8_t id, float tilt) override { return log("moveToTiltTarget %u %g", id, tilt); }
|
||||
bool sendCommand(uint8_t id, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) override {
|
||||
return log("sendCommand %u %s repeat=%u step=%u", id, name(cmd), repeat, stepSize);
|
||||
}
|
||||
bool sendTiltCommand(uint8_t id, somfy_commands cmd) override { return log("sendTiltCommand %u %s", id, name(cmd)); }
|
||||
bool isLastCommand(uint8_t, somfy_commands) override { return lastCommand; }
|
||||
bool repeatFrame(uint8_t id, uint8_t repeat) override { return log("repeatFrame %u repeat=%u", id, repeat); }
|
||||
bool sendSensorCommand(uint8_t id, int8_t windy, int8_t sunny, uint8_t repeat) override {
|
||||
return log("sendSensorCommand %u windy=%d sunny=%d repeat=%u", id, windy, sunny, repeat);
|
||||
}
|
||||
bool setMyPosition(uint8_t id, int8_t pos, int8_t tilt) override { return log("setMyPosition %u %d %d", id, pos, tilt); }
|
||||
void setPositions(uint8_t id, float pos, float tilt) override { log("setPositions %u %g %g", id, pos, tilt); }
|
||||
void setUnpaired(uint8_t id) override { log("setUnpaired %u", id); }
|
||||
void setRollingCode(uint8_t id, uint16_t code) override { log("setRollingCode %u %u", id, code); }
|
||||
bool sendGroupCommand(uint8_t id, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) override {
|
||||
return log("sendGroupCommand %u %s repeat=%u step=%u", id, name(cmd), repeat, stepSize);
|
||||
}
|
||||
bool isLastGroupCommand(uint8_t, somfy_commands) override { return lastCommand; }
|
||||
bool repeatGroupFrame(uint8_t id, uint8_t repeat) override { return log("repeatGroupFrame %u repeat=%u", id, repeat); }
|
||||
bool sendGroupSensorCommand(uint8_t id, int8_t windy, int8_t sunny, uint8_t repeat) override {
|
||||
return log("sendGroupSensorCommand %u windy=%d sunny=%d repeat=%u", id, windy, sunny, repeat);
|
||||
}
|
||||
private:
|
||||
static const char *name(somfy_commands cmd) {
|
||||
static std::string s;
|
||||
s = translateSomfyCommand(cmd).c_str();
|
||||
return s.c_str();
|
||||
}
|
||||
template<typename... Args>
|
||||
bool log(const char *fmt, Args... args) {
|
||||
char line[128];
|
||||
snprintf(line, sizeof(line), fmt, args...);
|
||||
if(!calls.empty()) calls += "; ";
|
||||
calls += line;
|
||||
return radioTakes;
|
||||
}
|
||||
};
|
||||
|
||||
static FakeShadeOps ops;
|
||||
static ShadeRequests requests(ops);
|
||||
|
||||
static shade_info_t rollerShade() {
|
||||
shade_info_t s;
|
||||
s.repeats = 2;
|
||||
return s;
|
||||
}
|
||||
static shade_info_t flippedShade() {
|
||||
shade_info_t s = rollerShade();
|
||||
s.flipPosition = true;
|
||||
return s;
|
||||
}
|
||||
static shade_info_t tiltShade(tilt_types tilt) {
|
||||
shade_info_t s = rollerShade();
|
||||
s.tiltType = tilt;
|
||||
return s;
|
||||
}
|
||||
static shade_request_t request(shade_request_type_t type, uint8_t id = 4) {
|
||||
shade_request_t req;
|
||||
req.type = type;
|
||||
req.id = id;
|
||||
return req;
|
||||
}
|
||||
|
||||
void setUp() {
|
||||
ops.shades.clear();
|
||||
ops.groups.clear();
|
||||
ops.radioTakes = true;
|
||||
ops.lastCommand = false;
|
||||
ops.calls.clear();
|
||||
}
|
||||
void tearDown() {}
|
||||
|
||||
/*********************************************************************
|
||||
* Finding the shade or group
|
||||
********************************************************************/
|
||||
static void test_a_missing_shade_is_dropped() {
|
||||
shade_request_t req = request(shade_request_type_t::shade_command);
|
||||
req.target = 30;
|
||||
TEST_ASSERT_TRUE(requests.run(req));
|
||||
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
|
||||
}
|
||||
static void test_a_missing_group_is_dropped() {
|
||||
TEST_ASSERT_TRUE(requests.run(request(shade_request_type_t::group_command, 2)));
|
||||
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
|
||||
}
|
||||
static void test_a_shade_id_is_not_a_group_id() {
|
||||
ops.shades[2] = rollerShade();
|
||||
requests.run(request(shade_request_type_t::group_command, 2));
|
||||
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Positions as the client sees them
|
||||
********************************************************************/
|
||||
static void test_target_moves_a_shade_there() {
|
||||
ops.shades[4] = rollerShade();
|
||||
shade_request_t req = request(shade_request_type_t::shade_command);
|
||||
req.target = 30;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("moveToTarget 4 30", ops.calls.c_str());
|
||||
}
|
||||
static void test_target_on_a_flipped_shade_is_turned_round() {
|
||||
ops.shades[4] = flippedShade();
|
||||
shade_request_t req = request(shade_request_type_t::shade_command);
|
||||
req.target = 30;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("moveToTarget 4 70", ops.calls.c_str());
|
||||
}
|
||||
static void test_tilt_target_on_a_flipped_shade_is_turned_round() {
|
||||
// MQTT used to send this one straight through.
|
||||
ops.shades[4] = flippedShade();
|
||||
shade_request_t req = request(shade_request_type_t::tilt_command);
|
||||
req.target = 30;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("moveToTiltTarget 4 70", ops.calls.c_str());
|
||||
}
|
||||
static void test_without_a_target_the_command_is_sent() {
|
||||
ops.shades[4] = rollerShade();
|
||||
shade_request_t req = request(shade_request_type_t::shade_command);
|
||||
req.command = somfy_commands::Down;
|
||||
req.stepSize = 3;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Down repeat=2 step=3", ops.calls.c_str());
|
||||
}
|
||||
static void test_tilt_without_a_target_sends_the_tilt_command() {
|
||||
ops.shades[4] = rollerShade();
|
||||
shade_request_t req = request(shade_request_type_t::tilt_command);
|
||||
req.command = somfy_commands::Up;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("sendTiltCommand 4 Up", ops.calls.c_str());
|
||||
}
|
||||
static void test_set_positions_on_a_flipped_shade_are_turned_round() {
|
||||
// /setPositions used to send these straight through.
|
||||
ops.shades[4] = flippedShade();
|
||||
ops.shades[4].tiltType = tilt_types::integrated;
|
||||
shade_request_t req = request(shade_request_type_t::set_positions);
|
||||
req.position = 20;
|
||||
req.tiltPosition = 90;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("setPositions 4 80 10", ops.calls.c_str());
|
||||
}
|
||||
static void test_set_positions_leaves_the_tilt_of_a_shade_without_one() {
|
||||
ops.shades[4] = rollerShade();
|
||||
shade_request_t req = request(shade_request_type_t::set_positions);
|
||||
req.position = 20;
|
||||
req.tiltPosition = 90;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("setPositions 4 20 -1", ops.calls.c_str());
|
||||
}
|
||||
static void test_set_positions_with_nothing_in_range_does_nothing() {
|
||||
ops.shades[4] = tiltShade(tilt_types::integrated);
|
||||
shade_request_t req = request(shade_request_type_t::set_positions);
|
||||
req.position = -1;
|
||||
req.tiltPosition = 101;
|
||||
TEST_ASSERT_TRUE(requests.run(req));
|
||||
TEST_ASSERT_EQUAL_STRING("", ops.calls.c_str());
|
||||
}
|
||||
static void test_client_values_are_checked_on_the_way_in() {
|
||||
TEST_ASSERT_EQUAL_INT8(0, requestTarget(0));
|
||||
TEST_ASSERT_EQUAL_INT8(100, requestTarget(100));
|
||||
TEST_ASSERT_EQUAL_INT8(-1, requestTarget(101));
|
||||
TEST_ASSERT_EQUAL_INT8(-1, requestTarget(255));
|
||||
TEST_ASSERT_EQUAL_INT8(-1, requestRepeat(-5));
|
||||
TEST_ASSERT_EQUAL_INT8(0, requestRepeat(0));
|
||||
TEST_ASSERT_EQUAL_INT8(127, requestRepeat(200));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Repeats
|
||||
********************************************************************/
|
||||
static void test_no_repeat_means_the_shades_own() {
|
||||
ops.shades[4] = rollerShade();
|
||||
shade_request_t req = request(shade_request_type_t::shade_command);
|
||||
req.command = somfy_commands::Up;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Up repeat=2 step=0", ops.calls.c_str());
|
||||
}
|
||||
static void test_repeat_zero_means_no_repeats_for_a_shade_too() {
|
||||
// It used to mean "the shade's own" for /shadeCommand but not for groups.
|
||||
ops.shades[4] = rollerShade();
|
||||
ops.groups[2] = 3;
|
||||
shade_request_t req = request(shade_request_type_t::shade_command);
|
||||
req.command = somfy_commands::Up;
|
||||
req.repeat = 0;
|
||||
requests.run(req);
|
||||
shade_request_t group = request(shade_request_type_t::group_command, 2);
|
||||
group.command = somfy_commands::Up;
|
||||
group.repeat = 0;
|
||||
requests.run(group);
|
||||
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Up repeat=0 step=0; sendGroupCommand 2 Up repeat=0 step=0", ops.calls.c_str());
|
||||
}
|
||||
static void test_no_repeat_means_the_groups_own() {
|
||||
ops.groups[2] = 3;
|
||||
shade_request_t req = request(shade_request_type_t::group_command, 2);
|
||||
req.command = somfy_commands::Down;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("sendGroupCommand 2 Down repeat=3 step=0", ops.calls.c_str());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Repeating the last command
|
||||
********************************************************************/
|
||||
static void test_repeat_sends_a_new_command_when_it_was_not_the_last() {
|
||||
ops.shades[4] = rollerShade();
|
||||
shade_request_t req = request(shade_request_type_t::shade_repeat);
|
||||
req.command = somfy_commands::Prog;
|
||||
req.repeat = 10;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Prog repeat=10 step=0", ops.calls.c_str());
|
||||
}
|
||||
static void test_repeat_repeats_the_last_frame() {
|
||||
ops.shades[4] = rollerShade();
|
||||
ops.lastCommand = true;
|
||||
shade_request_t req = request(shade_request_type_t::shade_repeat);
|
||||
req.command = somfy_commands::Prog;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("repeatFrame 4 repeat=2", ops.calls.c_str());
|
||||
}
|
||||
static void test_prog_on_a_one_button_garage_door_is_toggle() {
|
||||
ops.shades[4] = rollerShade();
|
||||
ops.shades[4].shadeType = shade_types::garage1;
|
||||
shade_request_t req = request(shade_request_type_t::shade_repeat);
|
||||
req.command = somfy_commands::Prog;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Toggle repeat=2 step=0", ops.calls.c_str());
|
||||
}
|
||||
static void test_group_repeat_repeats_the_last_frame() {
|
||||
ops.groups[2] = 3;
|
||||
ops.lastCommand = true;
|
||||
requests.run(request(shade_request_type_t::group_repeat, 2));
|
||||
TEST_ASSERT_EQUAL_STRING("repeatGroupFrame 2 repeat=3", ops.calls.c_str());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Sensor frames
|
||||
********************************************************************/
|
||||
static void test_sensor_sends_what_was_given() {
|
||||
ops.shades[4] = rollerShade();
|
||||
shade_request_t req = request(shade_request_type_t::shade_sensor);
|
||||
req.sunny = 1;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("sendSensorCommand 4 windy=-1 sunny=1 repeat=2", ops.calls.c_str());
|
||||
}
|
||||
static void test_group_sensor_uses_the_groups_repeats() {
|
||||
ops.groups[2] = 3;
|
||||
shade_request_t req = request(shade_request_type_t::group_sensor, 2);
|
||||
req.windy = 0;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("sendGroupSensorCommand 2 windy=0 sunny=-1 repeat=3", ops.calls.c_str());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* The favorite
|
||||
********************************************************************/
|
||||
static void test_favorite_on_a_flipped_shade_is_turned_round() {
|
||||
ops.shades[4] = flippedShade();
|
||||
shade_request_t req = request(shade_request_type_t::set_favorite);
|
||||
req.position = 30;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 70 -1", ops.calls.c_str());
|
||||
}
|
||||
static void test_favorite_without_a_tilt_keeps_the_favorite_tilt() {
|
||||
// MQTT mypos used to set the favorite tilt to 0 here.
|
||||
ops.shades[4] = tiltShade(tilt_types::integrated);
|
||||
ops.shades[4].myTiltPos = 40.0f;
|
||||
shade_request_t req = request(shade_request_type_t::set_favorite);
|
||||
req.position = 30;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 30 40", ops.calls.c_str());
|
||||
}
|
||||
static void test_favorite_tilt_alone_keeps_the_favorite_lift() {
|
||||
ops.shades[4] = tiltShade(tilt_types::integrated);
|
||||
ops.shades[4].myPos = 60.0f;
|
||||
ops.shades[4].currentPos = 20.0f;
|
||||
shade_request_t req = request(shade_request_type_t::set_favorite);
|
||||
req.tiltPosition = 25;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 60 25", ops.calls.c_str());
|
||||
}
|
||||
static void test_favorite_tilt_alone_without_a_favorite_uses_where_the_shade_is() {
|
||||
ops.shades[4] = tiltShade(tilt_types::integrated);
|
||||
ops.shades[4].currentPos = 20.0f;
|
||||
shade_request_t req = request(shade_request_type_t::set_favorite);
|
||||
req.tiltPosition = 25;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 20 25", ops.calls.c_str());
|
||||
}
|
||||
static void test_favorite_tilt_on_a_flipped_shade_is_turned_round() {
|
||||
ops.shades[4] = flippedShade();
|
||||
ops.shades[4].tiltType = tilt_types::integrated;
|
||||
shade_request_t req = request(shade_request_type_t::set_favorite);
|
||||
req.position = 30;
|
||||
req.tiltPosition = 10;
|
||||
requests.run(req);
|
||||
TEST_ASSERT_EQUAL_STRING("setMyPosition 4 70 90", ops.calls.c_str());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Pairing and rolling codes
|
||||
********************************************************************/
|
||||
static void test_unpair_holds_prog_longer_on_a_56_bit_motor() {
|
||||
ops.shades[4] = rollerShade();
|
||||
ops.shades[5] = rollerShade();
|
||||
ops.shades[5].bitLength = 80;
|
||||
requests.run(request(shade_request_type_t::unpair, 4));
|
||||
requests.run(request(shade_request_type_t::unpair, 5));
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"sendCommand 4 Prog repeat=7 step=0; setUnpaired 4; sendCommand 5 Prog repeat=1 step=0; setUnpaired 5",
|
||||
ops.calls.c_str());
|
||||
}
|
||||
static void test_unpair_the_radio_refused_leaves_the_shade_paired() {
|
||||
ops.shades[4] = rollerShade();
|
||||
ops.radioTakes = false;
|
||||
TEST_ASSERT_FALSE(requests.run(request(shade_request_type_t::unpair)));
|
||||
TEST_ASSERT_EQUAL_STRING("sendCommand 4 Prog repeat=7 step=0", ops.calls.c_str());
|
||||
}
|
||||
static void test_rolling_code_is_set() {
|
||||
ops.shades[4] = rollerShade();
|
||||
shade_request_t req = request(shade_request_type_t::set_rolling_code);
|
||||
req.rollingCode = 1234;
|
||||
TEST_ASSERT_TRUE(requests.run(req));
|
||||
TEST_ASSERT_EQUAL_STRING("setRollingCode 4 1234", ops.calls.c_str());
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* The radio refusing
|
||||
********************************************************************/
|
||||
static void test_a_refused_command_is_reported() {
|
||||
ops.shades[4] = rollerShade();
|
||||
ops.radioTakes = false;
|
||||
shade_request_t req = request(shade_request_type_t::shade_command);
|
||||
req.target = 50;
|
||||
TEST_ASSERT_FALSE(requests.run(req));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_a_missing_shade_is_dropped);
|
||||
RUN_TEST(test_a_missing_group_is_dropped);
|
||||
RUN_TEST(test_a_shade_id_is_not_a_group_id);
|
||||
RUN_TEST(test_target_moves_a_shade_there);
|
||||
RUN_TEST(test_target_on_a_flipped_shade_is_turned_round);
|
||||
RUN_TEST(test_tilt_target_on_a_flipped_shade_is_turned_round);
|
||||
RUN_TEST(test_without_a_target_the_command_is_sent);
|
||||
RUN_TEST(test_tilt_without_a_target_sends_the_tilt_command);
|
||||
RUN_TEST(test_set_positions_on_a_flipped_shade_are_turned_round);
|
||||
RUN_TEST(test_set_positions_leaves_the_tilt_of_a_shade_without_one);
|
||||
RUN_TEST(test_set_positions_with_nothing_in_range_does_nothing);
|
||||
RUN_TEST(test_client_values_are_checked_on_the_way_in);
|
||||
RUN_TEST(test_no_repeat_means_the_shades_own);
|
||||
RUN_TEST(test_repeat_zero_means_no_repeats_for_a_shade_too);
|
||||
RUN_TEST(test_no_repeat_means_the_groups_own);
|
||||
RUN_TEST(test_repeat_sends_a_new_command_when_it_was_not_the_last);
|
||||
RUN_TEST(test_repeat_repeats_the_last_frame);
|
||||
RUN_TEST(test_prog_on_a_one_button_garage_door_is_toggle);
|
||||
RUN_TEST(test_group_repeat_repeats_the_last_frame);
|
||||
RUN_TEST(test_sensor_sends_what_was_given);
|
||||
RUN_TEST(test_group_sensor_uses_the_groups_repeats);
|
||||
RUN_TEST(test_favorite_on_a_flipped_shade_is_turned_round);
|
||||
RUN_TEST(test_favorite_without_a_tilt_keeps_the_favorite_tilt);
|
||||
RUN_TEST(test_favorite_tilt_alone_keeps_the_favorite_lift);
|
||||
RUN_TEST(test_favorite_tilt_alone_without_a_favorite_uses_where_the_shade_is);
|
||||
RUN_TEST(test_favorite_tilt_on_a_flipped_shade_is_turned_round);
|
||||
RUN_TEST(test_unpair_holds_prog_longer_on_a_56_bit_motor);
|
||||
RUN_TEST(test_unpair_the_radio_refused_leaves_the_shade_paired);
|
||||
RUN_TEST(test_rolling_code_is_set);
|
||||
RUN_TEST(test_a_refused_command_is_reported);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
#include <unity.h>
|
||||
#include "WebCommand.h"
|
||||
|
||||
static const web_cmd_t RADIO_COMMANDS[] = {
|
||||
web_cmd_t::shade_command, web_cmd_t::group_command, web_cmd_t::tilt_command,
|
||||
web_cmd_t::shade_repeat, web_cmd_t::group_repeat, web_cmd_t::shade_sensor,
|
||||
web_cmd_t::group_sensor, web_cmd_t::set_my_position, web_cmd_t::unpair_shade
|
||||
};
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static const web_cmd_t LOOP_COMMANDS[] = {
|
||||
web_cmd_t::set_positions, web_cmd_t::set_rolling_code, web_cmd_t::restore_shades, web_cmd_t::load_shades
|
||||
};
|
||||
|
||||
static void test_only_loop_commands_stay_off_the_radio() {
|
||||
int offRadio = 0;
|
||||
for(uint8_t t = 0; t <= static_cast<uint8_t>(web_cmd_t::load_shades); t++) {
|
||||
if(!webCommandUsesRadio(static_cast<web_cmd_t>(t))) offRadio++;
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(4, offRadio);
|
||||
for(web_cmd_t t : LOOP_COMMANDS) TEST_ASSERT_FALSE(webCommandUsesRadio(t));
|
||||
}
|
||||
static void test_loop_commands_are_never_held() {
|
||||
// Nothing goes on air, so a full or suspended radio doesn't matter.
|
||||
for(web_cmd_t t : LOOP_COMMANDS) {
|
||||
TEST_ASSERT_FALSE(holdWebCommand(t, false, false));
|
||||
TEST_ASSERT_FALSE(holdWebCommand(t, true, false));
|
||||
}
|
||||
}
|
||||
static void test_radio_command_waits_for_room() {
|
||||
for(web_cmd_t t : RADIO_COMMANDS) {
|
||||
TEST_ASSERT_TRUE(holdWebCommand(t, false, false));
|
||||
TEST_ASSERT_FALSE(holdWebCommand(t, false, true));
|
||||
}
|
||||
}
|
||||
static void test_commands_are_not_held_while_suspended() {
|
||||
// Nothing goes on air during a frequency scan or OTA anyway.
|
||||
for(web_cmd_t t : RADIO_COMMANDS) {
|
||||
if(t == web_cmd_t::unpair_shade) continue;
|
||||
TEST_ASSERT_FALSE(holdWebCommand(t, true, false));
|
||||
TEST_ASSERT_FALSE(holdWebCommand(t, true, true));
|
||||
}
|
||||
}
|
||||
static void test_unpair_waits_out_a_suspend() {
|
||||
// The client was already told the shade is unpaired.
|
||||
TEST_ASSERT_TRUE(holdWebCommand(web_cmd_t::unpair_shade, true, false));
|
||||
TEST_ASSERT_TRUE(holdWebCommand(web_cmd_t::unpair_shade, true, true));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_only_loop_commands_stay_off_the_radio);
|
||||
RUN_TEST(test_loop_commands_are_never_held);
|
||||
RUN_TEST(test_radio_command_waits_for_room);
|
||||
RUN_TEST(test_commands_are_not_held_while_suspended);
|
||||
RUN_TEST(test_unpair_waits_out_a_suspend);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue