Browse Source

api: rework plain and JSON implementations (#2405)

- match paths through a custom AsyncWebHandler instead of using generic not-found fallback handler
- allow MQTT-like patterns when registering paths (`simple/path`, `path/+/something`, `path/#`)
Replaces `relay/0`, `relay/1` etc. with `relay/+`. Magnitudes are plain paths, but using `/+` in case there's more than 1 magnitude of the same type.
- restore `std::function` as callback container (no more single-byte arg nonsense). Still, limit to 1 type per handler type
- adds JSON handlers which will receive JsonObject root as both input and output. Same logic as plain - GET returns resource data, PUT updates it.
- breaking change to `apiAuthenticate(request)`, it no longer will do `request->send(403)` and expect this to be handled externally.
- allow `Api-Key` header containing the key, works for both GET & PUT plain requests. The only way to set apikey for JSON.
- add `ApiRequest::param` to retrieve both GET and PUT params (aka args), remove ApiBuffer
- remove `API_BUFFER_SIZE`. Allow custom form-data key=value pairs for requests, allow to send basic `String`.
- add `API_JSON_BUFFER_SIZE` for the JSON buffer (both input and output)
- `/apis` replaced with `/api/list`, no longer uses custom handler and is an `apiRegister` callback
- `/api/rpc` custom handler replaced with an `apiRegister` callback

WIP further down:
- no more `webLog` for API requests, unless `webAccessLog` / `WEB_ACCESS_LOG` is set to `1`. This also needs to happen to the other handlers. 
- migrate to ArduinoJson v6, since it become apparent it is actually a good upgrade :)
- actually make use of JSON endpoints more, right now it's just existing GET for sensors and relays
- fork ESPAsyncWebServer to cleanup path parsing and temporary objects attached to the request (also, fix things a lot of things based on PRs there...)
mcspr-patch-1
Max Prokhorov 3 years ago
committed by GitHub
parent
commit
8e80a7786c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1364 additions and 505 deletions
  1. +1
    -0
      code/espurna/alexa.cpp
  2. +622
    -151
      code/espurna/api.cpp
  3. +14
    -66
      code/espurna/api.h
  4. +27
    -10
      code/espurna/api_common.cpp
  5. +182
    -0
      code/espurna/api_impl.h
  6. +11
    -3
      code/espurna/config/general.h
  7. +5
    -0
      code/espurna/encoder.cpp
  8. +0
    -5
      code/espurna/encoder.h
  9. +137
    -73
      code/espurna/light.cpp
  10. +6
    -2
      code/espurna/light.h
  11. +1
    -1
      code/espurna/ota_web.cpp
  12. +27
    -11
      code/espurna/prometheus.cpp
  13. +158
    -114
      code/espurna/relay.cpp
  14. +21
    -20
      code/espurna/rfbridge.cpp
  15. +73
    -27
      code/espurna/sensor.cpp
  16. +1
    -1
      code/espurna/ssdp.cpp
  17. +55
    -13
      code/espurna/terminal.cpp
  18. +20
    -5
      code/espurna/web.cpp
  19. +1
    -1
      code/espurna/web.h
  20. +2
    -2
      code/espurna/ws.cpp

+ 1
- 0
code/espurna/alexa.cpp View File

@ -12,6 +12,7 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <queue>
#include "api.h"
#include "broker.h"
#include "light.h"
#include "relay.h"


+ 622
- 151
code/espurna/api.cpp View File

