mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 15:59: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)
|
- 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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user