mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
feat: Adopt the OpenAPI 3.0 standard for the REST API #50
This commit is contained in:
@@ -21,5 +21,6 @@
|
||||
- Re-enabled Shower Alert (still experimental)
|
||||
- lowercased Flow temp in commands
|
||||
- system console commands to main
|
||||
- new secure API ([#50](https://github.com/emsesp/EMS-ESP32/issues/50))
|
||||
|
||||
## Removed
|
||||
|
||||
@@ -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
|
||||
|
||||
```bash
|
||||
curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -X POST 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}" -X GET http://ems-esp/api/system/settings
|
||||
curl -i -H "Authorization: Bearer {ACCESS_TOKEN}" -H "Content-Type: application/json" -d '{ "name": "wwtemp", "value":60}' http://ems-esp/api/boiler
|
||||
```
|
||||
|
||||
## Error handling
|
||||
@@ -36,26 +36,11 @@ OpenAPI is an open standard specification for describing REST APIs. From the [Op
|
||||
{"message":"Problems parsing JSON"}
|
||||
```
|
||||
|
||||
- Sending the wrong type of JSON values will result in a `400 Bad Request` 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.
|
||||
- Sending invalid fields will result in a `422 Unprocessable Entity` response.
|
||||
|
||||
```html
|
||||
HTTP/1.1 422 Unprocessable Entity
|
||||
{
|
||||
"message": "Validation Failed",
|
||||
"errors": [
|
||||
{
|
||||
"field": "title",
|
||||
"code": "missing_field"
|
||||
}
|
||||
]
|
||||
}
|
||||
{"message":"Invalid command"}
|
||||
```
|
||||
|
||||
## 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}/{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 |
|
||||
| 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}] }` |
|
||||
| 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 | `{ ["name" : <string>] , ["hc": <number>], "value": <value> }` |
|
||||
|
||||
## System Endpoints
|
||||
|
||||
| Method | Endpoint | Description | Access Token required | JSON body data |
|
||||
| - | - | - | - | - |
|
||||
| GET | `/system/info` | list system information | | | |
|
||||
| GET | `/system/settings` | list all settings, except passwords | x |
|
||||
| POST/PUT | `/system/pin` | switch a GPIO state to HIGH or LOW | x | `{ "pin":<integer>, "value":<boolean> }` |
|
||||
| POST/PUT | `/system/send` | send a telegram to the EMS bus | x | `{ "telegram" : <string> }` |
|
||||
| POST/PUT | `/system/publish` | force an MQTT publish | x | `[{ "name" : <device> \| "ha" }]` |
|
||||
| POST/PUT | `/system/fetch` | fetch all EMS data from all devices | x | |
|
||||
| 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
|
||||
| GET | `/system/settings` | list all settings, except passwords | |
|
||||
| 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 | `{ "value" : <string> }` |
|
||||
| POST/PUT | `/system/publish` | force an MQTT publish | x | `{ "value" : <device> \| "ha" }` |
|
||||
| POST/PUT | `/system/fetch` | fetch all EMS data from all devices | x | `{ "value" : <device> \| "all" }` |
|
||||
|
||||
@@ -396,12 +396,12 @@ class EMSESPSettingsForm extends React.Component<EMSESPSettingsFormProps> {
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data.api_enabled}
|
||||
onChange={handleValueChange("api_enabled")}
|
||||
value="api_enabled"
|
||||
checked={data.notoken_api}
|
||||
onChange={handleValueChange("notoken_api")}
|
||||
value="notoken_api"
|
||||
/>
|
||||
}
|
||||
label="Enable API write commands"
|
||||
label="Bypass Access Token authorization on API calls"
|
||||
/>
|
||||
<BlockFormControlLabel
|
||||
control={
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface EMSESPSettings {
|
||||
dallas_parasite: boolean;
|
||||
led_gpio: number;
|
||||
hide_led: boolean;
|
||||
api_enabled: boolean;
|
||||
notoken_api: boolean;
|
||||
analog_enabled: boolean;
|
||||
pbutton_gpio: number;
|
||||
trace_raw: boolean;
|
||||
|
||||
@@ -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 {
|
||||
protected:
|
||||
DynamicJsonDocument _jsonBuffer;
|
||||
@@ -56,14 +103,18 @@ class AsyncJsonResponse {
|
||||
else
|
||||
_root = _jsonBuffer.createNestedObject();
|
||||
}
|
||||
|
||||
~AsyncJsonResponse() {
|
||||
}
|
||||
|
||||
JsonVariant & getRoot() {
|
||||
return _root;
|
||||
}
|
||||
|
||||
bool _sourceValid() const {
|
||||
return _isValid;
|
||||
}
|
||||
|
||||
size_t setLength() {
|
||||
return 0;
|
||||
}
|
||||
@@ -75,6 +126,9 @@ class AsyncJsonResponse {
|
||||
size_t _fillBuffer(uint8_t * data, size_t len) {
|
||||
return len;
|
||||
}
|
||||
|
||||
void setCode(uint16_t) {
|
||||
}
|
||||
};
|
||||
|
||||
typedef std::function<void(AsyncWebServerRequest * request, JsonVariant & json)> ArJsonRequestHandlerFunction;
|
||||
|
||||
@@ -25,7 +25,7 @@ class DummySettings {
|
||||
bool shower_timer = true;
|
||||
bool shower_alert = false;
|
||||
bool hide_led = false;
|
||||
bool api_enabled = true;
|
||||
bool notoken_api = false;
|
||||
|
||||
// MQTT
|
||||
uint16_t publish_time = 10; // seconds
|
||||
|
||||
@@ -11,6 +11,7 @@ class AsyncWebServer;
|
||||
class AsyncWebServerRequest;
|
||||
class AsyncWebServerResponse;
|
||||
class AsyncJsonResponse;
|
||||
class PrettyAsyncJsonResponse;
|
||||
|
||||
class AsyncWebParameter {
|
||||
private:
|
||||
@@ -68,11 +69,13 @@ class AsyncWebServerRequest {
|
||||
AsyncWebServer * _server;
|
||||
WebRequestMethodComposite _method;
|
||||
|
||||
String _url;
|
||||
|
||||
public:
|
||||
void * _tempObject;
|
||||
|
||||
AsyncWebServerRequest(AsyncWebServer *, AsyncClient *);
|
||||
~AsyncWebServerRequest();
|
||||
AsyncWebServerRequest(AsyncWebServer *, AsyncClient *){};
|
||||
~AsyncWebServerRequest(){};
|
||||
|
||||
AsyncClient * client() {
|
||||
return _client;
|
||||
@@ -82,13 +85,30 @@ class AsyncWebServerRequest {
|
||||
return _method;
|
||||
}
|
||||
|
||||
void method(WebRequestMethodComposite method_s) {
|
||||
_method = method_s;
|
||||
}
|
||||
|
||||
void addInterestingHeader(const String & name){};
|
||||
|
||||
size_t args() const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void send(AsyncWebServerResponse * 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, 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 {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ const emsesp_settings = {
|
||||
"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,
|
||||
"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"
|
||||
};
|
||||
const emsesp_alldevices = {
|
||||
|
||||
@@ -16,76 +16,308 @@
|
||||
* 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"
|
||||
|
||||
using namespace std::placeholders; // for `_1` etc
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
WebAPIService::WebAPIService(AsyncWebServer * server) {
|
||||
server->on(EMSESP_API_SERVICE_PATH, HTTP_GET, std::bind(&WebAPIService::webAPIService, this, _1));
|
||||
WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * securityManager)
|
||||
: _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
|
||||
void WebAPIService::webAPIService(AsyncWebServerRequest * request) {
|
||||
// must have device and cmd parameters
|
||||
if ((!request->hasParam(F_(device))) || (!request->hasParam(F_(cmd)))) {
|
||||
request->send(400, "text/plain", F("Invalid syntax"));
|
||||
// GET /{device}
|
||||
// GET /{device}/{name}
|
||||
// GET /device={device}?cmd={name}?data={value}[?id={hc}
|
||||
void WebAPIService::webAPIService_get(AsyncWebServerRequest * request) {
|
||||
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;
|
||||
}
|
||||
|
||||
// get device
|
||||
String device = request->getParam(F_(device))->value();
|
||||
uint8_t device_type = EMSdevice::device_name_2_device_type(device.c_str());
|
||||
if (device_type == emsesp::EMSdevice::DeviceType::UNKNOWN) {
|
||||
request->send(400, "text/plain", F("Invalid device"));
|
||||
return;
|
||||
// extract values from the json
|
||||
// these will be used as default values
|
||||
auto && body = json.as<JsonObject>();
|
||||
|
||||
std::string device = body["name"].as<std::string>(); // note this was called device in the v2
|
||||
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
|
||||
String cmd = request->getParam(F_(cmd))->value();
|
||||
// make sure we have a value. There must always be a 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))) {
|
||||
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))) {
|
||||
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()) {
|
||||
id = "-1";
|
||||
// now go and validate everything
|
||||
|
||||
// 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);
|
||||
JsonObject json = doc.to<JsonObject>();
|
||||
bool ok = false;
|
||||
// cmd check
|
||||
// if the cmd is empty, default it 'info'
|
||||
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
|
||||
if (data.isEmpty()) {
|
||||
ok = Command::call(device_type, cmd.c_str(), nullptr, id.toInt(), json); // command only
|
||||
} else {
|
||||
// we only allow commands with parameters if the API is enabled
|
||||
bool api_enabled;
|
||||
EMSESP::webSettingsService.read([&](WebSettings & settings) { api_enabled = settings.api_enabled; });
|
||||
if (api_enabled) {
|
||||
ok = Command::call(device_type, cmd.c_str(), data.c_str(), id.toInt(), json); // has cmd, data and id
|
||||
} else {
|
||||
request->send(401, "text/plain", F("Unauthorized"));
|
||||
// check that we have permissions first. We require authenticating on 1 or more of these conditions:
|
||||
// 1. any HTTP POSTs or PUTs
|
||||
// 2. a HTTP GET which has a 'data' parameter which is not empty (to keep v2 compatibility)
|
||||
auto method = request->method();
|
||||
bool have_data = !value_s.empty();
|
||||
bool admin_allowed;
|
||||
EMSESP::webSettingsService.read([&](WebSettings & settings) {
|
||||
Authentication authentication = _securityManager->authenticateRequest(request);
|
||||
admin_allowed = settings.notoken_api | AuthenticationPredicates::IS_ADMIN(authentication);
|
||||
});
|
||||
|
||||
if ((method != HTTP_GET) || ((method == HTTP_GET) && have_data)) {
|
||||
if (!admin_allowed) {
|
||||
send_message_response(request, 401, "Bad credentials"); // Unauthorized
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (ok && json.size()) {
|
||||
// send json output back to web
|
||||
doc.shrinkToFit();
|
||||
std::string buffer;
|
||||
serializeJsonPretty(doc, buffer);
|
||||
request->send(200, "text/plain;charset=utf-8", buffer.c_str());
|
||||
|
||||
// now we have all the parameters go and execute the command
|
||||
PrettyAsyncJsonResponse * response = new PrettyAsyncJsonResponse(false, EMSESP_JSON_SIZE_XLARGE_DYN);
|
||||
JsonObject json = response->getRoot();
|
||||
|
||||
// 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;
|
||||
}
|
||||
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
|
||||
@@ -23,16 +23,52 @@
|
||||
#include <AsyncJson.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#define EMSESP_API_SERVICE_PATH "/api"
|
||||
|
||||
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 {
|
||||
public:
|
||||
WebAPIService(AsyncWebServer * server);
|
||||
WebAPIService(AsyncWebServer * server, SecurityManager * securityManager);
|
||||
|
||||
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
|
||||
|
||||
@@ -110,7 +110,7 @@ void WebDevicesService::device_data(AsyncWebServerRequest * request, JsonVariant
|
||||
void WebDevicesService::write_value(AsyncWebServerRequest * request, JsonVariant & json) {
|
||||
// only issue commands if the API is enabled
|
||||
EMSESP::webSettingsService.read([&](WebSettings & settings) {
|
||||
if (!settings.api_enabled) {
|
||||
if (!settings.notoken_api) {
|
||||
request->send(403); // forbidden error
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ void WebSettings::read(WebSettings & settings, JsonObject & root) {
|
||||
root["dallas_parasite"] = settings.dallas_parasite;
|
||||
root["led_gpio"] = settings.led_gpio;
|
||||
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["pbutton_gpio"] = settings.pbutton_gpio;
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class WebSettings {
|
||||
bool dallas_parasite;
|
||||
uint8_t led_gpio;
|
||||
bool hide_led;
|
||||
bool api_enabled;
|
||||
bool notoken_api;
|
||||
bool analog_enabled;
|
||||
uint8_t pbutton_gpio;
|
||||
String board_profile;
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
#define EMSESP_DEFAULT_DALLAS_PARASITE false
|
||||
#endif
|
||||
|
||||
#ifndef EMSESP_DEFAULT_API_ENABLED
|
||||
#define EMSESP_DEFAULT_API_ENABLED false // turn off, because its insecure
|
||||
#ifndef EMSESP_DEFAULT_NOTOKEN_API
|
||||
#define EMSESP_DEFAULT_NOTOKEN_API false
|
||||
#endif
|
||||
|
||||
#ifndef EMSESP_DEFAULT_BOOL_FORMAT
|
||||
|
||||
@@ -39,7 +39,7 @@ WebSettingsService EMSESP::webSettingsService = WebSettingsService(&webServer, &
|
||||
|
||||
WebStatusService EMSESP::webStatusService = WebStatusService(&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 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
|
||||
void EMSESP::clear_all_devices() {
|
||||
// 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;
|
||||
} 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());
|
||||
} else if (!trace_raw_) {
|
||||
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);
|
||||
// if we correctly processes the telegram follow up with sending it via MQTT if needed
|
||||
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_) {
|
||||
publish_id_ = 0;
|
||||
}
|
||||
@@ -803,7 +814,8 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
|
||||
if (watch() == WATCH_UNKNOWN) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
// so only add boilers if the device_id is 0x08, which is fixed for EMS
|
||||
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;
|
||||
break;
|
||||
}
|
||||
@@ -915,7 +928,8 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
|
||||
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);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -933,7 +947,9 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
|
||||
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;
|
||||
}
|
||||
@@ -952,7 +968,8 @@ bool EMSESP::command_info(uint8_t device_type, JsonObject & json, const int8_t i
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -966,7 +983,12 @@ void EMSESP::send_read_request(const uint16_t type_id, const uint8_t dest, const
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@ class EMSESP {
|
||||
}
|
||||
|
||||
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 void scan_devices();
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#define EMSESP_APP_VERSION "3.0.3b4"
|
||||
#define EMSESP_APP_VERSION "3.0.3b5"
|
||||
#define EMSESP_PLATFORM "ESP32"
|
||||
|
||||
Reference in New Issue
Block a user