@ -12,8 +12,6 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#if API_SUPPORT
#include <vector>
#include "system.h"
#include "web.h"
#include "rpc.h"
@ -21,252 +19,725 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include <ESPAsyncTCP.h>
#include <ArduinoJson.h>
constexpr size_t ApiPathSizeMax { 64ul };
std::vector<Api> _apis;
#include <algorithm>
#include <cstring>
#include <forward_list>
#include <vector>
// -----------------------------------------------------------------------------
// API
// -----------------------------------------------------------------------------
bool _asJson(AsyncWebServerRequest *request) {
bool asJson = false;
if (request->hasHeader("Accept")) {
AsyncWebHeader* h = request->getHeader("Accept");
asJson = h->value().equals("application/json");
PathParts::PathParts(const String& path) :
_path(path)
{
if (!_path.length()) {
_ok = false;
return;
}
PathPart::Type type { PathPart::Type::Unknown };
size_t length { 0ul };
size_t offset { 0ul };
const char* p { _path.c_str() };
if (*p == '\0') {
goto error;
}
_parts.reserve(std::count(_path.begin(), _path.end(), '/') + 1);
start:
type = PathPart::Type::Unknown;
length = 0;
offset = p - _path.c_str();
switch (*p) {
case '+':
goto parse_single_wildcard;
case '#':
goto parse_multi_wildcard;
case '/':
default:
goto parse_value;
}
return asJson;
}
void _onAPIsText(AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("text/plain");
char buffer[ApiPathSizeMax] = {0};
for (auto& api : _apis) {
sprintf_P(buffer, PSTR("/api/%s\n"), api.path.c_str());
response->write(buffer);
parse_value:
type = PathPart::Type::Value;
switch (*p) {
case '+':
case '#':
goto error;
case '/':
case '\0':
goto push_result;
}
++p;
++length;
goto parse_value;
parse_single_wildcard:
type = PathPart::Type::SingleWildcard;
++p;
switch (*p) {
case '/':
++p;
case '\0':
goto push_result;
}
goto error;
parse_multi_wildcard:
type = PathPart::Type::MultiWildcard;
++p;
if (*p == '\0') {
goto push_result;
}
goto error;
push_result:
emplace_back(type, offset, length);
if (*p == '/') {
++p;
goto start;
} else if (*p != '\0') {
goto start;
}
request->send(response);
goto success;
error:
_ok = false;
_parts.clear();
return;
success:
_ok = true;
}
constexpr size_t ApiJsonBufferSize = 1024;
// match when, for example, given the path 'topic/one/two/three' and pattern 'topic/+/two/+'
bool PathParts::match(const PathParts& path) const {
if (!_ok || !path) {
return false;
}
void _onAPIsJson(AsyncWebServerRequest *request) {
auto lhs = begin();
auto rhs = path.begin();
DynamicJsonBuffer jsonBuffer(ApiJsonBufferSize);
JsonArray& root = jsonBuffer.createArray();
auto lhs_end = end();
auto rhs_end = path.end();
char buffer[ApiPathSizeMax] = {0};
for (auto& api : _apis) {
sprintf(buffer, "/api/%s", api.path.c_str());
root.add(buffer);
loop:
if (lhs == lhs_end) {
goto check_end;
}
AsyncResponseStream *response = request->beginResponseStream("application/json");
root.printTo(*response);
request->send(response);
switch ((*lhs).type) {
case PathPart::Type::Value:
if (
(rhs != rhs_end)
&& ((*rhs).type == PathPart::Type::Value)
&& ((*rhs).offset == (*lhs).offset)
&& ((*rhs).length == (*lhs).length)
) {
if (0 == std::memcmp(
_path.c_str() + (*lhs).offset,
path.path().c_str() + (*rhs).offset,
(*rhs).length))
{
std::advance(lhs, 1);
std::advance(rhs, 1);
goto loop;
}
}
goto error;
case PathPart::Type::SingleWildcard:
if (
(rhs != rhs_end)
&& ((*rhs).type == PathPart::Type::Value)
) {
std::advance(lhs, 1);
std::advance(rhs, 1);
goto loop;
}
goto error;
case PathPart::Type::MultiWildcard:
if (std::next(lhs) == lhs_end) {
while (rhs != rhs_end) {
if ((*rhs).type != PathPart::Type::Value) {
goto error;
}
std::advance(rhs, 1);
}
lhs = lhs_end;
break;
}
goto error;
case PathPart::Type::Unknown:
goto error;
};
check_end:
if ((lhs == lhs_end) && (rhs == rhs_end)) {
return true;
}
error:
return false;
}
void _onAPIs(AsyncWebServerRequest *request) {
String ApiRequest::wildcard(int index) const {
if (index < 0) {
index = std::abs(index + 1);
}
webLog(request);
if (!apiAuthenticate(request)) return;
if (std::abs(index) >= _pattern.parts().size()) {
return _empty_string();
}
bool asJson = _asJson(request);
int counter { 0 };
auto& pattern = _pattern.parts();
String output;
if (asJson) {
_onAPIsJson(request);
} else {
_onAPIsText(request);
for (unsigned int part = 0; part < pattern.size(); ++part) {
auto& lhs = pattern[part];
if (PathPart::Type::SingleWildcard == lhs.type) {
if (counter == index) {
auto& rhs = _parts.parts()[part];
return _parts.path().substring(rhs.offset, rhs.offset + rhs.length);
}
++counter;
}
}
return _empty_string();
}
void _onRPC(AsyncWebServerRequest *request) {
size_t ApiRequest::wildcards() const {
size_t result { 0ul };
for (auto& part : _pattern) {
if (PathPart::Type::SingleWildcard == part.type) {
++result;
}
}
webLog(request);
if (!apiAuthenticate(request)) return;
return result;
}
//bool asJson = _asJson(request);
int response = 404;
// -----------------------------------------------------------------------------
if (request->hasParam("action")) {
bool _apiAccepts(AsyncWebServerRequest* request, const __FlashStringHelper* str) {
auto* header = request->getHeader(F("Accept"));
if (header) {
return
(header->value().indexOf(F("*/*")) >= 0)
|| (header->value().indexOf(str) >= 0);
}
AsyncWebParameter* p = request->getParam("action");
return false;
}
const auto action = p->value();
DEBUG_MSG_P(PSTR("[RPC] Action: %s\n"), action.c_str());
bool _apiAcceptsText(AsyncWebServerRequest* request) {
return _apiAccepts(request, F("text/plain"));
}
if (rpcHandleAction(action)) {
response = 204;
}
bool _apiAcceptsJson(AsyncWebServerRequest* request) {
return _apiAccepts(request, F("application/json"));
}
bool _apiMatchHeader(AsyncWebServerRequest* request, const __FlashStringHelper* key, const __FlashStringHelper* value) {
auto* header = request->getHeader(key);
if (header) {
return header->value().equals(value);
}
request->send(response);
return false;
}
bool _apiIsJsonContent(AsyncWebServerRequest* request) {
return _apiMatchHeader(request, F("Content-Type"), F("application/json"));
}
struct ApiMatch {
Api* api { nullptr };
Api::Type type { Api::Type::Basic };
};
bool _apiIsFormDataContent(AsyncWebServerRequest* request) {
return _apiMatchHeader(request, F("Content-Type"), F("application/x-www-form-urlencoded"));
}
ApiMatch _apiMatch(const String& url, AsyncWebServerRequest* request) {
struct ApiRequestHelper {
ApiRequestHelper(const ApiRequestHelper&) = delete;
ApiRequestHelper(ApiRequestHelper&&) noexcept = default;
ApiMatch result;
char buffer[ApiPathSizeMax] = {0};
// &path is expected to be request->url(), which is valid throughout the request's lifetime
explicit ApiRequestHelper(AsyncWebServerRequest& request, const PathParts& pattern) :
_request(request),
_pattern(pattern),
_path(request.url()),
_match(_pattern.match(_path))
{}
for (auto& api : _apis) {
sprintf_P(buffer, PSTR("/api/%s"), api.path.c_str());
if (url != buffer) {
continue;
}
ApiRequest request() const {
return ApiRequest(_request, _pattern, _path);
}
auto type = _asJson(request)
? Api::Type::Json
: Api::Type::Basic;
const PathParts& parts() const {
return _path;
}
result.api = &api;
result.type = type;
break;
bool match() const {
return _match;
}
return result;
private:
AsyncWebServerRequest& _request;
const PathParts& _pattern;
PathParts _path;
bool _match;
};
// Because the webserver request is split between multiple separate function invocations, we need to preserve some state.
// TODO: in case we are dealing with multicore, perhaps enforcing static-size data structs instead of the vector would we better,
// to avoid calling generic malloc when paths are parsed?
//
// Some quirks to deal with:
// - handleBody is called before handleRequest, and there's no way to signal completion / success of both callbacks to the server
// - Server never checks for request closing in filter or canHandle, so if we don't want to handle large content-length, it
// will still flow through the lwip backend.
// - `request->_tempObject` is used to keep API request state, but it's just a plain void pointer
// - espasyncwebserver will `free(_tempObject)` when request is disconnected, but only after this callbackhandler is done.
// make sure it's set to nullptr via `AsyncWebServerRequest::onDisconnect`
// - ALL headers are parsed (and we could access those during filter and canHandle callbacks), but we need to explicitly
// request them to stay in memory so that the actual handler can work with them
void _apiAttachHelper(AsyncWebServerRequest& request, ApiRequestHelper&& helper) {
request._tempObject = new ApiRequestHelper(std::move(helper));
request.onDisconnect([&]() {
auto* ptr = reinterpret_cast<ApiRequestHelper*>(request._tempObject);
delete ptr;
request._tempObject = nullptr;
});
request.addInterestingHeader(F("Api-Key"));
}
bool _apiDispatchRequest(const String& url, AsyncWebServerRequest* request) {
class ApiBaseWebHandler : public AsyncWebHandler {
public:
ApiBaseWebHandler() = delete;
ApiBaseWebHandler(const ApiBaseWebHandler&) = delete;
ApiBaseWebHandler(ApiBaseWebHandler&&) = delete;
auto match = _apiMatch(url, request);
if (!match.api) {
return false;
// In case this needs to be copied or moved, ensure PathParts copy references the new object's string
template <typename Pattern>
explicit ApiBaseWebHandler(Pattern&& pattern) :
_pattern(std::forward<Pattern>(pattern)),
_parts(_pattern)
{}
const String& pattern() const {
return _pattern;
}
if (match.type != match.api->type) {
DEBUG_MSG_P(PSTR("[API] Cannot handle the request type\n"));
request->send(404);
return true;
const PathParts& parts() const {
return _parts;
}
const bool is_put = (
(!apiRestFul() || (request->method() == HTTP_PUT))
&& request->hasParam("value", request->method() == HTTP_PUT)
);
private:
String _pattern;
PathParts _parts;
};
ApiBuffer buffer;
// 'Modernized' API configuration:
// - `Api-Key` header for both GET and PUT
// - Parse request body as JSON object. Limited to LWIP internal buffer size, and will also break when client
// does weird stuff and PUTs data in multiple packets b/c only the initial packet is parsed.
// - Same as the text/plain, when ApiRequest::handle was not called it will then call GET
//
// TODO: bump to arduinojson v6 to handle partial / broken data payloads
// TODO: somehow detect partial data and buffer (optionally)
// TODO: POST instead of PUT?
class ApiJsonWebHandler final : public ApiBaseWebHandler {
public:
static constexpr size_t BufferSize { API_JSON_BUFFER_SIZE };
struct ReadOnlyStream : public Stream {
ReadOnlyStream() = delete;
explicit ReadOnlyStream(const uint8_t* buffer, size_t size) :
_buffer(buffer),
_size(size)
{}
int available() override {
return _size - _index;
}
switch (match.api->type) {
int peek() override {
if (_index < _size) {
return static_cast<int>(_buffer[_index]);
}
case Api::Type::Basic: {
if (!match.api->get.basic) {
break;
return -1;
}
if (is_put) {
if (!match.api->put.basic) {
break;
int read() override {
auto peeked = peek();
if (peeked >= 0) {
++_index;
}
auto value = request->getParam("value", request->method() == HTTP_PUT)->value();
if (buffer.size < (value.length() + 1ul)) {
break;
return peeked;
}
// since we are fixed in size, no need for any timeouts and the only available option is to return full chunk of data
size_t readBytes(uint8_t* ptr, size_t size) override {
if ((_index < _size) && ((_size - _index) >= size)) {
std::copy(_buffer + _index, _buffer + _index + size, ptr);
_index += size;
return size;
}
std::copy(value.c_str(), value.c_str() + value.length() + 1, buffer.data);
match.api->put.basic(*match.api, buffer);
buffer.erase();
return 0;
}
match.api->get.basic(*match.api, buffer);
request->send(200, "text/plain", buffer.data);
size_t readBytes(char* ptr, size_t size) override {
return readBytes(reinterpret_cast<uint8_t*>(ptr), size);
}
void flush() override {
}
size_t write(const uint8_t*, size_t) override {
return 0;
}
size_t write(uint8_t) override {
return 0;
}
const uint8_t* _buffer;
const size_t _size;
size_t _index { 0 };
};
ApiJsonWebHandler() = delete;
ApiJsonWebHandler(const ApiJsonWebHandler&) = delete;
ApiJsonWebHandler(ApiJsonWebHandler&&) = delete;
template <typename Path, typename Callback>
ApiJsonWebHandler(Path&& path, Callback&& get, Callback&& put) :
ApiBaseWebHandler(std::forward<Path>(path)),
_get(std::forward<Callback>(get)),
_put(std::forward<Callback>(put))
{}
bool isRequestHandlerTrivial() override {
return true;
}
// TODO: pass the body instead of `value` param
// TODO: handle HTTP_PUT
case Api::Type::Json: {
if (!match.api->get.json || is_put) {
break;
bool canHandle(AsyncWebServerRequest* request) override {
if (!apiEnabled()) {
return false;
}
DynamicJsonBuffer jsonBuffer(API_BUFFER_SIZE);
if (!_apiAcceptsJson(request)) {
return false;
}
auto helper = ApiRequestHelper(*request, parts());
if (helper.match() && apiAuthenticate(request)) {
switch (request->method()) {
case HTTP_HEAD:
return true;
case HTTP_PUT:
if (!_apiIsJsonContent(request)) {
return false;
}
if (!_put) {
return false;
}
case HTTP_GET:
if (!_get) {
return false;
}
break;
default:
return false;
}
_apiAttachHelper(*request, std::move(helper));
return true;
}
return false;
}
void _handleGet(AsyncWebServerRequest* request, ApiRequest& apireq) {
DynamicJsonBuffer jsonBuffer(API_JSON_BUFFER_SIZE);
JsonObject& root = jsonBuffer.createObject();
if (!_get(apireq, root)) {
request->send(500);
return;
}
match.api->get.json(*match.api, root);
if (!apireq.done()) {
AsyncResponseStream *response = request->beginResponseStream("application/json", root.measureLength() + 1);
root.printTo(*response);
request->send(response);
return;
}
AsyncResponseStream *response = request->beginResponseStream("application/json", root.measureLength() + 1);
root.printTo(*response);
request->send(response);
request->send(500);
}
return true;
void _handlePut(AsyncWebServerRequest* request, uint8_t* data, size_t size) {
// XXX: arduinojson v5 de-serializer will happily read garbage from raw ptr, since there's no length limit
// this is fixed in v6 though. for now, use a wrapper, but be aware that this actually uses more mem for the jsonbuffer
DynamicJsonBuffer jsonBuffer(API_JSON_BUFFER_SIZE);
ReadOnlyStream stream(data, size);
JsonObject& root = jsonBuffer.parseObject(stream);
if (!root.success()) {
request->send(500);
return;
}
auto& helper = *reinterpret_cast<ApiRequestHelper*>(request->_tempObject);
auto apireq = helper.request();
if (!_put(apireq, root)) {
request->send(500);
return;
}
if (!apireq.done()) {
_handleGet(request, apireq);
}
return;
}
void handleBody(AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t, size_t total) override {
if (total && (len == total)) {
_handlePut(request, data, total);
}
}
DEBUG_MSG_P(PSTR("[API] Method not supported\n"));
request->send(405);
void handleRequest(AsyncWebServerRequest* request) override {
auto& helper = *reinterpret_cast<ApiRequestHelper*>(request->_tempObject);
return true;
switch (request->method()) {
case HTTP_HEAD:
request->send(204);
return;
}
case HTTP_GET: {
auto apireq = helper.request();
_handleGet(request, apireq);
return;
}
bool _apiRequestCallback(AsyncWebServerRequest* request) {
// see handleBody()
case HTTP_PUT:
break;
String url = request->url();
default:
request->send(405);
break;
}
}
if (url.equals("/rpc")) {
_onRPC(request);
return true;
const String& pattern() const {
return ApiBaseWebHandler::pattern();
}
if (url.equals("/api") || url.equals("/apis")) {
_onAPIs(request);
return true;
const PathParts& parts() const {
return ApiBaseWebHandler::parts();
}
if (!url.startsWith("/api/")) return false;
private:
ApiJsonHandler _get;
ApiJsonHandler _put;
};
// [alexa] don't call the http api -> response for alexa is done by fauxmoesp lib
#if ALEXA_SUPPORT
if (url.indexOf("/lights") > 14 ) return false;
#endif
// ESPurna legacy API configuration
// - ?apikey=... to authorize in GET or PUT
// - ?anything=... for input data (common key is "value")
// MUST correctly override isRequestHandlerTrivial() to allow auth with PUT
// (i.e. so that ESPAsyncWebServer parses the body and adds form-data to request params list)
class ApiBasicWebHandler final : public ApiBaseWebHandler {
public:
template <typename Path, typename Callback>
ApiBasicWebHandler(Path&& path, Callback&& get, Callback&& put) :
ApiBaseWebHandler(std::forward<Path>(path)),
_get(std::forward<Callback>(get)),
_put(std::forward<Callback>(put))
{}
bool isRequestHandlerTrivial() override {
return false;
}
if (!apiAuthenticate(request)) return false;
bool canHandle(AsyncWebServerRequest* request) override {
if (!apiEnabled()) {
return false;
}
return _apiDispatchRequest(url, request);
if (!_apiAcceptsText(request)) {
return false;
}
}
switch (request->method()) {
case HTTP_HEAD:
case HTTP_GET:
break;
case HTTP_PUT:
if (!_apiIsFormDataContent(request)) {
return false;
}
break;
default:
return false;
}
auto helper = ApiRequestHelper(*request, parts());
if (helper.match()) {
_apiAttachHelper(*request, std::move(helper));
return true;
}
return false;
}
void handleRequest(AsyncWebServerRequest* request) override {
if (!apiAuthenticate(request)) {
request->send(403);
return;
}
auto method = request->method();
const bool is_put = (
(!apiRestFul()|| (HTTP_PUT == method))
&& request->hasParam("value", HTTP_PUT == method)
);
switch (method) {
case HTTP_HEAD:
request->send(204);
return;
case HTTP_GET:
case HTTP_PUT: {
auto& helper = *reinterpret_cast<ApiRequestHelper*>(request->_tempObject);
auto apireq = helper.request();
if (is_put) {
if (!_put(apireq)) {
request->send(500);
return;
}
if (apireq.done()) {
return;
}
}
if (!_get(apireq)) {
request->send(500);
return;
}
if (!apireq.done()) {
request->send(204);
return;
}
}
default:
request->send(405);
return;
}
}
const ApiBasicHandler& get() const {
return _get;
}
const ApiBasicHandler& put() const {
return _put;
}
const String& pattern() const {
return ApiBaseWebHandler::pattern();
}
const PathParts& parts() const {
return ApiBaseWebHandler::parts();
}
private:
ApiBasicHandler _get;
ApiBasicHandler _put;
};
// -----------------------------------------------------------------------------
void apiReserve(size_t size) {
_apis.reserve(_apis.size() + size);
namespace {
std::forward_list<ApiBaseWebHandler*> _apis;
template <typename Handler, typename Callback>
void _apiRegister(const String& path, Callback&& get, Callback&& put) {
// `String` is a given, since we *do* need to construct this dynamically in sensors
auto* ptr = new Handler(String(F(API_BASE_PATH)) + path, std::forward<Callback>(get), std::forward<Callback>(put));
webServer().addHandler(reinterpret_cast<AsyncWebHandler*>(ptr));
_apis.emplace_front(ptr);
}
void apiRegister(const Api& api) {
if (api.path.length() >= (ApiPathSizeMax - strlen("/api/") - 1ul)) {
return;
}
_apis.push_back(api);
} // namespace
void apiRegister(const String& path, ApiBasicHandler&& get, ApiBasicHandler&& put) {
_apiRegister<ApiBasicWebHandler>(path, std::move(get), std::move(put));
}
void apiRegister(const String& path, ApiJsonHandler&& get, ApiJsonHandler&& put) {
_apiRegister<ApiJsonWebHandler>(path, std::move(get), std::move(put));
}
void apiSetup() {
webRequestRegister(_apiRequestCallback);
apiRegister(F("list"),
[](ApiRequest& request) {
String paths;
for (auto& api : _apis) {
paths += api->pattern() + "\r\n";
}
request.send(paths);
return true;
},
nullptr
);
apiRegister(F("rpc"),
nullptr,
[](ApiRequest& request) {
if (rpcHandleAction(request.param(F("action")))) {
return apiOk(request);
}
return apiError(request);
}
);
}
void apiOk(const Api&, ApiBuffer& buffer) {
buffer.data[0] = 'O';
buffer.data[1] = 'K';
buffer.data[2] = '\0';
bool apiOk(ApiRequest& request) {
request.send(F("OK"));
return true;
}
void apiError(const Api&, ApiBuffer& buffer) {
buffer.data[0] = '-';
buffer.data[1] = 'E';
buffer.data[2] = 'R';
buffer.data[3] = 'R';
buffer.data[4] = 'O';
buffer.data[5] = 'R';
buffer.data[6] = '\0';
bool apiError(ApiRequest& request) {
request.send(F("ERROR"));
return true;
}
#endif // API_SUPPORT


+ 14
- 66
code/espurna/api.h View File

@ -13,7 +13,10 @@ Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
#if WEB_SUPPORT
bool apiAuthenticateHeader(AsyncWebServerRequest*, const String& key);
bool apiAuthenticateParam(AsyncWebServerRequest*, const String& key);
bool apiAuthenticate(AsyncWebServerRequest*);
void apiCommonSetup();
bool apiEnabled();
bool apiRestFul();
String apiKey();
@ -22,74 +25,19 @@ String apiKey();
#if WEB_SUPPORT && API_SUPPORT
#include <vector>
constexpr unsigned char ApiUnusedArg = 0u;
struct ApiBuffer {
constexpr static size_t size = API_BUFFER_SIZE;
char data[size];
void erase() {
std::fill(data, data + size, '\0');
}
};
struct Api {
using BasicHandler = void(*)(const Api& api, ApiBuffer& buffer);
using JsonHandler = void(*)(const Api& api, JsonObject& root);
enum class Type {
Basic,
Json
};
Api() = delete;
// TODO:
// - bind to multiple paths, dispatch specific path in the callback
// - allow index to be passed through path argument (/{arg1}/{arg2} syntax, for example)
Api(const String& path_, Type type_, unsigned char arg_, BasicHandler get_, BasicHandler put_ = nullptr) :
path(path_),
type(type_),
arg(arg_)
{
get.basic = get_;
put.basic = put_;
}
Api(const String& path_, Type type_, unsigned char arg_, JsonHandler get_, JsonHandler put_ = nullptr) :
path(path_),
type(type_),
arg(arg_)
{
get.json = get_;
put.json = put_;
}
String path;
Type type;
unsigned char arg;
union {
BasicHandler basic;
JsonHandler json;
} get;
union {
BasicHandler basic;
JsonHandler json;
} put;
};
void apiRegister(const Api& api);
#include "api_impl.h"
void apiCommonSetup();
void apiSetup();
#include <functional>
void apiReserve(size_t);
using ApiBasicHandler = std::function<bool(ApiRequest&)>;
using ApiJsonHandler = std::function<bool(ApiRequest&, JsonObject& reponse)>;
void apiRegister(const String& path, ApiBasicHandler&& get, ApiBasicHandler&& put);
void apiRegister(const String& path, ApiJsonHandler&& get, ApiJsonHandler&& put);
void apiSetup();
void apiError(const Api&, ApiBuffer& buffer);
void apiOk(const Api&, ApiBuffer& buffer);
bool apiError(ApiRequest&);
bool apiOk(ApiRequest&);
#endif // API_SUPPORT == 1

+ 27
- 10
code/espurna/api_common.cpp View File

@ -49,24 +49,41 @@ String apiKey() {
return getSetting("apiKey", API_KEY);
}
bool apiAuthenticate(AsyncWebServerRequest *request) {
bool apiAuthenticateHeader(AsyncWebServerRequest* request, const String& key) {
if (apiEnabled() && key.length()) {
auto* header = request->getHeader(F("Api-Key"));
if (header && (key == header->value())) {
return true;
}
}
return false;
}
bool apiAuthenticateParam(AsyncWebServerRequest* request, const String& key) {
auto* param = request->getParam("apikey", (request->method() == HTTP_PUT));
if (param && (key == param->value())) {
return true;
}
return false;
}
bool apiAuthenticate(AsyncWebServerRequest* request) {
const auto key = apiKey();
if (!apiEnabled() || !key.length()) {
DEBUG_MSG_P(PSTR("[WEBSERVER] HTTP API is not enabled\n"));
request->send(403);
if (!key.length()) {
return false;
}
AsyncWebParameter* keyParam = request->getParam("apikey", (request->method() == HTTP_PUT));
if (!keyParam || !keyParam->value().equals(key)) {
DEBUG_MSG_P(PSTR("[WEBSERVER] Wrong / missing apikey parameter\n"));
request->send(403);
return false;
if (apiAuthenticateHeader(request, key)) {
return true;
}
return true;
if (apiAuthenticateParam(request, key)) {
return true;
}
return false;
}
void apiCommonSetup() {


+ 182
- 0
code/espurna/api_impl.h View File

@ -0,0 +1,182 @@
/*
Part of the API MODULE
Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
*/
#pragma once
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include <algorithm>
#include <memory>
#include <vector>
// -----------------------------------------------------------------------------
struct PathPart {
enum class Type {
Unknown,
Value,
SingleWildcard,
MultiWildcard
};
Type type;
size_t offset;
size_t length;
};
struct PathParts {
using Parts = std::vector<PathPart>;
PathParts() = delete;
PathParts(const PathParts&) = default;
PathParts(PathParts&&) noexcept = default;
explicit PathParts(const String& path);
explicit operator bool() const {
return _ok;
}
void clear() {
_parts.clear();
}
void reserve(size_t size) {
_parts.reserve(size);
}
String operator[](size_t index) const {
auto& part = _parts[index];
return _path.substring(part.offset, part.offset + part.length);
}
const String& path() const {
return _path;
}
const Parts& parts() const {
return _parts;
}
size_t size() const {
return _parts.size();
}
Parts::const_iterator begin() const {
return _parts.begin();
}
Parts::const_iterator end() const {
return _parts.end();
}
bool match(const PathParts& path) const;
bool match(const String& path) const {
return match(PathParts(path));
}
private:
PathPart& emplace_back(PathPart::Type type, size_t offset, size_t length) {
PathPart part { type, offset, length };
_parts.push_back(std::move(part));
return _parts.back();
}
const String& _path;
Parts _parts;
bool _ok { false };
};
// this is a purely temporary object, which we can only create while doing the API dispatch
struct ApiRequest {
ApiRequest() = delete;
ApiRequest(const ApiRequest&) = default;
ApiRequest(ApiRequest&&) noexcept = default;
explicit ApiRequest(AsyncWebServerRequest& request, const PathParts& pattern, const PathParts& parts) :
_request(request),
_pattern(pattern),
_parts(parts)
{}
template <typename T>
void handle(T&& handler) {
_done = true;
handler(&_request);
}
template <typename T>
void param_foreach(T&& handler) {
const size_t params { _request.params() };
for (size_t current = 0; current < params; ++current) {
auto* param = _request.getParam(current);
handler(param->name(), param->value());
}
}
template <typename T>
void param_foreach(const String& name, T&& handler) {
param_foreach([&](const String& param_name, const String& param_value) {
if (param_name == name) {
handler(param_value);
}
});
}
const String& param(const String& name) {
auto* result = _request.getParam(name, HTTP_PUT == _request.method());
if (result) {
return result->value();
}
return _empty_string();
}
void send(const String& payload) {
if (payload.length()) {
_request.send(200, "text/plain", payload);
} else {
_request.send(204);
}
_done = true;
}
bool done() const {
return _done;
}
const PathParts& parts() const {
return _parts;
}
String part(size_t index) const {
return _parts[index];
}
// Only works when pattern cointains '+', retrieving the part at the same index from the real path
// e.g. for the pair of `some/+/path` and `some/data/path`, calling `wildcard(0)` will return `data`
String wildcard(int index) const;
size_t wildcards() const;
private:
const String& _empty_string() const {
static const String string;
return string;
}
bool _done { false };
AsyncWebServerRequest& _request;
const PathParts& _pattern;
const PathParts& _parts;
};

+ 11
- 3
code/espurna/config/general.h View File

@ -190,7 +190,7 @@
#endif
#ifndef TERMINAL_WEB_API_PATH
#define TERMINAL_WEB_API_PATH "/api/cmd"
#define TERMINAL_WEB_API_PATH "cmd"
#endif
//------------------------------------------------------------------------------
@ -734,6 +734,10 @@
#define WEB_EMBEDDED 1 // Build the firmware with the web interface embedded in
#endif
#ifndef WEB_ACCESS_LOG
#define WEB_ACCESS_LOG 0 // Log every request that was received by the server (but, not necessarily processed)
#endif
// Requires ESPAsyncTCP to be built with ASYNC_TCP_SSL_ENABLED=1 and Arduino Core version >= 2.4.0
// XXX: This is not working at the moment!! Pending https://github.com/me-no-dev/ESPAsyncTCP/issues/95
#ifndef WEB_SSL_ENABLED
@ -806,8 +810,12 @@
// Setting this to 0 will allow using GET to change relays, for instance
#endif
#ifndef API_BUFFER_SIZE
#define API_BUFFER_SIZE 64 // Size of the buffer for HTTP GET API responses
#ifndef API_JSON_BUFFER_SIZE
#define API_JSON_BUFFER_SIZE 256 // Size of the (de)serializer buffer.
#endif
#ifndef API_BASE_PATH
#define API_BASE_PATH "/api/"
#endif
#ifndef API_REAL_TIME_VALUES


+ 5
- 0
code/espurna/encoder.cpp View File

@ -10,6 +10,11 @@ Copyright (C) 2018-2019 by Xose Pérez <xose dot perez at gmail dot com>
#if ENCODER_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE)
#include "light.h"
#include "libs/Encoder.h"
#include <vector>
struct encoder_t {
Encoder * encoder;
unsigned char button_pin;


+ 0
- 5
code/espurna/encoder.h View File

@ -8,9 +8,4 @@ Copyright (C) 2018-2019 by Xose Pérez <xose dot perez at gmail dot com>
#include "espurna.h"
#if ENCODER_SUPPORT && (LIGHT_PROVIDER != LIGHT_PROVIDER_NONE)
#include "libs/Encoder.h"
#include <vector>
#endif
void encoderSetup();

+ 137
- 73
code/espurna/light.cpp View File

@ -444,6 +444,12 @@ void _toRGB(char * rgb, size_t len, bool target = false) {
snprintf_P(rgb, len, PSTR("#%06X"), value);
}
String _toRGB(bool target) {
char buffer[64] { 0 };
_toRGB(buffer, sizeof(buffer), target);
return buffer;
}
void _toHSV(char * hsv, size_t len) {
double h {0.}, s {0.}, v {0.};
double r {0.}, g {0.}, b {0.};
@ -486,6 +492,12 @@ void _toHSV(char * hsv, size_t len) {
);
}
String _toHSV() {
char buffer[64] { 0 };
_toHSV(buffer, sizeof(buffer));
return buffer;
}
void _toLong(char * color, size_t len, bool target) {
if (!_light_has_color) return;
@ -502,6 +514,12 @@ void _toLong(char * color, size_t len) {
_toLong(color, len, false);
}
String _toLong(bool target = false) {
char buffer[64] { 0 };
_toLong(buffer, sizeof(buffer), target);
return buffer;
}
String _toCSV(bool target) {
const auto channels = lightChannels();
@ -539,22 +557,38 @@ int _lightAdjustValue(const int& value, const String& operation) {
return updated;
}
void _lightAdjustBrightness(const char *payload) {
void _lightAdjustBrightness(const char* payload) {
lightBrightness(_lightAdjustValue(lightBrightness(), payload));
}
void _lightAdjustChannel(unsigned char id, const char *payload) {
void _lightAdjustBrightness(const String& payload) {
_lightAdjustBrightness(payload.c_str());
}
void _lightAdjustChannel(unsigned char id, const char* payload) {
lightChannel(id, _lightAdjustValue(lightChannel(id), payload));
}
void _lightAdjustKelvin(const char *payload) {
void _lightAdjustChannel(unsigned char id, const String& payload) {
_lightAdjustChannel(id, payload.c_str());
}
void _lightAdjustKelvin(const char* payload) {
_fromKelvin(_lightAdjustValue(_toKelvin(_light_mireds), payload));
}
void _lightAdjustMireds(const char *payload) {
void _lightAdjustKelvin(const String& payload) {
_lightAdjustKelvin(payload.c_str());
}
void _lightAdjustMireds(const char* payload) {
_fromMireds(_lightAdjustValue(_light_mireds, payload));
}
void _lightAdjustMireds(const String& payload) {
_lightAdjustMireds(payload.c_str());
}
// -----------------------------------------------------------------------------
// PROVIDER
// -----------------------------------------------------------------------------
@ -853,101 +887,123 @@ void lightBroker() {
#if API_SUPPORT
void _lightApiSetup() {
bool _lightTryParseChannel(const char* p, unsigned char& id) {
char* endp { nullptr };
const unsigned long result { strtoul(p, &endp, 10) };
if ((endp == p) || (*endp != '\0') || (result >= lightChannels())) {
DEBUG_MSG_P(PSTR("[LIGHT] Invalid channelID (%s)\n"), p);
return false;
}
// Note that we expect a fixed number of entries.
// Otherwise, underlying vector will reserve more than we need (likely, *2 of the current size)
apiReserve(
(_light_has_color ? 4u : 0u) + 2u + _light_channels.size()
);
id = result;
return true;
}
template <typename T>
bool _lightApiTryHandle(ApiRequest& request, T&& callback) {
auto id_param = request.wildcard(0);
unsigned char id;
if (!_lightTryParseChannel(id_param.c_str(), id)) {
return false;
}
return callback(id);
}
void _lightApiSetup() {
if (_light_has_color) {
apiRegister({
MQTT_TOPIC_COLOR_RGB, Api::Type::Basic, ApiUnusedArg,
[](const Api&, ApiBuffer& buffer) {
if (getSetting("useCSS", 1 == LIGHT_USE_CSS)) {
_toRGB(buffer.data, buffer.size, true);
} else {
_toLong(buffer.data, buffer.size, true);
}
apiRegister(F(MQTT_TOPIC_COLOR_RGB),
[](ApiRequest& request) {
auto result = getSetting("useCSS", 1 == LIGHT_USE_CSS)
? _toRGB(true) : _toLong(true);
request.send(result);
return true;
},
[](const Api&, ApiBuffer& buffer) {
lightColor(buffer.data, true);
[](ApiRequest& request) {
lightColor(request.param(F("value")), true);
lightUpdate(true, true);
return true;
}
});
);
apiRegister({
MQTT_TOPIC_COLOR_HSV, Api::Type::Basic, ApiUnusedArg,
[](const Api&, ApiBuffer& buffer) {
_toHSV(buffer.data, buffer.size);
apiRegister(F(MQTT_TOPIC_COLOR_HSV),
[](ApiRequest& request) {
request.send(_toHSV());
return true;
},
[](const Api&, ApiBuffer& buffer) {
lightColor(buffer.data, false);
[](ApiRequest& request) {
lightColor(request.param(F("value")), false);
lightUpdate(true, true);
return true;
}
});
);
apiRegister({
MQTT_TOPIC_MIRED, Api::Type::Basic, ApiUnusedArg,
[](const Api&, ApiBuffer& buffer) {
sprintf(buffer.data, PSTR("%d"), _light_mireds);
apiRegister(F(MQTT_TOPIC_MIRED),
[](ApiRequest& request) {
request.send(String(_light_mireds));
return true;
},
[](const Api&, ApiBuffer& buffer) {
_lightAdjustMireds(buffer.data);
[](ApiRequest& request) {
_lightAdjustMireds(request.param(F("value")));
lightUpdate(true, true);
return true;
}
});
);
apiRegister({
MQTT_TOPIC_KELVIN, Api::Type::Basic, ApiUnusedArg,
[](const Api&, ApiBuffer& buffer) {
sprintf(buffer.data, PSTR("%d"), _toKelvin(_light_mireds));
apiRegister(F(MQTT_TOPIC_KELVIN),
[](ApiRequest& request) {
request.send(String(_toKelvin(_light_mireds)));
return true;
},
[](const Api&, ApiBuffer& buffer) {
_lightAdjustKelvin(buffer.data);
[](ApiRequest& request) {
_lightAdjustKelvin(request.param(F("value")));
lightUpdate(true, true);
return true;
}
});
);
}
apiRegister({
MQTT_TOPIC_TRANSITION, Api::Type::Basic, ApiUnusedArg,
[](const Api&, ApiBuffer& buffer) {
snprintf_P(buffer.data, buffer.size, PSTR("%u"), lightTransitionTime());
apiRegister(F(MQTT_TOPIC_TRANSITION),
[](ApiRequest& request) {
request.send(String(lightTransitionTime()));
return true;
},
[](const Api&, ApiBuffer& buffer) {
lightTransitionTime(atol(buffer.data));
[](ApiRequest& request) {
lightTransitionTime(request.param(F("value")).toInt());
return true;
}
});
);
apiRegister({
MQTT_TOPIC_BRIGHTNESS, Api::Type::Basic, ApiUnusedArg,
[](const Api&, ApiBuffer& buffer) {
snprintf_P(buffer.data, buffer.size, PSTR("%u"), _light_brightness);
apiRegister(F(MQTT_TOPIC_BRIGHTNESS),
[](ApiRequest& request) {
request.send(String(static_cast<int>(_light_brightness)));
return true;
},
[](const Api&, ApiBuffer& buffer) {
_lightAdjustBrightness(buffer.data);
[](ApiRequest& request) {
_lightAdjustBrightness(request.param(F("value")));
lightUpdate(true, true);
return true;
}
});
);
char path[32] = {0};
for (unsigned char id = 0; id < _light_channels.size(); ++id) {
snprintf_P(path, sizeof(path), PSTR(MQTT_TOPIC_CHANNEL "/%u"), id);
apiRegister({
path, Api::Type::Basic, id,
[](const Api& api, ApiBuffer& buffer) {
snprintf_P(buffer.data, buffer.size, PSTR("%u"), _light_channels[api.arg].target);
},
[](const Api& api, ApiBuffer& buffer) {
_lightAdjustChannel(api.arg, buffer.data);
apiRegister(F(MQTT_TOPIC_CHANNEL "/+"),
[](ApiRequest& request) {
return _lightApiTryHandle(request, [&](unsigned char id) {
request.send(String(static_cast<int>(_light_channels[id].target)));
return true;
});
},
[](ApiRequest& request) {
return _lightApiTryHandle(request, [&](unsigned char id) {
_lightAdjustChannel(id, request.param(F("value")));
lightUpdate(true, true);
}
});
}
return true;
});
}
);
}
@ -1006,11 +1062,11 @@ void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject
if (_light_has_color) {
if (strcmp(action, "color") == 0) {
if (data.containsKey("rgb")) {
lightColor(data["rgb"], true);
lightColor(data["rgb"].as<const char*>(), true);
lightUpdate(true, true);
}
if (data.containsKey("hsv")) {
lightColor(data["hsv"], false);
lightColor(data["hsv"].as<const char*>(), false);
lightUpdate(true, true);
}
}
@ -1104,7 +1160,7 @@ void _lightInitCommands() {
terminalRegisterCommand(F("MIRED"), [](const terminal::CommandContext& ctx) {
if (ctx.argc > 1) {
_lightAdjustMireds(ctx.argv[1].c_str());
_lightAdjustMireds(ctx.argv[1]);
lightUpdate(true, true);
}
DEBUG_MSG_P(PSTR("Color: %s\n"), lightColor().c_str());
@ -1225,10 +1281,18 @@ void lightColor(const char * color, bool rgb) {
}
}
void lightColor(const char * color) {
void lightColor(const String& color, bool rgb) {
lightColor(color.c_str(), rgb);
}
void lightColor(const char* color) {
lightColor(color, true);
}
void lightColor(const String& color) {
lightColor(color.c_str());
}
void lightColor(unsigned long color) {
_fromLong(color, false);
}


+ 6
- 2
code/espurna/light.h View File

@ -31,8 +31,12 @@ size_t lightChannels();
unsigned int lightTransitionTime();
void lightTransitionTime(unsigned long ms);
void lightColor(const char * color, bool rgb);
void lightColor(const char * color);
void lightColor(const char* color, bool rgb);
void lightColor(const String& color, bool rgb);
void lightColor(const String& color);
void lightColor(const char* color);
void lightColor(unsigned long color);
String lightColor(bool rgb);
String lightColor();


+ 1
- 1
code/espurna/ota_web.cpp View File

@ -136,7 +136,7 @@ void _onUpgradeFile(AsyncWebServerRequest *request, String filename, size_t inde
}
void otaWebSetup() {
webServer()->on("/upgrade", HTTP_POST, _onUpgrade, _onUpgradeFile);
webServer().on("/upgrade", HTTP_POST, _onUpgrade, _onUpgradeFile);
wsRegister().
onVisible([](JsonObject& root) {
root["otaVisible"] = 1;


+ 27
- 11
code/espurna/prometheus.cpp View File

@ -20,7 +20,7 @@ Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#include <cmath>
void _prometheusRequestHandler(AsyncWebServerRequest* request) {
static_assert(RELAY_SUPPORT || SENSOR_SUPPORT, "");
static_assert((RELAY_SUPPORT) || (SENSOR_SUPPORT), "");
// TODO: Add more stuff?
// Note: Response 'stream' backing buffer is customizable. Default is 1460 bytes (see ESPAsyncWebServer.h)
@ -55,20 +55,36 @@ void _prometheusRequestHandler(AsyncWebServerRequest* request) {
request->send(response);
}
bool _prometheusRequestCallback(AsyncWebServerRequest* request) {
if (request->url().equals(F("/api/metrics"))) {
webLog(request);
if (apiAuthenticate(request)) {
_prometheusRequestHandler(request);
}
return true;
}
return false;
#if API_SUPPORT
void prometheusSetup() {
apiRegister(F("metrics"),
[](ApiRequest& request) {
request.handle(_prometheusRequestHandler);
return true;
},
nullptr
);
}
#else
void prometheusSetup() {
webRequestRegister(_prometheusRequestCallback);
webRequestRegister([](AsyncWebServerRequest* request) {
if (request->url().equals(F(API_BASE_PATH "metrics"))) {
if (apiAuthenticate(request)) {
_prometheusRequestHandler(request);
return true;
}
request->send(403);
return true;
}
return false;
});
}
#endif // API_SUPPORT
#endif // PROMETHEUS_SUPPORT

+ 158
- 114
code/espurna/relay.cpp View File

@ -134,21 +134,85 @@ String _relay_rpc_payload_toggle;
// UTILITY
// -----------------------------------------------------------------------------
bool _relayHandlePayload(unsigned char relayID, const char* payload) {
auto value = relayParsePayload(payload);
if (value == PayloadStatus::Unknown) return false;
bool _relayTryParseId(const char* p, unsigned char& relayID) {
char* endp { nullptr };
const unsigned long result { strtoul(p, &endp, 10) };
if ((endp == p) || (*endp != '\0') || (result >= relayCount())) {
DEBUG_MSG_P(PSTR("[RELAY] Invalid relayID (%s)\n"), p);
return false;
}
relayID = result;
return true;
}
if (value == PayloadStatus::Off) {
bool _relayTryParseIdFromPath(const String& endpoint, unsigned char& relayID) {
int next_slash { endpoint.lastIndexOf('/') };
if (next_slash < 0) {
return false;
}
const char* p { endpoint.c_str() + next_slash + 1 };
if (*p == '\0') {
DEBUG_MSG_P(PSTR("[RELAY] relayID was not specified\n"));
return false;
}
return _relayTryParseId(p, relayID);
}
void _relayHandleStatus(unsigned char relayID, PayloadStatus status) {
switch (status) {
case PayloadStatus::Off:
relayStatus(relayID, false);
} else if (value == PayloadStatus::On) {
break;
case PayloadStatus::On:
relayStatus(relayID, true);
} else if (value == PayloadStatus::Toggle) {
break;
case PayloadStatus::Toggle:
relayToggle(relayID);
break;
case PayloadStatus::Unknown:
break;
}
}
bool _relayHandlePayload(unsigned char relayID, const char* payload) {
auto status = relayParsePayload(payload);
if (status != PayloadStatus::Unknown) {
_relayHandleStatus(relayID, status);
return true;
}
DEBUG_MSG_P(PSTR("[RELAY] Invalid API payload (%s)\n"), payload);
return false;
}
bool _relayHandlePayload(unsigned char relayID, const String& payload) {
return _relayHandlePayload(relayID, payload.c_str());
}
bool _relayHandlePulsePayload(unsigned char id, const char* payload) {
unsigned long pulse = 1000 * atof(payload);
if (!pulse) {
return false;
}
if (RELAY_PULSE_NONE != _relays[id].pulse) {
DEBUG_MSG_P(PSTR("[RELAY] Overriding relayID %u pulse settings\n"), id);
}
_relays[id].pulse_ms = pulse;
_relays[id].pulse = relayStatus(id) ? RELAY_PULSE_ON : RELAY_PULSE_OFF;
relayToggle(id, true, false);
return true;
}
bool _relayHandlePulsePayload(unsigned char id, const String& payload) {
return _relayHandlePulsePayload(id, payload.c_str());
}
PayloadStatus _relayStatusInvert(PayloadStatus status) {
return (status == PayloadStatus::On) ? PayloadStatus::Off : status;
}
@ -600,8 +664,8 @@ bool relayStatus(unsigned char id, bool status, bool report, bool group_report)
}
_relays[id].target_status = status;
if (report) _relays[id].report = true;
if (group_report) _relays[id].group_report = true;
_relays[id].report = report;
_relays[id].group_report = group_report;
relaySync(id);
@ -1009,7 +1073,7 @@ void _relayWebSocketOnAction(uint32_t client_id, const char * action, JsonObject
relayID = data["id"];
}
_relayHandlePayload(relayID, data["status"]);
_relayHandlePayload(relayID, data["status"].as<const char*>());
}
@ -1032,75 +1096,71 @@ void relaySetupWS() {
#if API_SUPPORT
void relaySetupAPI() {
template <typename T>
bool _relayApiTryHandle(ApiRequest& request, T&& callback) {
auto id_param = request.wildcard(0);
unsigned char id;
if (!_relayTryParseId(id_param.c_str(), id)) {
return false;
}
// Note that we expect a fixed number of entries.
// Otherwise, underlying vector will reserve more than we need (likely, *2 of the current size)
apiReserve(2u + (relayCount() * 2u));
return callback(id);
}
void relaySetupAPI() {
apiRegister({
MQTT_TOPIC_RELAY, Api::Type::Json, ApiUnusedArg,
[](const Api&, JsonObject& root) {
apiRegister(F(MQTT_TOPIC_RELAY),
[](ApiRequest&, JsonObject& root) {
JsonArray& relays = root.createNestedArray("relayStatus");
for (unsigned char id = 0; id < relayCount(); ++id) {
relays.add(_relays[id].target_status ? 1 : 0);
}
return true;
},
nullptr
);
apiRegister(F(MQTT_TOPIC_RELAY "/+"),
[](ApiRequest& request) {
return _relayApiTryHandle(request, [&](unsigned char id) {
request.send(String(_relays[id].target_status ? 1 : 0));
return true;
});
},
[](ApiRequest& request) {
return _relayApiTryHandle(request, [&](unsigned char id) {
return _relayHandlePayload(id, request.param(F("value")));
});
}
});
);
apiRegister(F(MQTT_TOPIC_PULSE "/+"),
[](ApiRequest& request) {
return _relayApiTryHandle(request, [&](unsigned char id) {
request.send(String(static_cast<double>(_relays[id].pulse_ms) / 1000));
return true;
});
},
[](ApiRequest& request) {
return _relayApiTryHandle(request, [&](unsigned char id) {
return _relayHandlePulsePayload(id, request.param(F("value")));
});
}
);
#if defined(ITEAD_SONOFF_IFAN02)
apiRegister({
MQTT_TOPIC_SPEED, Api::Type::Basic, ApiUnusedArg,
[](const Api&, ApiBuffer& buffer) {
snprintf(buffer.data, buffer.size, "%u", getSpeed());
},
[](const Api&, ApiBuffer& buffer) {
setSpeed(atoi(buffer.data));
snprintf(buffer.data, buffer.size, "%u", getSpeed());
}
});
#endif
char path[64] = {0};
for (unsigned char id = 0; id < relayCount(); ++id) {
sprintf_P(path, PSTR(MQTT_TOPIC_RELAY "/%u"), id);
apiRegister({
path, Api::Type::Basic, id,
[](const Api& api, ApiBuffer& buffer) {
snprintf_P(buffer.data, buffer.size, PSTR("%d"), _relays[api.arg].target_status ? 1 : 0);
apiRegister(F(MQTT_TOPIC_SPEED), {
[](ApiRequest& request) {
request.send(String(static_cast<int>(getSpeed())));
return true;
},
[](const Api& api, ApiBuffer& buffer) {
if (!_relayHandlePayload(api.arg, buffer.data)) {
DEBUG_MSG_P(PSTR("[RELAY] Invalid API payload (%s)\n"), buffer.data);
return;
}
}
});
sprintf_P(path, PSTR(MQTT_TOPIC_PULSE "/%u"), id);
apiRegister({
path, Api::Type::Basic, id,
[](const Api& api, ApiBuffer& buffer) {
dtostrf((double) _relays[api.arg].pulse_ms / 1000, 1, 3, buffer.data);
[](ApiRequest& request) {
setSpeed(atoi(request.param(F("value"))));
return true;
},
[](const Api& api, ApiBuffer& buffer) {
unsigned long pulse = 1000 * atof(buffer.data);
if (0 == pulse) {
return;
}
if (RELAY_PULSE_NONE != _relays[api.arg].pulse) {
DEBUG_MSG_P(PSTR("[RELAY] Overriding relay #%d pulse settings\n"), api.arg);
}
_relays[api.arg].pulse_ms = pulse;
_relays[api.arg].pulse = relayStatus(api.arg)
? RELAY_PULSE_ON
: RELAY_PULSE_OFF;
relayToggle(api.arg, true, false);
}
nullptr
});
}
#endif
}
@ -1243,58 +1303,31 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
if (type == MQTT_MESSAGE_EVENT) {
String t = mqttMagnitude((char *) topic);
unsigned char id;
if (!_relayTryParseIdFromPath(t.c_str(), id)) {
return;
}
// magnitude is relay/#/pulse
if (t.startsWith(MQTT_TOPIC_PULSE)) {
unsigned int id = t.substring(strlen(MQTT_TOPIC_PULSE)+1).toInt();
if (id >= relayCount()) {
DEBUG_MSG_P(PSTR("[RELAY] Wrong relayID (%d)\n"), id);
return;
}
unsigned long pulse = 1000 * atof(payload);
if (0 == pulse) return;
if (RELAY_PULSE_NONE != _relays[id].pulse) {
DEBUG_MSG_P(PSTR("[RELAY] Overriding relay #%d pulse settings\n"), id);
}
_relays[id].pulse_ms = pulse;
_relays[id].pulse = relayStatus(id) ? RELAY_PULSE_ON : RELAY_PULSE_OFF;
relayToggle(id, true, false);
_relayHandlePulsePayload(id, payload);
_relays[id].report = mqttForward();
return;
}
// magnitude is relay/#
if (t.startsWith(MQTT_TOPIC_RELAY)) {
// Get relay ID
unsigned int id = t.substring(strlen(MQTT_TOPIC_RELAY)+1).toInt();
if (id >= relayCount()) {
DEBUG_MSG_P(PSTR("[RELAY] Wrong relayID (%d)\n"), id);
return;
}
// Get value
auto value = relayParsePayload(payload);
if (value == PayloadStatus::Unknown) return;
relayStatusWrap(id, value, false);
_relayHandlePayload(id, payload);
_relays[id].report = mqttForward();
return;
}
// Check group topics
// TODO: cache group topics instead of reading settings each time?
// TODO: this is another kvs::foreach case, since we slow down MQTT when settings grow
for (unsigned char i=0; i < _relays.size(); i++) {
const String t = getSetting({"mqttGroup", i});
if (!t.length()) break;
if ((t.length() > 0) && t.equals(topic)) {
if (t == topic) {
auto value = relayParsePayload(payload);
if (value == PayloadStatus::Unknown) return;
@ -1306,7 +1339,8 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
}
DEBUG_MSG_P(PSTR("[RELAY] Matched group topic for relayID %d\n"), i);
relayStatusWrap(i, value, true);
_relayHandleStatus(i, value);
_relays[i].group_report = false;
}
}
@ -1320,18 +1354,28 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
}
// TODO: safeguard against network issues. this one has good intentions, but we may end up
// switching relays back and forth when connection is unstable but reconnects very fast after the failure
if (type == MQTT_DISCONNECT_EVENT) {
for (unsigned char i=0; i < _relays.size(); i++){
for (unsigned char i=0; i < _relays.size(); i++) {
const auto reaction = getSetting({"relayOnDisc", i}, 0);
if (1 == reaction) { // switch relay OFF
DEBUG_MSG_P(PSTR("[RELAY] Reset relay (%d) due to MQTT disconnection\n"), i);
relayStatusWrap(i, PayloadStatus::Off, false);
} else if(2 == reaction) { // switch relay ON
DEBUG_MSG_P(PSTR("[RELAY] Set relay (%d) due to MQTT disconnection\n"), i);
relayStatusWrap(i, PayloadStatus::On, false);
bool status;
switch (reaction) {
case 1:
status = false;
break;
case 2:
status = true;
break;
default:
return;
}
}
DEBUG_MSG_P(PSTR("[RELAY] Turn %s relay #%u due to MQTT disconnection\n"), status ? "ON" : "OFF", i);
relayStatus(i, status);
}
}
}


+ 21
- 20
code/espurna/rfbridge.cpp View File

@ -1020,42 +1020,43 @@ void _rfbMqttCallback(unsigned int type, const char * topic, char * payload) {
void _rfbApiSetup() {
apiReserve(3u);
apiRegister({
MQTT_TOPIC_RFOUT, Api::Type::Basic, ApiUnusedArg,
apiRegister(F(MQTT_TOPIC_RFOUT),
apiOk, // just a stub, nothing to return
[](const Api&, ApiBuffer& buffer) {
_rfbSendFromPayload(buffer.data);
[](ApiRequest& request) {
_rfbSendFromPayload(request.param(F("value")).c_str());
return true;
}
});
);
#if RELAY_SUPPORT
apiRegister({
MQTT_TOPIC_RFLEARN, Api::Type::Basic, ApiUnusedArg,
[](const Api&, ApiBuffer& buffer) {
apiRegister(F(MQTT_TOPIC_RFLEARN),
[](ApiRequest& request) {
char buffer[64] { 0 };
if (_rfb_learn) {
snprintf_P(buffer.data, buffer.size, PSTR("learning id:%u,status:%c"),
snprintf_P(buffer, sizeof(buffer), PSTR("learning id:%u,status:%c"),
_rfb_learn->id, _rfb_learn->status ? 't' : 'f'
);
} else {
snprintf_P(buffer.data, buffer.size, PSTR("waiting"));
snprintf_P(buffer, sizeof(buffer), PSTR("waiting"));
}
request.send(buffer);
return true;
},
[](const Api&, ApiBuffer& buffer) {
_rfbLearnStartFromPayload(buffer.data);
[](ApiRequest& request) {
_rfbLearnStartFromPayload(request.param(F("value")).c_str());
return true;
}
});
);
#endif
#if RFB_PROVIDER == RFB_PROVIDER_EFM8BB1
apiRegister({
MQTT_TOPIC_RFRAW, Api::Type::Basic, ApiUnusedArg,
apiRegister(F(MQTT_TOPIC_RFRAW),
apiOk, // just a stub, nothing to return
[](const Api&, ApiBuffer& buffer) {
_rfbSendRawFromPayload(buffer.data);
[](ApiRequest& request) {
_rfbSendRawFromPayload(request.param(F("value")).c_str());
return true;
}
});
);
#endif
}


+ 73
- 27
code/espurna/sensor.cpp View File

@ -449,6 +449,10 @@ void _sensorApiResetEnergy(const sensor_magnitude_t& magnitude, const char* payl
sensor->resetEnergy(magnitude.index_local, energy);
}
void _sensorApiResetEnergy(const sensor_magnitude_t& magnitude, const String& payload) {
_sensorApiResetEnergy(magnitude, payload.c_str());
}
sensor::Energy _sensorEnergyTotal(unsigned char index) {
sensor::Energy result;
@ -1436,44 +1440,86 @@ String _sensorApiMagnitudeName(sensor_magnitude_t& magnitude) {
return name;
}
void _sensorApiJsonCallback(const Api&, JsonObject& root) {
JsonArray& magnitudes = root.createNestedArray("magnitudes");
for (auto& magnitude : _magnitudes) {
JsonArray& data = magnitudes.createNestedArray();
data.add(_sensorApiMagnitudeName(magnitude));
data.add(magnitude.last);
data.add(magnitude.reported);
bool _sensorApiTryParseMagnitudeIndex(const char* p, unsigned char type, unsigned char& magnitude_index) {
char* endp { nullptr };
const unsigned long result { strtoul(p, &endp, 10) };
if ((endp == p) || (*endp != '\0') || (result >= sensor_magnitude_t::counts(type))) {
DEBUG_MSG_P(PSTR("[RELAY] Invalid magnitude ID (%s)\n"), p);
return false;
}
}
void _sensorApiGetValue(const Api& api, ApiBuffer& buffer) {
auto& magnitude = _magnitudes[api.arg];
double value = _sensor_realtime ? magnitude.last : magnitude.reported;
dtostrf(value, 1, magnitude.decimals, buffer.data);
magnitude_index = result;
return true;
}
void _sensorApiResetEnergyPutCallback(const Api& api, ApiBuffer& buffer) {
_sensorApiResetEnergy(_magnitudes[api.arg], buffer.data);
template <typename T>
bool _sensorApiTryHandle(ApiRequest& request, unsigned char type, T&& callback) {
unsigned char index { 0u };
if (request.wildcards()) {
auto index_param = request.wildcard(0);
if (!_sensorApiTryParseMagnitudeIndex(index_param.c_str(), type, index)) {
return false;
}
}
for (auto& magnitude : _magnitudes) {
if ((type == magnitude.type) && (index == magnitude.index_global)) {
callback(magnitude);
return true;
}
}
return false;
}
void _sensorApiSetup() {
apiReserve(
_magnitudes.size() + sensor_magnitude_t::counts(MAGNITUDE_ENERGY) + 1u
apiRegister(F("magnitudes"),
[](ApiRequest&, JsonObject& root) {
JsonArray& magnitudes = root.createNestedArray("magnitudes");
for (auto& magnitude : _magnitudes) {
JsonArray& data = magnitudes.createNestedArray();
data.add(_sensorApiMagnitudeName(magnitude));
data.add(magnitude.last);
data.add(magnitude.reported);
}
return true;
},
nullptr
);
apiRegister({"magnitudes", Api::Type::Json, ApiUnusedArg, _sensorApiJsonCallback});
_magnitudeForEachCounted([](unsigned char type) {
String pattern = magnitudeTopic(type);
if (SENSOR_USE_INDEX || (sensor_magnitude_t::counts(type) > 1)) {
pattern += "/+";
}
for (unsigned char id = 0; id < _magnitudes.size(); ++id) {
apiRegister({
_sensorApiMagnitudeName(_magnitudes[id]).c_str(),
Api::Type::Basic, id,
_sensorApiGetValue,
(_magnitudes[id].type == MAGNITUDE_ENERGY)
? _sensorApiResetEnergyPutCallback
: nullptr
});
}
ApiBasicHandler get {
[type](ApiRequest& request) {
return _sensorApiTryHandle(request, type, [&](const sensor_magnitude_t& magnitude) {
char buffer[64] { 0 };
dtostrf(
_sensor_realtime ? magnitude.last : magnitude.reported,
1, magnitude.decimals,
buffer
);
request.send(String(buffer));
return true;
});
}
};
ApiBasicHandler put { nullptr };
if (type == MAGNITUDE_ENERGY) {
put = [](ApiRequest& request) {
return _sensorApiTryHandle(request, MAGNITUDE_ENERGY, [&](const sensor_magnitude_t& magnitude) {
_sensorApiResetEnergy(magnitude, request.param(F("value")));
});
};
}
apiRegister(pattern, std::move(get), std::move(put));
});
}


+ 1
- 1
code/espurna/ssdp.cpp View File

@ -42,7 +42,7 @@ const char _ssdp_template[] PROGMEM =
void ssdpSetup() {
webServer()->on("/description.xml", HTTP_GET, [](AsyncWebServerRequest *request) {
webServer().on("/description.xml", HTTP_GET, [](AsyncWebServerRequest *request) {
DEBUG_MSG_P(PSTR("[SSDP] Schema request\n"));


+ 55
- 13
code/espurna/terminal.cpp View File

@ -471,24 +471,64 @@ void _terminalLoop() {
}
#if WEB_SUPPORT && TERMINAL_WEB_API_SUPPORT
bool _terminalWebApiMatchPath(AsyncWebServerRequest* request) {
const String api_path = getSetting("termWebApiPath", TERMINAL_WEB_API_PATH);
return request->url().equals(api_path);
}
#if TERMINAL_WEB_API_SUPPORT
void _terminalWebApiSetup() {
#if API_SUPPORT
apiRegister(getSetting("termWebApiPath", TERMINAL_WEB_API_PATH),
[](ApiRequest& api) {
api.handle([](AsyncWebServerRequest* request) {
AsyncResponseStream *response = request->beginResponseStream("text/plain");
for (auto& command : _terminal.commandNames()) {
response->print(command);
response->print("\r\n");
}
request->send(response);
});
return true;
},
[](ApiRequest& api) {
// TODO: since HTTP spec allows query string to contain repeating keys, allow iteration
// over every 'value' available to provide a way to call multiple commands at once
auto cmd = api.param(F("value"));
if (!cmd.length()) {
return false;
}
if (!cmd.endsWith("\r\n") && !cmd.endsWith("\n")) {
cmd += '\n';
}
api.handle([&](AsyncWebServerRequest* request) {
AsyncWebPrint::scheduleFromRequest(request, [cmd](Print& print) {
StreamAdapter<const char*> stream(print, cmd.c_str(), cmd.c_str() + cmd.length() + 1);
terminal::Terminal handler(stream);
handler.processLine();
});
});
return true;
}
);
#else
webRequestRegister([](AsyncWebServerRequest* request) {
// continue to the next handler if path does not match
if (!_terminalWebApiMatchPath(request)) return false;
String path(F(API_BASE_PATH));
path += getSetting("termWebApiPath", TERMINAL_WEB_API_PATH);
if (path != request->url()) {
return false;
}
// return 'true' after this point, since we did handle the request
webLog(request);
if (!apiAuthenticate(request)) return true;
if (!apiAuthenticate(request)) {
request->send(403);
return true;
}
auto* cmd_param = request->getParam("line", (request->method() == HTTP_PUT));
auto* cmd_param = request->getParam("value", (request->method() == HTTP_PUT));
if (!cmd_param) {
request->send(500);
return true;
@ -514,9 +554,11 @@ void _terminalWebApiSetup() {
return true;
});
#endif // API_SUPPORT
}
#endif // WEB_SUPPORT && TERMINAL_WEB_API_SUPPORT
#endif // TERMINAL_WEB_API_SUPPORT
#if MQTT_SUPPORT && TERMINAL_MQTT_SUPPORT


+ 20
- 5
code/espurna/web.cpp View File

@ -514,8 +514,8 @@ bool webAuthenticate(AsyncWebServerRequest *request) {
// -----------------------------------------------------------------------------
AsyncWebServer * webServer() {
return _server;
AsyncWebServer& webServer() {
return *_server;
}
void webBodyRegister(web_body_callback_f callback) {
@ -536,22 +536,37 @@ uint16_t webPort() {
}
void webLog(AsyncWebServerRequest *request) {
DEBUG_MSG_P(PSTR("[WEBSERVER] Request: %s %s\n"), request->methodToString(), request->url().c_str());
DEBUG_MSG_P(PSTR("[WEBSERVER] %s %s\n"), request->methodToString(), request->url().c_str());
}
class WebAccessLogHandler : public AsyncWebHandler {
bool canHandle(AsyncWebServerRequest* request) override {
webLog(request);
return false;
}
};
void webSetup() {
// Cache the Last-Modifier header value
snprintf_P(_last_modified, sizeof(_last_modified), PSTR("%s %s GMT"), __DATE__, __TIME__);
// Create server
// Create server and install global URL debug handler
// (since we don't want to forcibly add it to each instance)
unsigned int port = webPort();
_server = new AsyncWebServer(port);
#if DEBUG_SUPPORT
if (getSetting("webAccessLog", (1 == WEB_ACCESS_LOG))) {
static WebAccessLogHandler log;
_server->addHandler(&log);
}
#endif
// Rewrites
_server->rewrite("/", "/index.html");
// Serve home (basic authentication protection)
// Serve home (basic authentication protection is done manually b/c the handler is installed through callback functions)
#if WEB_EMBEDDED
_server->on("/index.html", HTTP_GET, _onHome);
#endif


+ 1
- 1
code/espurna/web.h View File

@ -80,7 +80,7 @@ struct AsyncWebPrint : public Print {
using web_body_callback_f = std::function<bool(AsyncWebServerRequest*, uint8_t* data, size_t len, size_t index, size_t total)>;
using web_request_callback_f = std::function<bool(AsyncWebServerRequest*)>;
AsyncWebServer* webServer();
AsyncWebServer& webServer();
bool webAuthenticate(AsyncWebServerRequest *request);
void webLog(AsyncWebServerRequest *request);


+ 2
- 2
code/espurna/ws.cpp View File

@ -714,7 +714,7 @@ void wsSend_P(uint32_t client_id, PGM_P payload) {
void wsSetup() {
_ws.onEvent(_wsEvent);
webServer()->addHandler(&_ws);
webServer().addHandler(&_ws);
// CORS
const String webDomain = getSetting("webDomain", WEB_REMOTE_DOMAIN);
@ -723,7 +723,7 @@ void wsSetup() {
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
}
webServer()->on("/auth", HTTP_GET, _onAuth);
webServer().on("/auth", HTTP_GET, _onAuth);
wsRegister()
.onConnected(_wsOnConnected)


Loading…
Cancel
Save