feat: Adopt the OpenAPI 3.0 standard for the REST API #50

This commit is contained in:
proddy
2021-05-02 22:08:53 +02:00
parent 101978f713
commit 5339e0876e
17 changed files with 460 additions and 120 deletions

View File

@@ -21,5 +21,6 @@
- Re-enabled Shower Alert (still experimental) - Re-enabled Shower Alert (still experimental)
- lowercased Flow temp in commands - lowercased Flow temp in commands
- system console commands to main - system console commands to main
- new secure API ([#50](https://github.com/emsesp/EMS-ESP32/issues/50))
## Removed ## Removed

View File

@@ -15,8 +15,8 @@ OpenAPI is an open standard specification for describing REST APIs. From the [Op
- Unless explicitly bypassed in the WebUI some operations required admin privileges in the form of an Access Token which can be generated from the Web UI's Security tab. An Access Token is a string 152 characters long. Token's do not expire. The token needs to be either embedded into the HTTP Header as `"Authorization: Bearer {ACCESS_TOKEN}"` or as query parameter `?access_token={ACCESS_TOKEN}`. To test you can use a command line instruction like - Unless explicitly bypassed in the WebUI some operations required admin privileges in the form of an Access Token which can be generated from the Web UI's Security tab. An Access Token is a string 152 characters long. Token's do not expire. The token needs to be either embedded into the HTTP Header as `"Authorization: Bearer {ACCESS_TOKEN}"` or as query parameter `?access_token={ACCESS_TOKEN}`. To test you can use a command line instruction like
```bash ```bash
curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -X POST http://ems-esp/api/system/settings curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -X GET http://ems-esp/api/system/settings
curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -d '{ "name": "wwtemp", "value":60}' http://ems-esp/api/boiler curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{ "name": "wwtemp", "value":60}' http://ems-esp/api/boiler
``` ```
## Error handling ## Error handling
@@ -36,26 +36,11 @@ OpenAPI is an open standard specification for describing REST APIs. From the [Op
{"message":"Problems parsing JSON"} {"message":"Problems parsing JSON"}
``` ```
- Sending the wrong type of JSON values will result in a `400 Bad Request` response. - Sending invalid fields will result in a `422 Unprocessable Entity` response.
```html
HTTP/1.1 400 Bad Request
{"message":"Body should be a JSON object"}
```
- Sending invalid fields will result in a `422 Unprocessable Entity` response. `code` can be missing, missing_field, invalid or unprocessable. For example when selecting an invalid heating circuit number.
```html ```html
HTTP/1.1 422 Unprocessable Entity HTTP/1.1 422 Unprocessable Entity
{ {"message":"Invalid command"}
"message": "Validation Failed",
"errors": [
{
"field": "title",
"code": "missing_field"
}
]
}
``` ```
## Endpoints ## Endpoints
@@ -80,27 +65,16 @@ OpenAPI is an open standard specification for describing REST APIs. From the [Op
| - | - | - | - | - | | - | - | - | - | - |
| GET | `/{device}` | return all device details and values | | | | GET | `/{device}` | return all device details and values | | |
| GET | `/{device}/{name}` | return a specific parameter and all its properties (name, fullname, value, type, min, max, unit, writeable) | | | | GET | `/{device}/{name}` | return a specific parameter and all its properties (name, fullname, value, type, min, max, unit, writeable) | | |
| GET | `/device={device}?cmd={name}?data={value}[?id={hc}` | to keep compatibility with v2. Unless bypassed in the EMS-ESP settings make sure you include `access_token={ACCESS_TOKEN}` | x | | GET | `/device={device}?cmd={name}?data={value}[?hc=<number>` | Using HTTP query parameters. This is to keep compatibility with v2. Unless bypassed in the EMS-ESP settings make sure you include `access_token={ACCESS_TOKEN}` | x |
| POST/PUT | `/{device}[/{hc}][/{name}]` | sets a specific value to a parameter name. If no hc is selected and one is required for the device, the default will be used | x | `{ "value": <value> [, "hc": {hc}] }` | | POST/PUT | `/{device}[/{hc}][/{name}]` | sets a specific value to a parameter name. If no hc is selected and one is required for the device, the default will be used | x | `{ ["name" : <string>] , ["hc": <number>], "value": <value> }` |
## System Endpoints ## System Endpoints
| Method | Endpoint | Description | Access Token required | JSON body data | | Method | Endpoint | Description | Access Token required | JSON body data |
| - | - | - | - | - | | - | - | - | - | - |
| GET | `/system/info` | list system information | | | | | GET | `/system/info` | list system information | | | |
| GET | `/system/settings` | list all settings, except passwords | x | | GET | `/system/settings` | list all settings, except passwords | |
| POST/PUT | `/system/pin` | switch a GPIO state to HIGH or LOW | x | `{ "pin":<integer>, "value":<boolean> }` | | POST/PUT | `/system/pin` | switch a GPIO state to HIGH or LOW | x | `{ "id":<gpio>, "value":<boolean> }` |
| POST/PUT | `/system/send` | send a telegram to the EMS bus | x | `{ "telegram" : <string> }` | | POST/PUT | `/system/send` | send a telegram to the EMS bus | x | `{ "value" : <string> }` |
| POST/PUT | `/system/publish` | force an MQTT publish | x | `[{ "name" : <device> \| "ha" }]` | | POST/PUT | `/system/publish` | force an MQTT publish | x | `{ "value" : <device> \| "ha" }` |
| POST/PUT | `/system/fetch` | fetch all EMS data from all devices | x | | | POST/PUT | `/system/fetch` | fetch all EMS data from all devices | x | `{ "value" : <device> \| "all" }` |
| POST/PUT | `/system/restart` | restarts the EMS-ESP | x | |
## To Do
- add restart command
- update EMS-ESP wiki/documentation for v3
- make adjustments to the command line
- change the URLs in the web UI help page to call system commands directly instead of via URLs
- add long name to value query (only shortname is shown)
- create Postman schema
- rename setting "Enable API write commands" to something like "Use non-authenticated API" with a disclaimer that its insecure and not recommended

View File

@@ -396,12 +396,12 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
<BlockFormControlLabel <BlockFormControlLabel
control={ control={
<Checkbox <Checkbox
checked={data.api_enabled} checked={data.notoken_api}
onChange={handleValueChange("api_enabled")} onChange={handleValueChange("notoken_api")}
value="api_enabled" value="notoken_api"
/> />
} }
label="Enable API write commands" label="Bypass Access Token authorization on API calls"
/> />
<BlockFormControlLabel <BlockFormControlLabel
control={ control={

View File

@@ -16,7 +16,7 @@ export interface EMSESPSettings {
dallas_parasite: boolean; dallas_parasite: boolean;
led_gpio: number; led_gpio: number;
hide_led: boolean; hide_led: boolean;
api_enabled: boolean; notoken_api: boolean;
analog_enabled: boolean; analog_enabled: boolean;
pbutton_gpio: number; pbutton_gpio: number;
trace_raw: boolean; trace_raw: boolean;

View File

@@ -40,6 +40,53 @@ class ChunkPrint : public Print {
} }
}; };
class PrettyAsyncJsonResponse {
protected:
DynamicJsonDocument _jsonBuffer;
JsonVariant _root;
bool _isValid;
public:
PrettyAsyncJsonResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE)
: _jsonBuffer(maxJsonBufferSize)
, _isValid{false} {
if (isArray)
_root = _jsonBuffer.createNestedArray();
else
_root = _jsonBuffer.createNestedObject();
}
~PrettyAsyncJsonResponse() {
}
JsonVariant & getRoot() {
return _root;
}
bool _sourceValid() const {
return _isValid;
}
size_t setLength() {
return 0;
}
void setContentType(const char * s) {
}
size_t getSize() {
return _jsonBuffer.size();
}
size_t _fillBuffer(uint8_t * data, size_t len) {
return len;
}
void setCode(uint16_t) {
}
};
class AsyncJsonResponse { class AsyncJsonResponse {
protected: protected:
DynamicJsonDocument _jsonBuffer; DynamicJsonDocument _jsonBuffer;
@@ -56,14 +103,18 @@ class AsyncJsonResponse {
else else
_root = _jsonBuffer.createNestedObject(); _root = _jsonBuffer.createNestedObject();
} }
~AsyncJsonResponse() { ~AsyncJsonResponse() {
} }
JsonVariant & getRoot() { JsonVariant & getRoot() {
return _root; return _root;
} }
bool _sourceValid() const { bool _sourceValid() const {
return _isValid; return _isValid;
} }
size_t setLength() { size_t setLength() {
return 0; return 0;
} }
@@ -75,6 +126,9 @@ class AsyncJsonResponse {
size_t _fillBuffer(uint8_t * data, size_t len) { size_t _fillBuffer(uint8_t * data, size_t len) {
return len; return len;
} }
void setCode(uint16_t) {
}
}; };
typedef std::function<void(AsyncWebServerRequest * request, JsonVariant & json)> ArJsonRequestHandlerFunction; typedef std::function<void(AsyncWebServerRequest * request, JsonVariant & json)> ArJsonRequestHandlerFunction;

View File

@@ -25,7 +25,7 @@ class DummySettings {
bool shower_timer = true; bool shower_timer = true;
bool shower_alert = false; bool shower_alert = false;
bool hide_led = false; bool hide_led = false;
bool api_enabled = true; bool notoken_api = false;
// MQTT // MQTT
uint16_t publish_time = 10; // seconds uint16_t publish_time = 10; // seconds

View File

@@ -11,6 +11,7 @@ class AsyncWebServer;
class AsyncWebServerRequest; class AsyncWebServerRequest;
class AsyncWebServerResponse; class AsyncWebServerResponse;
class AsyncJsonResponse; class AsyncJsonResponse;
class PrettyAsyncJsonResponse;
class AsyncWebParameter { class AsyncWebParameter {
private: private:
@@ -68,11 +69,13 @@ class AsyncWebServerRequest {
AsyncWebServer * _server; AsyncWebServer * _server;
WebRequestMethodComposite _method; WebRequestMethodComposite _method;
String _url;
public: public:
void * _tempObject; void * _tempObject;
AsyncWebServerRequest(AsyncWebServer *, AsyncClient *); AsyncWebServerRequest(AsyncWebServer *, AsyncClient *){};
~AsyncWebServerRequest(); ~AsyncWebServerRequest(){};
AsyncClient * client() { AsyncClient * client() {
return _client; return _client;
@@ -82,13 +85,30 @@ class AsyncWebServerRequest {
return _method; return _method;
} }
void method(WebRequestMethodComposite method_s) {
_method = method_s;
}
void addInterestingHeader(const String & name){}; void addInterestingHeader(const String & name){};
size_t args() const {
return 0;
}
void send(AsyncWebServerResponse * response){}; void send(AsyncWebServerResponse * response){};
void send(AsyncJsonResponse * response){}; void send(AsyncJsonResponse * response){};
void send(PrettyAsyncJsonResponse * response){};
void send(int code, const String & contentType = String(), const String & content = String()){}; void send(int code, const String & contentType = String(), const String & content = String()){};
void send(int code, const String & contentType, const __FlashStringHelper *){}; void send(int code, const String & contentType, const __FlashStringHelper *){};
const String & url() const {
return _url;
}
void url(const String & url_s) {
_url = url_s;
}
bool hasParam(const String & name, bool post, bool file) const { bool hasParam(const String & name, bool post, bool file) const {
return false; return false;
} }

View File

@@ -117,7 +117,7 @@ const emsesp_settings = {
"tx_mode": 1, "tx_delay": 0, "ems_bus_id": 11, "syslog_enabled": false, "syslog_level": 3, "tx_mode": 1, "tx_delay": 0, "ems_bus_id": 11, "syslog_enabled": false, "syslog_level": 3,
"trace_raw": false, "syslog_mark_interval": 0, "syslog_host": "192.168.1.4", "syslog_port": 514, "trace_raw": false, "syslog_mark_interval": 0, "syslog_host": "192.168.1.4", "syslog_port": 514,
"master_thermostat": 0, "shower_timer": true, "shower_alert": false, "rx_gpio": 23, "tx_gpio": 5, "master_thermostat": 0, "shower_timer": true, "shower_alert": false, "rx_gpio": 23, "tx_gpio": 5,
"dallas_gpio": 3, "dallas_parasite": false, "led_gpio": 2, "hide_led": false, "api_enabled": true, "dallas_gpio": 3, "dallas_parasite": false, "led_gpio": 2, "hide_led": false, "notoken_api": false,
"analog_enabled": false, "pbutton_gpio": 0, "board_profile": "S32" "analog_enabled": false, "pbutton_gpio": 0, "board_profile": "S32"
}; };
const emsesp_alldevices = { const emsesp_alldevices = {

View File

@@ -16,76 +16,308 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
// SUrlParser from https://github.com/Mad-ness/simple-url-parser
#include "emsesp.h" #include "emsesp.h"
using namespace std::placeholders; // for `_1` etc using namespace std::placeholders; // for `_1` etc
namespace emsesp { namespace emsesp {
WebAPIService::WebAPIService(AsyncWebServer * server) { WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * securityManager)
server->on(EMSESP_API_SERVICE_PATH, HTTP_GET, std::bind(&WebAPIService::webAPIService, this, _1)); : _securityManager(securityManager)
, _apiHandler("/api", std::bind(&WebAPIService::webAPIService_post, this, _1, _2), 256) { // for POSTS
server->on("/api", HTTP_GET, std::bind(&WebAPIService::webAPIService_get, this, _1)); // for GETS
server->addHandler(&_apiHandler);
} }
// e.g. http://ems-esp/api?device=boiler&cmd=wwtemp&data=20&id=1 // GET /{device}
void WebAPIService::webAPIService(AsyncWebServerRequest * request) { // GET /{device}/{name}
// must have device and cmd parameters // GET /device={device}?cmd={name}?data={value}[?id={hc}
if ((!request->hasParam(F_(device))) || (!request->hasParam(F_(cmd)))) { void WebAPIService::webAPIService_get(AsyncWebServerRequest * request) {
request->send(400, "text/plain", F("Invalid syntax")); std::string device("");
std::string cmd("");
int id = -1;
std::string value("");
parse(request, device, cmd, id, value); // pass it defaults
}
// For POSTS with an optional JSON body
// HTTP_POST | HTTP_PUT | HTTP_PATCH
// POST/PUT /{device}[/{hc}][/{name}]
void WebAPIService::webAPIService_post(AsyncWebServerRequest * request, JsonVariant & json) {
// extra the params from the json body
if (not json.is<JsonObject>()) {
webAPIService_get(request);
return; return;
} }
// get device // extract values from the json
String device = request->getParam(F_(device))->value(); // these will be used as default values
uint8_t device_type = EMSdevice::device_name_2_device_type(device.c_str()); auto && body = json.as<JsonObject>();
if (device_type == emsesp::EMSdevice::DeviceType::UNKNOWN) {
request->send(400, "text/plain", F("Invalid device")); std::string device = body["name"].as<std::string>(); // note this was called device in the v2
return; std::string cmd = body["cmd"].as<std::string>();
int id = -1;
if (body.containsKey("id")) {
id = body["id"];
} else if (body.containsKey("hc")) {
id = body["hc"];
} else {
id = -1;
} }
// get cmd, we know we have one // make sure we have a value. There must always be a value
String cmd = request->getParam(F_(cmd))->value(); if (!body.containsKey("value")) {
send_message_response(request, 400, "Problems parsing JSON"); // Bad Request
return;
}
std::string value = body["value"].as<std::string>(); // always convert value to string
String data; // now parse the URL. The URL is always leading and will overwrite anything provided in the json body
parse(request, device, cmd, id, value); // pass it defaults
}
// parse the URL looking for query or path parameters
// reporting back any errors
void WebAPIService::parse(AsyncWebServerRequest * request, std::string & device_s, std::string & cmd_s, int id, std::string & value_s) {
#ifndef EMSESP_STANDALONE
// parse URL for the path names
SUrlParser p;
p.parse(request->url().c_str());
// remove the /api from the path
if (p.paths().front() == "api") {
p.paths().erase(p.paths().begin());
} else {
return; // bad URL
}
uint8_t device_type;
int8_t id_n = -1; // default hc
// check for query parameters first
// /device={device}?cmd={name}?data={value}[?id={hc}
if (p.paths().size() == 0) {
// get the device
if (request->hasParam(F_(device))) {
device_s = request->getParam(F_(device))->value().c_str();
}
// get cmd
if (request->hasParam(F_(cmd))) {
cmd_s = request->getParam(F_(cmd))->value().c_str();
}
// get data, which is optional. This is now replaced with the name 'value' in JSON body
if (request->hasParam(F_(data))) { if (request->hasParam(F_(data))) {
data = request->getParam(F_(data))->value(); value_s = request->getParam(F_(data))->value().c_str();
}
if (request->hasParam("value")) {
value_s = request->getParam("value")->value().c_str();
} }
String id; // get id (or hc), which is optional
if (request->hasParam(F_(id))) { if (request->hasParam(F_(id))) {
id = request->getParam(F_(id))->value(); id_n = Helpers::atoint(request->getParam(F_(id))->value().c_str());
}
if (request->hasParam("hc")) {
id_n = Helpers::atoint(request->getParam("hc")->value().c_str());
}
} else {
// parse paths and json data
// /{device}[/{hc}][/{name}]
// first param must be a valid device, which includes "system"
device_s = p.paths().front();
device_type = EMSdevice::device_name_2_device_type(device_s.c_str());
// if there are no more paths parameters, default to 'info'
auto num_paths = p.paths().size();
if (num_paths > 1) {
auto path2 = p.paths()[1]; // get next path
// if it's a system, the next path must be a command (info, settings,...)
if (device_type == EMSdevice::DeviceType::SYSTEM) {
cmd_s = path2;
} else {
// it's an EMS device
// path2 could be a hc which is optional or a name. first check if it's a hc
if (path2.substr(0, 2) == "hc") {
id_n = (byte)path2[2] - '0'; // bit of a hack
// there must be a name following
if (num_paths > 2) {
cmd_s = p.paths()[2];
}
} else {
cmd_s = path2;
}
}
}
} }
if (id.isEmpty()) { // now go and validate everything
id = "-1";
// device check
if (device_s.empty()) {
send_message_response(request, 422, "Missing device"); // Unprocessable Entity
return;
}
device_type = EMSdevice::device_name_2_device_type(device_s.c_str());
if (device_type == EMSdevice::DeviceType::UNKNOWN) {
send_message_response(request, 422, "Invalid device"); // Unprocessable Entity
return;
} }
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN); // cmd check
JsonObject json = doc.to<JsonObject>(); // if the cmd is empty, default it 'info'
bool ok = false; if (cmd_s.empty()) {
cmd_s = "info";
}
if (Command::find_command(device_type, cmd_s.c_str()) == nullptr) {
send_message_response(request, 422, "Invalid cmd"); // Unprocessable Entity
return;
}
// execute the command // check that we have permissions first. We require authenticating on 1 or more of these conditions:
if (data.isEmpty()) { // 1. any HTTP POSTs or PUTs
ok = Command::call(device_type, cmd.c_str(), nullptr, id.toInt(), json); // command only // 2. a HTTP GET which has a 'data' parameter which is not empty (to keep v2 compatibility)
} else { auto method = request->method();
// we only allow commands with parameters if the API is enabled bool have_data = !value_s.empty();
bool api_enabled; bool admin_allowed;
EMSESP::webSettingsService.read([&](WebSettings & settings) { api_enabled = settings.api_enabled; }); EMSESP::webSettingsService.read([&](WebSettings & settings) {
if (api_enabled) { Authentication authentication = _securityManager->authenticateRequest(request);
ok = Command::call(device_type, cmd.c_str(), data.c_str(), id.toInt(), json); // has cmd, data and id admin_allowed = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication);
} else { });
request->send(401, "text/plain", F("Unauthorized"));
if ((method != HTTP_GET) || ((method == HTTP_GET) && have_data)) {
if (!admin_allowed) {
send_message_response(request, 401, "Bad credentials"); // Unauthorized
return; return;
} }
} }
if (ok && json.size()) {
// send json output back to web // now we have all the parameters go and execute the command
doc.shrinkToFit(); PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XLARGE_DYN);
std::string buffer; JsonObject json = response->getRoot();
serializeJsonPretty(doc, buffer);
request->send(200, "text/plain;charset=utf-8", buffer.c_str()); // EMSESP::logger().notice("Calling device=%s, cmd=%s, data=%s, id/hc=%d", device_s.c_str(), cmd_s.c_str(), value_s.c_str(), id_n);
bool ok = Command::call(device_type, cmd_s.c_str(), (have_data ? value_s.c_str() : nullptr), id_n, json);
// check for errors
if (!ok) {
send_message_response(request, 400, "Problems parsing elements"); // Bad Request
return; return;
} }
request->send(200, "text/plain", ok ? F("OK") : F("Invalid"));
if (!json.size()) {
send_message_response(request, 200, "OK"); // OK
return;
}
// send the json that came back from the command call
response->setLength();
request->send(response); // send json response
#endif
}
// send a HTTP error back, with optional JSON body data
void WebAPIService::send_message_response(AsyncWebServerRequest * request, uint16_t error_code, const char * error_message) {
if (error_message == nullptr) {
AsyncWebServerResponse * response = request->beginResponse(error_code); // just send the code
request->send(response);
} else {
// build a return message and send it
PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_SMALL);
JsonObject json = response->getRoot();
json["message"] = error_message;
response->setCode(error_code);
response->setLength();
response->setContentType("application/json");
request->send(response);
}
}
/**
* Extract only the path component from the passed URI
* and normalized it.
* Ex. //one/two////three///
* becomes
* /one/two/three
*/
std::string SUrlParser::path() {
std::string s = "/"; // set up the beginning slash
for (std::string & f : m_folders) {
s += f;
s += "/";
}
s.pop_back(); // deleting last letter, that is slash '/'
return std::string(s);
}
SUrlParser::SUrlParser(const char * uri) {
parse(uri);
}
bool SUrlParser::parse(const char * uri) {
m_folders.clear();
m_keysvalues.clear();
enum Type { begin, folder, param, value };
std::string s;
const char * c = uri;
enum Type t = Type::begin;
std::string last_param;
if (c != NULL || *c != '\0') {
do {
if (*c == '/') {
if (s.length() > 0) {
m_folders.push_back(s);
s.clear();
}
t = Type::folder;
} else if (*c == '?' && (t == Type::folder || t == Type::begin)) {
if (s.length() > 0) {
m_folders.push_back(s);
s.clear();
}
t = Type::param;
} else if (*c == '=' && (t == Type::param || t == Type::begin)) {
m_keysvalues[s] = "";
last_param = s;
s.clear();
t = Type::value;
} else if (*c == '&' && (t == Type::value || t == Type::param || t == Type::begin)) {
if (t == Type::value) {
m_keysvalues[last_param] = s;
} else if ((t == Type::param || t == Type::begin) && (s.length() > 0)) {
m_keysvalues[s] = "";
last_param = s;
}
t = Type::param;
s.clear();
} else if (*c == '\0' && s.length() > 0) {
if (t == Type::value) {
m_keysvalues[last_param] = s;
} else if (t == Type::folder || t == Type::begin) {
m_folders.push_back(s);
} else if (t == Type::param) {
m_keysvalues[s] = "";
last_param = s;
}
s.clear();
} else if (*c == '\0' && s.length() == 0) {
if (t == Type::param && last_param.length() > 0) {
m_keysvalues[last_param] = "";
}
s.clear();
} else {
s += *c;
}
} while (*c++ != '\0');
}
return true;
} }
} // namespace emsesp } // namespace emsesp

View File

@@ -23,16 +23,52 @@
#include <AsyncJson.h> #include <AsyncJson.h>
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <string>
#include <unordered_map>
#include <vector>
#define EMSESP_API_SERVICE_PATH "/api" #define EMSESP_API_SERVICE_PATH "/api"
namespace emsesp { namespace emsesp {
typedef std::unordered_map<std::string, std::string> KeyValueMap_t;
typedef std::vector<std::string> Folder_t;
class SUrlParser {
private:
KeyValueMap_t m_keysvalues;
Folder_t m_folders;
public:
SUrlParser(){};
SUrlParser(const char * url);
bool parse(const char * url);
Folder_t & paths() {
return m_folders;
};
KeyValueMap_t & params() {
return m_keysvalues;
};
std::string path();
};
class WebAPIService { class WebAPIService {
public: public:
WebAPIService(AsyncWebServer * server); WebAPIService(AsyncWebServer * server, SecurityManager * securityManager);
private: private:
void webAPIService(AsyncWebServerRequest * request); SecurityManager * _securityManager;
AsyncCallbackJsonWebHandler _apiHandler; // for POSTs
void webAPIService_post(AsyncWebServerRequest * request, JsonVariant & json); // for POSTs
void webAPIService_get(AsyncWebServerRequest * request); // for GETs
void parse(AsyncWebServerRequest * request, std::string & device, std::string & cmd, int id, std::string & value);
void send_message_response(AsyncWebServerRequest * request, uint16_t error_code, const char * error_message = nullptr);
}; };
} // namespace emsesp } // namespace emsesp

View File

@@ -110,7 +110,7 @@ void WebDevicesService::device_data(AsyncWebServerRequest * request, JsonVariant
void WebDevicesService::write_value(AsyncWebServerRequest * request, JsonVariant & json) { void WebDevicesService::write_value(AsyncWebServerRequest * request, JsonVariant & json) {
// only issue commands if the API is enabled // only issue commands if the API is enabled
EMSESP::webSettingsService.read([&](WebSettings & settings) { EMSESP::webSettingsService.read([&](WebSettings & settings) {
if (!settings.api_enabled) { if (!settings.notoken_api) {
request->send(403); // forbidden error request->send(403); // forbidden error
return; return;
} }

View File

@@ -55,7 +55,7 @@ void WebSettings::read(WebSettings & settings, JsonObject & root) {
root["dallas_parasite"] = settings.dallas_parasite; root["dallas_parasite"] = settings.dallas_parasite;
root["led_gpio"] = settings.led_gpio; root["led_gpio"] = settings.led_gpio;
root["hide_led"] = settings.hide_led; root["hide_led"] = settings.hide_led;
root["api_enabled"] = settings.api_enabled; root["notoken_api"] = settings.notoken_api;
root["analog_enabled"] = settings.analog_enabled; root["analog_enabled"] = settings.analog_enabled;
root["pbutton_gpio"] = settings.pbutton_gpio; root["pbutton_gpio"] = settings.pbutton_gpio;
root["board_profile"] = settings.board_profile; root["board_profile"] = settings.board_profile;
@@ -169,7 +169,7 @@ StateUpdateResult WebSettings::update(JsonObject & root, WebSettings & settings)
settings.master_thermostat = root["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT; settings.master_thermostat = root["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT;
// doesn't need any follow-up actions // doesn't need any follow-up actions
settings.api_enabled = root["api_enabled"] | EMSESP_DEFAULT_API_ENABLED; settings.notoken_api = root["notoken_api"] | EMSESP_DEFAULT_NOTOKEN_API;
return StateUpdateResult::CHANGED; return StateUpdateResult::CHANGED;
} }

View File

@@ -50,7 +50,7 @@ class WebSettings {
bool dallas_parasite; bool dallas_parasite;
uint8_t led_gpio; uint8_t led_gpio;
bool hide_led; bool hide_led;
bool api_enabled; bool notoken_api;
bool analog_enabled; bool analog_enabled;
uint8_t pbutton_gpio; uint8_t pbutton_gpio;
String board_profile; String board_profile;

View File

@@ -76,8 +76,8 @@
#define EMSESP_DEFAULT_DALLAS_PARASITE false #define EMSESP_DEFAULT_DALLAS_PARASITE false
#endif #endif
#ifndef EMSESP_DEFAULT_API_ENABLED #ifndef EMSESP_DEFAULT_NOTOKEN_API
#define EMSESP_DEFAULT_API_ENABLED false // turn off, because its insecure #define EMSESP_DEFAULT_NOTOKEN_API false
#endif #endif
#ifndef EMSESP_DEFAULT_BOOL_FORMAT #ifndef EMSESP_DEFAULT_BOOL_FORMAT

View File

@@ -39,7 +39,7 @@ WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, &
WebStatusService EMSESP::webStatusService = WebStatusService(&webServer, EMSESP::esp8266React.getSecurityManager()); WebStatusService EMSESP::webStatusService = WebStatusService(&webServer, EMSESP::esp8266React.getSecurityManager());
WebDevicesService EMSESP::webDevicesService = WebDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager()); WebDevicesService EMSESP::webDevicesService = WebDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager());
WebAPIService EMSESP::webAPIService = WebAPIService(&webServer); WebAPIService EMSESP::webAPIService = WebAPIService(&webServer, EMSESP::esp8266React.getSecurityManager());
using DeviceFlags = EMSdevice; using DeviceFlags = EMSdevice;
using DeviceType = EMSdevice::DeviceType; using DeviceType = EMSdevice::DeviceType;
@@ -90,6 +90,15 @@ void EMSESP::fetch_device_values(const uint8_t device_id) {
} }
} }
// for a specific EMS device type go and request data values
void EMSESP::fetch_device_values_type(const uint8_t device_type) {
for (const auto & emsdevice : emsdevices) {
if ((emsdevice) && (emsdevice->device_type() == device_type)) {
emsdevice->fetch_values();
}
}
}
// clears list of recognized devices // clears list of recognized devices
void EMSESP::clear_all_devices() { void EMSESP::clear_all_devices() {
// temporary removed: clearing the list causes a crash, the associated commands and mqtt should also be removed. // temporary removed: clearing the list causes a crash, the associated commands and mqtt should also be removed.
@@ -744,7 +753,8 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
} }
read_next_ = false; read_next_ = false;
} else if (watch() == WATCH_ON) { } else if (watch() == WATCH_ON) {
if ((watch_id_ == WATCH_ID_NONE) || (telegram->type_id == watch_id_) || ((watch_id_ < 0x80) && ((telegram->src == watch_id_) || (telegram->dest == watch_id_)))) { if ((watch_id_ == WATCH_ID_NONE) || (telegram->type_id == watch_id_)
|| ((watch_id_ < 0x80) && ((telegram->src == watch_id_) || (telegram->dest == watch_id_)))) {
LOG_NOTICE(pretty_telegram(telegram).c_str()); LOG_NOTICE(pretty_telegram(telegram).c_str());
} else if (!trace_raw_) { } else if (!trace_raw_) {
LOG_TRACE(pretty_telegram(telegram).c_str()); LOG_TRACE(pretty_telegram(telegram).c_str());
@@ -785,7 +795,8 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
found = emsdevice->handle_telegram(telegram); found = emsdevice->handle_telegram(telegram);
// if we correctly processes the telegram follow up with sending it via MQTT if needed // if we correctly processes the telegram follow up with sending it via MQTT if needed
if (found && Mqtt::connected()) { if (found && Mqtt::connected()) {
if ((mqtt_.get_publish_onchange(emsdevice->device_type()) && emsdevice->has_update()) || (telegram->type_id == publish_id_ && telegram->dest == txservice_.ems_bus_id())) { if ((mqtt_.get_publish_onchange(emsdevice->device_type()) && emsdevice->has_update())
|| (telegram->type_id == publish_id_ && telegram->dest == txservice_.ems_bus_id())) {
if (telegram->type_id == publish_id_) { if (telegram->type_id == publish_id_) {
publish_id_ = 0; publish_id_ = 0;
} }
@@ -803,7 +814,8 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
if (watch() == WATCH_UNKNOWN) { if (watch() == WATCH_UNKNOWN) {
LOG_NOTICE(pretty_telegram(telegram).c_str()); LOG_NOTICE(pretty_telegram(telegram).c_str());
} }
if (first_scan_done_ && !knowndevice && (telegram->src != EMSbus::ems_bus_id()) && (telegram->src != 0x0B) && (telegram->src != 0x0C) && (telegram->src != 0x0D)) { if (first_scan_done_ && !knowndevice && (telegram->src != EMSbus::ems_bus_id()) && (telegram->src != 0x0B) && (telegram->src != 0x0C)
&& (telegram->src != 0x0D)) {
send_read_request(EMSdevice::EMS_TYPE_VERSION, telegram->src); send_read_request(EMSdevice::EMS_TYPE_VERSION, telegram->src);
} }
} }
@@ -899,7 +911,8 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
// sometimes boilers share the same product id as controllers // sometimes boilers share the same product id as controllers
// so only add boilers if the device_id is 0x08, which is fixed for EMS // so only add boilers if the device_id is 0x08, which is fixed for EMS
if (device.device_type == DeviceType::BOILER) { if (device.device_type == DeviceType::BOILER) {
if (device_id == EMSdevice::EMS_DEVICE_ID_BOILER || (device_id >= EMSdevice::EMS_DEVICE_ID_BOILER_1 && device_id <= EMSdevice::EMS_DEVICE_ID_BOILER_F)) { if (device_id == EMSdevice::EMS_DEVICE_ID_BOILER
|| (device_id >= EMSdevice::EMS_DEVICE_ID_BOILER_1 && device_id <= EMSdevice::EMS_DEVICE_ID_BOILER_F)) {
device_p = &device; device_p = &device;
break; break;
} }
@@ -915,7 +928,8 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
if (device_p == nullptr) { if (device_p == nullptr) {
LOG_NOTICE(F("Unrecognized EMS device (device ID 0x%02X, product ID %d). Please report on GitHub."), device_id, product_id); LOG_NOTICE(F("Unrecognized EMS device (device ID 0x%02X, product ID %d). Please report on GitHub."), device_id, product_id);
std::string name("unknown"); std::string name("unknown");
emsdevices.push_back(EMSFactory::add(DeviceType::GENERIC, device_id, product_id, version, name, DeviceFlags::EMS_DEVICE_FLAG_NONE, EMSdevice::Brand::NO_BRAND)); emsdevices.push_back(
EMSFactory::add(DeviceType::GENERIC, device_id, product_id, version, name, DeviceFlags::EMS_DEVICE_FLAG_NONE, EMSdevice::Brand::NO_BRAND));
return false; // not found return false; // not found
} }
@@ -933,7 +947,9 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
return true; return true;
} }
Command::add_with_json(device_type, F_(info), [device_type](const char * value, const int8_t id, JsonObject & json) { return command_info(device_type, json, id); }); Command::add_with_json(device_type, F_(info), [device_type](const char * value, const int8_t id, JsonObject & json) {
return command_info(device_type, json, id);
});
return true; return true;
} }
@@ -952,7 +968,8 @@ bool EMSESP::command_info(uint8_t device_type, JsonObject & json, const int8_t i
} }
for (const auto & emsdevice : emsdevices) { for (const auto & emsdevice : emsdevices) {
if (emsdevice && (emsdevice->device_type() == device_type) && ((device_type != DeviceType::THERMOSTAT) || (emsdevice->device_id() == EMSESP::actual_master_thermostat()))) { if (emsdevice && (emsdevice->device_type() == device_type)
&& ((device_type != DeviceType::THERMOSTAT) || (emsdevice->device_id() == EMSESP::actual_master_thermostat()))) {
has_value |= emsdevice->generate_values_json(json, tag, (id < 1), (id == -1)); // nested for id -1,0 & console for id -1 has_value |= emsdevice->generate_values_json(json, tag, (id < 1), (id == -1)); // nested for id -1,0 & console for id -1
} }
} }
@@ -966,7 +983,12 @@ void EMSESP::send_read_request(const uint16_t type_id, const uint8_t dest, const
} }
// sends write request // sends write request
void EMSESP::send_write_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset, uint8_t * message_data, const uint8_t message_length, const uint16_t validate_typeid) { void EMSESP::send_write_request(const uint16_t type_id,
const uint8_t dest,
const uint8_t offset,
uint8_t * message_data,
const uint8_t message_length,
const uint16_t validate_typeid) {
txservice_.add(Telegram::Operation::TX_WRITE, dest, type_id, offset, message_data, message_length, validate_typeid, true); txservice_.add(Telegram::Operation::TX_WRITE, dest, type_id, offset, message_data, message_length, validate_typeid, true);
} }

View File

@@ -178,6 +178,7 @@ class EMSESP {
} }
static void fetch_device_values(const uint8_t device_id = 0); static void fetch_device_values(const uint8_t device_id = 0);
static void fetch_device_values_type(const uint8_t device_type);
static bool add_device(const uint8_t device_id, const uint8_t product_id, std::string & version, const uint8_t brand); static bool add_device(const uint8_t device_id, const uint8_t product_id, std::string & version, const uint8_t brand);
static void scan_devices(); static void scan_devices();

View File

@@ -1,2 +1,2 @@
#define EMSESP_APP_VERSION "3.0.3b4" #define EMSESP_APP_VERSION "3.0.3b5"
#define EMSESP_PLATFORM "ESP32" #define EMSESP_PLATFORM "ESP32"