mirror of
https://github.com/esphome/esphome.git
synced 2024-11-22 23:18:10 +01:00
http_request component (#719)
* It works * Template doesn't work * Template fix * CA Certificate untested * ESP32 done * URL validation * Lint fix * Lint fix (2) * Lint fix (<3) * Support unsecure requests with framework >=2.5.0 * Removed fingerprint, payload renamed to body * Removed add_extra * Review * Review * New HTTP methods * Check recommended version * Removed dead code * Small improvement * Small improvement * CONF_METHOD from const * JSON support * New JSON syntax * Templatable headers * verify_ssl param * verify_ssl param (fix) * Lint * nolint * JSON string_strict * Two json syntax * Lambda url fix validation * CI fix * CI fix
This commit is contained in:
parent
3e8fd48dc0
commit
f8d98ac494
6 changed files with 341 additions and 5 deletions
|
@ -7,13 +7,17 @@ from esphome.util import Registry
|
||||||
|
|
||||||
|
|
||||||
def maybe_simple_id(*validators):
|
def maybe_simple_id(*validators):
|
||||||
|
return maybe_conf(CONF_ID, *validators)
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_conf(conf, *validators):
|
||||||
validator = cv.All(*validators)
|
validator = cv.All(*validators)
|
||||||
|
|
||||||
def validate(value):
|
def validate(value):
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
return validator(value)
|
return validator(value)
|
||||||
with cv.remove_prepend_path([CONF_ID]):
|
with cv.remove_prepend_path([conf]):
|
||||||
return validator({CONF_ID: value})
|
return validator({conf: value})
|
||||||
|
|
||||||
return validate
|
return validate
|
||||||
|
|
||||||
|
|
148
esphome/components/http_request/__init__.py
Normal file
148
esphome/components/http_request/__init__.py
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome import automation
|
||||||
|
from esphome.const import CONF_ID, CONF_TIMEOUT, CONF_ESPHOME, CONF_METHOD, \
|
||||||
|
CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266_2_5_0
|
||||||
|
from esphome.core import CORE, Lambda
|
||||||
|
from esphome.core_config import PLATFORMIO_ESP8266_LUT
|
||||||
|
from esphome.py_compat import IS_PY3
|
||||||
|
|
||||||
|
if IS_PY3:
|
||||||
|
import urllib.parse as urlparse # pylint: disable=no-name-in-module,import-error
|
||||||
|
else:
|
||||||
|
import urlparse # pylint: disable=import-error
|
||||||
|
|
||||||
|
DEPENDENCIES = ['network']
|
||||||
|
AUTO_LOAD = ['json']
|
||||||
|
|
||||||
|
http_request_ns = cg.esphome_ns.namespace('http_request')
|
||||||
|
HttpRequestComponent = http_request_ns.class_('HttpRequestComponent', cg.Component)
|
||||||
|
HttpRequestSendAction = http_request_ns.class_('HttpRequestSendAction', automation.Action)
|
||||||
|
|
||||||
|
CONF_URL = 'url'
|
||||||
|
CONF_HEADERS = 'headers'
|
||||||
|
CONF_USERAGENT = 'useragent'
|
||||||
|
CONF_BODY = 'body'
|
||||||
|
CONF_JSON = 'json'
|
||||||
|
CONF_VERIFY_SSL = 'verify_ssl'
|
||||||
|
|
||||||
|
|
||||||
|
def validate_framework(config):
|
||||||
|
if CORE.is_esp32:
|
||||||
|
return config
|
||||||
|
|
||||||
|
version = 'RECOMMENDED'
|
||||||
|
if CONF_ARDUINO_VERSION in CORE.raw_config[CONF_ESPHOME]:
|
||||||
|
version = CORE.raw_config[CONF_ESPHOME][CONF_ARDUINO_VERSION]
|
||||||
|
|
||||||
|
if version in ['LATEST', 'DEV']:
|
||||||
|
return config
|
||||||
|
|
||||||
|
framework = PLATFORMIO_ESP8266_LUT[version] if version in PLATFORMIO_ESP8266_LUT else version
|
||||||
|
if framework < ARDUINO_VERSION_ESP8266_2_5_0:
|
||||||
|
raise cv.Invalid('This component is not supported on arduino framework version below 2.5.0')
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def validate_url(value):
|
||||||
|
value = cv.string(value)
|
||||||
|
try:
|
||||||
|
parsed = list(urlparse.urlparse(value))
|
||||||
|
except Exception:
|
||||||
|
raise cv.Invalid('Invalid URL')
|
||||||
|
|
||||||
|
if not parsed[0] or not parsed[1]:
|
||||||
|
raise cv.Invalid('URL must have a URL scheme and host')
|
||||||
|
|
||||||
|
if parsed[0] not in ['http', 'https']:
|
||||||
|
raise cv.Invalid('Scheme must be http or https')
|
||||||
|
|
||||||
|
if not parsed[2]:
|
||||||
|
parsed[2] = '/'
|
||||||
|
|
||||||
|
return urlparse.urlunparse(parsed)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_secure_url(config):
|
||||||
|
url_ = config[CONF_URL]
|
||||||
|
if config.get(CONF_VERIFY_SSL) and not isinstance(url_, Lambda) \
|
||||||
|
and url_.lower().startswith('https:'):
|
||||||
|
raise cv.Invalid('Currently ESPHome doesn\'t support SSL verification. '
|
||||||
|
'Set \'verify_ssl: false\' to make insecure HTTPS requests.')
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema({
|
||||||
|
cv.GenerateID(): cv.declare_id(HttpRequestComponent),
|
||||||
|
cv.Optional(CONF_USERAGENT, 'ESPHome'): cv.string,
|
||||||
|
cv.Optional(CONF_TIMEOUT, default='5s'): cv.positive_time_period_milliseconds,
|
||||||
|
}).add_extra(validate_framework).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
|
||||||
|
cg.add(var.set_useragent(config[CONF_USERAGENT]))
|
||||||
|
yield cg.register_component(var, config)
|
||||||
|
|
||||||
|
|
||||||
|
HTTP_REQUEST_ACTION_SCHEMA = cv.Schema({
|
||||||
|
cv.GenerateID(): cv.use_id(HttpRequestComponent),
|
||||||
|
cv.Required(CONF_URL): cv.templatable(validate_url),
|
||||||
|
cv.Optional(CONF_HEADERS): cv.All(cv.Schema({cv.string: cv.templatable(cv.string)})),
|
||||||
|
cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean,
|
||||||
|
}).add_extra(validate_secure_url)
|
||||||
|
HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf(
|
||||||
|
CONF_URL, HTTP_REQUEST_ACTION_SCHEMA.extend({
|
||||||
|
cv.Optional(CONF_METHOD, default='GET'): cv.one_of('GET', upper=True),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
HTTP_REQUEST_POST_ACTION_SCHEMA = automation.maybe_conf(
|
||||||
|
CONF_URL, HTTP_REQUEST_ACTION_SCHEMA.extend({
|
||||||
|
cv.Optional(CONF_METHOD, default='POST'): cv.one_of('POST', upper=True),
|
||||||
|
cv.Exclusive(CONF_BODY, 'body'): cv.templatable(cv.string),
|
||||||
|
cv.Exclusive(CONF_JSON, 'body'): cv.Any(
|
||||||
|
cv.lambda_, cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend({
|
||||||
|
cv.Required(CONF_METHOD): cv.one_of('GET', 'POST', 'PUT', 'DELETE', 'PATCH', upper=True),
|
||||||
|
cv.Exclusive(CONF_BODY, 'body'): cv.templatable(cv.string),
|
||||||
|
cv.Exclusive(CONF_JSON, 'body'): cv.Any(
|
||||||
|
cv.lambda_, cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action('http_request.get', HttpRequestSendAction,
|
||||||
|
HTTP_REQUEST_GET_ACTION_SCHEMA)
|
||||||
|
@automation.register_action('http_request.post', HttpRequestSendAction,
|
||||||
|
HTTP_REQUEST_POST_ACTION_SCHEMA)
|
||||||
|
@automation.register_action('http_request.send', HttpRequestSendAction,
|
||||||
|
HTTP_REQUEST_SEND_ACTION_SCHEMA)
|
||||||
|
def http_request_action_to_code(config, action_id, template_arg, args):
|
||||||
|
paren = yield cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
|
||||||
|
template_ = yield cg.templatable(config[CONF_URL], args, cg.const_char_ptr)
|
||||||
|
cg.add(var.set_url(template_))
|
||||||
|
cg.add(var.set_method(config[CONF_METHOD]))
|
||||||
|
if CONF_BODY in config:
|
||||||
|
template_ = yield cg.templatable(config[CONF_BODY], args, cg.std_string)
|
||||||
|
cg.add(var.set_body(template_))
|
||||||
|
if CONF_JSON in config:
|
||||||
|
json_ = config[CONF_JSON]
|
||||||
|
if isinstance(json_, Lambda):
|
||||||
|
args_ = args + [(cg.JsonObjectRef, 'root')]
|
||||||
|
lambda_ = yield cg.process_lambda(json_, args_, return_type=cg.void)
|
||||||
|
cg.add(var.set_json(lambda_))
|
||||||
|
else:
|
||||||
|
for key in json_:
|
||||||
|
template_ = yield cg.templatable(json_[key], args, cg.std_string)
|
||||||
|
cg.add(var.add_json(key, template_))
|
||||||
|
for key in config.get(CONF_HEADERS, []):
|
||||||
|
template_ = yield cg.templatable(config[CONF_HEADERS][key], args, cg.const_char_ptr)
|
||||||
|
cg.add(var.add_header(key, template_))
|
||||||
|
|
||||||
|
yield var
|
63
esphome/components/http_request/http_request.cpp
Normal file
63
esphome/components/http_request/http_request.cpp
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
#include "http_request.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace http_request {
|
||||||
|
|
||||||
|
static const char *TAG = "http_request";
|
||||||
|
|
||||||
|
void HttpRequestComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "HTTP Request:");
|
||||||
|
ESP_LOGCONFIG(TAG, " Timeout: %ums", this->timeout_);
|
||||||
|
ESP_LOGCONFIG(TAG, " User-Agent: %s", this->useragent_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpRequestComponent::send() {
|
||||||
|
bool begin_status = false;
|
||||||
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
begin_status = this->client_.begin(this->url_);
|
||||||
|
#endif
|
||||||
|
#ifdef ARDUINO_ARCH_ESP8266
|
||||||
|
#ifndef CLANG_TIDY
|
||||||
|
begin_status = this->client_.begin(*this->wifi_client_, this->url_);
|
||||||
|
this->client_.setFollowRedirects(true);
|
||||||
|
this->client_.setRedirectLimit(3);
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!begin_status) {
|
||||||
|
this->client_.end();
|
||||||
|
this->status_set_warning();
|
||||||
|
ESP_LOGW(TAG, "HTTP Request failed at the begin phase. Please check the configuration");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->client_.setTimeout(this->timeout_);
|
||||||
|
if (this->useragent_ != nullptr) {
|
||||||
|
this->client_.setUserAgent(this->useragent_);
|
||||||
|
}
|
||||||
|
for (const auto &header : this->headers_) {
|
||||||
|
this->client_.addHeader(header.name, header.value, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
int http_code = this->client_.sendRequest(this->method_, this->body_.c_str());
|
||||||
|
this->client_.end();
|
||||||
|
|
||||||
|
if (http_code < 0) {
|
||||||
|
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", this->url_, HTTPClient::errorToString(http_code).c_str());
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (http_code < 200 || http_code >= 300) {
|
||||||
|
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d", this->url_, http_code);
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->status_clear_warning();
|
||||||
|
ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d", this->url_, http_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace http_request
|
||||||
|
} // namespace esphome
|
121
esphome/components/http_request/http_request.h
Normal file
121
esphome/components/http_request/http_request.h
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <list>
|
||||||
|
#include <map>
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/components/json/json_util.h"
|
||||||
|
|
||||||
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#endif
|
||||||
|
#ifdef ARDUINO_ARCH_ESP8266
|
||||||
|
#include <ESP8266HTTPClient.h>
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace http_request {
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
const char *name;
|
||||||
|
const char *value;
|
||||||
|
};
|
||||||
|
|
||||||
|
class HttpRequestComponent : public Component {
|
||||||
|
public:
|
||||||
|
void setup() override {
|
||||||
|
#ifdef ARDUINO_ARCH_ESP8266
|
||||||
|
this->wifi_client_ = new BearSSL::WiFiClientSecure();
|
||||||
|
this->wifi_client_->setInsecure();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
|
||||||
|
void set_url(const char *url) { this->url_ = url; }
|
||||||
|
void set_method(const char *method) { this->method_ = method; }
|
||||||
|
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
||||||
|
void set_timeout(uint16_t timeout) { this->timeout_ = timeout; }
|
||||||
|
void set_body(std::string body) { this->body_ = body; }
|
||||||
|
void set_headers(std::list<Header> headers) { this->headers_ = headers; }
|
||||||
|
void send();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HTTPClient client_{};
|
||||||
|
const char *url_;
|
||||||
|
const char *method_;
|
||||||
|
const char *useragent_{nullptr};
|
||||||
|
uint16_t timeout_{5000};
|
||||||
|
std::string body_;
|
||||||
|
std::list<Header> headers_;
|
||||||
|
#ifdef ARDUINO_ARCH_ESP8266
|
||||||
|
BearSSL::WiFiClientSecure *wifi_client_;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
HttpRequestSendAction(HttpRequestComponent *parent) : parent_(parent) {}
|
||||||
|
TEMPLATABLE_VALUE(const char *, url)
|
||||||
|
TEMPLATABLE_VALUE(const char *, method)
|
||||||
|
TEMPLATABLE_VALUE(std::string, body)
|
||||||
|
TEMPLATABLE_VALUE(const char *, useragent)
|
||||||
|
TEMPLATABLE_VALUE(uint16_t, timeout)
|
||||||
|
|
||||||
|
void add_header(const char *key, TemplatableValue<const char *, Ts...> value) { this->headers_.insert({key, value}); }
|
||||||
|
|
||||||
|
void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.insert({key, value}); }
|
||||||
|
|
||||||
|
void set_json(std::function<void(Ts..., JsonObject &)> json_func) { this->json_func_ = json_func; }
|
||||||
|
|
||||||
|
void play(Ts... x) override {
|
||||||
|
this->parent_->set_url(this->url_.value(x...));
|
||||||
|
this->parent_->set_method(this->method_.value(x...));
|
||||||
|
if (this->body_.has_value()) {
|
||||||
|
this->parent_->set_body(this->body_.value(x...));
|
||||||
|
}
|
||||||
|
if (!this->json_.empty()) {
|
||||||
|
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_, this, x..., std::placeholders::_1);
|
||||||
|
this->parent_->set_body(json::build_json(f));
|
||||||
|
}
|
||||||
|
if (this->json_func_ != nullptr) {
|
||||||
|
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1);
|
||||||
|
this->parent_->set_body(json::build_json(f));
|
||||||
|
}
|
||||||
|
if (this->useragent_.has_value()) {
|
||||||
|
this->parent_->set_useragent(this->useragent_.value(x...));
|
||||||
|
}
|
||||||
|
if (this->timeout_.has_value()) {
|
||||||
|
this->parent_->set_timeout(this->timeout_.value(x...));
|
||||||
|
}
|
||||||
|
if (!this->headers_.empty()) {
|
||||||
|
std::list<Header> headers;
|
||||||
|
for (const auto &item : this->headers_) {
|
||||||
|
auto val = item.second;
|
||||||
|
Header header;
|
||||||
|
header.name = item.first;
|
||||||
|
header.value = val.value(x...);
|
||||||
|
headers.push_back(header);
|
||||||
|
}
|
||||||
|
this->parent_->set_headers(headers);
|
||||||
|
}
|
||||||
|
this->parent_->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void encode_json_(Ts... x, JsonObject &root) {
|
||||||
|
for (const auto &item : this->json_) {
|
||||||
|
auto val = item.second;
|
||||||
|
root[item.first] = val.value(x...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void encode_json_func_(Ts... x, JsonObject &root) { this->json_func_(x..., root); }
|
||||||
|
HttpRequestComponent *parent_;
|
||||||
|
std::map<const char *, TemplatableValue<const char *, Ts...>> headers_{};
|
||||||
|
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
|
||||||
|
std::function<void(Ts..., JsonObject &)> json_func_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace http_request
|
||||||
|
} // namespace esphome
|
|
@ -20,7 +20,7 @@ ARDUINO_VERSION_ESP32_1_0_3 = 'espressif32@1.10.0'
|
||||||
ARDUINO_VERSION_ESP32_1_0_4 = 'espressif32@1.11.0'
|
ARDUINO_VERSION_ESP32_1_0_4 = 'espressif32@1.11.0'
|
||||||
ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \
|
ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \
|
||||||
'/stage'
|
'/stage'
|
||||||
ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.0'
|
ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.1'
|
||||||
ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0'
|
ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0'
|
||||||
ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.3'
|
ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.3'
|
||||||
ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0'
|
ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0'
|
||||||
|
|
|
@ -76,7 +76,7 @@ def validate_arduino_version(value):
|
||||||
if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP8266_LUT:
|
if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP8266_LUT:
|
||||||
raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported "
|
raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported "
|
||||||
"at this time. You can override this by manually using "
|
"at this time. You can override this by manually using "
|
||||||
"espressif8266@<platformio version>")
|
"espressif8266@<platformio version>".format(value))
|
||||||
if value_ in PLATFORMIO_ESP8266_LUT:
|
if value_ in PLATFORMIO_ESP8266_LUT:
|
||||||
return PLATFORMIO_ESP8266_LUT[value_]
|
return PLATFORMIO_ESP8266_LUT[value_]
|
||||||
return value
|
return value
|
||||||
|
@ -84,7 +84,7 @@ def validate_arduino_version(value):
|
||||||
if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP32_LUT:
|
if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP32_LUT:
|
||||||
raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported "
|
raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported "
|
||||||
"at this time. You can override this by manually using "
|
"at this time. You can override this by manually using "
|
||||||
"espressif32@<platformio version>")
|
"espressif32@<platformio version>".format(value))
|
||||||
if value_ in PLATFORMIO_ESP32_LUT:
|
if value_ in PLATFORMIO_ESP32_LUT:
|
||||||
return PLATFORMIO_ESP32_LUT[value_]
|
return PLATFORMIO_ESP32_LUT[value_]
|
||||||
return value
|
return value
|
||||||
|
|
Loading…
Reference in a new issue