Add Captive Portal (#624)

* WIP: Captive Portal

* Updates

* Updates

* Lint

* Fixes
This commit is contained in:
Otto Winter 2019-06-09 17:03:51 +02:00 committed by GitHub
parent 8db6f3129c
commit 36f47ade70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 846 additions and 326 deletions

View file

@ -0,0 +1,26 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import web_server_base
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
from esphome.const import CONF_ID
from esphome.core import coroutine_with_priority
AUTO_LOAD = ['web_server_base']
DEPENDENCIES = ['wifi']
captive_portal_ns = cg.esphome_ns.namespace('captive_portal')
CaptivePortal = captive_portal_ns.class_('CaptivePortal', cg.Component)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(CaptivePortal),
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase),
}).extend(cv.COMPONENT_SCHEMA)
@coroutine_with_priority(64.0)
def to_code(config):
paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
var = cg.new_Pvariable(config[CONF_ID], paren)
yield cg.register_component(var, config)
cg.add_define('USE_CAPTIVE_PORTAL')

View file

@ -0,0 +1,173 @@
#include "captive_portal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/components/wifi/wifi_component.h"
namespace esphome {
namespace captive_portal {
static const char *TAG = "captive_portal";
void CaptivePortal::handle_index(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/html");
stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" "
"content=\"width=device-width,initial-scale=1,user-scalable=no\"/><title>"));
stream->print(App.get_name().c_str());
stream->print(F("</title><link rel=\"stylesheet\" href=\"/stylesheet.css\">"));
stream->print(F("<script>function c(l){document.getElementById('ssid').value=l.innerText||l.textContent; "
"document.getElementById('psk').focus();}</script>"));
stream->print(F("</head>"));
stream->print(F("<body><div class=\"main\"><h1>WiFi Networks</h1>"));
if (request->hasArg("save")) {
stream->print(F("<div class=\"info\">The ESP will now try to connect to the network...<br/>Please give it some "
"time to connect.<br/>Note: Copy the changed network to your YAML file - the next OTA update will "
"overwrite these settings.</div>"));
}
for (auto &scan : wifi::global_wifi_component->get_scan_result()) {
if (scan.get_is_hidden())
continue;
stream->print(F("<div class=\"network\" onclick=\"c(this)\"><a href=\"#\" class=\"network-left\">"));
if (scan.get_rssi() >= -50) {
stream->print(F("<img src=\"/wifi-strength-4.svg\">"));
} else if (scan.get_rssi() >= -65) {
stream->print(F("<img src=\"/wifi-strength-3.svg\">"));
} else if (scan.get_rssi() >= -85) {
stream->print(F("<img src=\"/wifi-strength-2.svg\">"));
} else {
stream->print(F("<img src=\"/wifi-strength-1.svg\">"));
}
stream->print(F("<span class=\"network-ssid\">"));
stream->print(scan.get_ssid().c_str());
stream->print(F("</span></a>"));
if (scan.get_with_auth()) {
stream->print(F("<img src=\"/lock.svg\">"));
}
stream->print(F("</div>"));
}
stream->print(F("<h3>WiFi Settings</h3><form method=\"GET\" action=\"/wifisave\"><input id=\"ssid\" name=\"ssid\" "
"length=32 placeholder=\"SSID\"><br/><input id=\"psk\" name=\"psk\" length=64 type=\"password\" "
"placeholder=\"Password\"><br/><br/><button type=\"submit\">Save</button></form><br><hr><br>"));
stream->print(F("<h1>OTA Update</h1><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
"type=\"file\" name=\"update\"><button type=\"submit\">Update</button></form>"));
stream->print(F("</div></body></html>"));
request->send(stream);
}
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
std::string ssid = request->arg("ssid").c_str();
std::string psk = request->arg("psk").c_str();
ESP_LOGI(TAG, "Captive Portal Requested WiFi Settings Change:");
ESP_LOGI(TAG, " SSID='%s'", ssid.c_str());
ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
this->override_sta_(ssid, psk);
request->redirect("/?save=true");
}
void CaptivePortal::override_sta_(const std::string &ssid, const std::string &password) {
CaptivePortalSettings save{};
strcpy(save.ssid, ssid.c_str());
strcpy(save.password, password.c_str());
this->pref_.save(&save);
wifi::WiFiAP sta{};
sta.set_ssid(ssid);
sta.set_password(password);
wifi::global_wifi_component->set_sta(sta);
}
void CaptivePortal::setup() {
// Hash with compilation time
// This ensures the AP override is not applied for OTA
uint32_t hash = fnv1_hash(App.get_compilation_time());
this->pref_ = global_preferences.make_preference<CaptivePortalSettings>(hash, true);
CaptivePortalSettings save{};
if (this->pref_.load(&save)) {
this->override_sta_(save.ssid, save.password);
}
}
void CaptivePortal::start() {
this->base_->init();
if (!this->initialized_) {
this->base_->add_handler(this);
this->base_->add_ota_handler();
}
this->dns_server_ = new DNSServer();
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
this->dns_server_->start(53, "*", ip);
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
bool not_found = false;
if (!this->active_) {
not_found = true;
} else if (req->host() == wifi::global_wifi_component->wifi_soft_ap_ip().toString()) {
not_found = true;
}
if (not_found) {
req->send(404, "text/html", "File not found");
return;
}
auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().toString();
req->redirect(url);
});
this->initialized_ = true;
this->active_ = true;
}
const char STYLESHEET_CSS[] PROGMEM =
R"(*{box-sizing:inherit}div,input{padding:5px;font-size:1em}input{width:95%}body{text-align:center;font-family:sans-serif}button{border:0;border-radius:.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;padding:0}.main{text-align:left;display:inline-block;min-width:260px}.network{display:flex;justify-content:space-between;align-items:center}.network-left{display:flex;align-items:center}.network-ssid{margin-bottom:-7px;margin-left:10px}.info{border:1px solid;margin:10px 0;padding:15px 10px;color:#4f8a10;background-color:#dff2bf})";
const char LOCK_SVG[] PROGMEM =
R"(<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z"/></svg>)";
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
if (req->url() == "/") {
this->handle_index(req);
return;
} else if (req->url() == "/wifisave") {
this->handle_wifisave(req);
return;
} else if (req->url() == "/stylesheet.css") {
req->send_P(200, "text/css", STYLESHEET_CSS);
return;
} else if (req->url() == "/lock.svg") {
req->send_P(200, "image/svg+xml", LOCK_SVG);
return;
}
AsyncResponseStream *stream = req->beginResponseStream("image/svg+xml");
stream->print(F("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\"><path d=\"M12 3A18.9 18.9 0 0 "
"0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 "));
if (req->url() == "/wifi-strength-4.svg") {
stream->print(F("3z"));
} else {
if (req->url() == "/wifi-strength-1.svg") {
stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.4"));
} else if (req->url() == "/wifi-strength-2.svg") {
stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.4"));
} else if (req->url() == "/wifi-strength-3.svg") {
stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2."));
}
stream->print(F("4A16.94 16.94 0 0 1 12 5z"));
}
stream->print(F("\"/></svg>"));
req->send(stream);
}
CaptivePortal::CaptivePortal(web_server_base::WebServerBase *base) : base_(base) { global_captive_portal = this; }
float CaptivePortal::get_setup_priority() const {
// Before WiFi
return setup_priority::WIFI + 1.0f;
}
CaptivePortal *global_captive_portal = nullptr;
} // namespace captive_portal
} // namespace esphome

View file

@ -0,0 +1,80 @@
#pragma once
#include <DNSServer.h>
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/components/web_server_base/web_server_base.h"
namespace esphome {
namespace captive_portal {
struct CaptivePortalSettings {
char ssid[33];
char password[65];
} PACKED; // NOLINT
class CaptivePortal : public AsyncWebHandler, public Component {
public:
CaptivePortal(web_server_base::WebServerBase *base);
void setup() override;
void loop() override {
if (this->dns_server_ != nullptr)
this->dns_server_->processNextRequest();
}
float get_setup_priority() const override;
void start();
bool is_active() const { return this->active_; }
void end() {
this->active_ = false;
this->base_->deinit();
this->dns_server_->stop();
delete this->dns_server_;
}
bool canHandle(AsyncWebServerRequest *request) override {
if (!this->active_)
return false;
if (request->method() == HTTP_GET) {
if (request->url() == "/")
return true;
if (request->url() == "/stylesheet.css")
return true;
if (request->url() == "/wifi-strength-1.svg")
return true;
if (request->url() == "/wifi-strength-2.svg")
return true;
if (request->url() == "/wifi-strength-3.svg")
return true;
if (request->url() == "/wifi-strength-4.svg")
return true;
if (request->url() == "/lock.svg")
return true;
if (request->url() == "/wifisave")
return true;
}
return false;
}
void handle_index(AsyncWebServerRequest *request);
void handle_wifisave(AsyncWebServerRequest *request);
void handleRequest(AsyncWebServerRequest *req) override;
protected:
void override_sta_(const std::string &ssid, const std::string &password);
web_server_base::WebServerBase *base_;
bool initialized_{false};
bool active_{false};
ESPPreferenceObject pref_;
DNSServer *dns_server_{nullptr};
};
extern CaptivePortal *global_captive_portal;
} // namespace captive_portal
} // namespace esphome

View file

@ -0,0 +1,55 @@
<!-- HTTP_HEAD -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
<title>{{ App.get_name() }}</title>
<link rel="stylesheet" href="./stylesheet.css">
<script>
function c(l) {
document.getElementById('ssid').value = l.innerText || l.textContent;
document.getElementById('psk').focus();
}
</script>
</head>
<body>
<div class="main">
<h1>WiFi Networks</h1>
<div class="info">
The ESP will now try to connect to the network...<br/>
Please give it some time to connect.<br/>
Note: Copy the changed network to your YAML file - the next OTA update will overwrite these settings.
</div>
<div class="network" onclick="c(this)">
<a href="#" class="network-left">
<img src="./wifi-strength-4.svg">
<span class="network-ssid">AP1</span>
</a>
<img src="./lock.svg">
</div>
<div class="network" onclick="c(this)">
<a href="#" class="network-left">
<img src="./wifi-strength-2.svg">
<span class="network-ssid">AP2</span>
</a>
</div>
<h3>WiFi Settings</h3>
<form method="GET" action="/wifisave">
<input id="ssid" name="ssid" length=32 placeholder="SSID"><br/>
<input id="psk" name="psk" length=64 type="password" placeholder="Password"><br/>
<br/>
<button type="submit">Save</button>
</form>
<br><hr>
<br>
<h1>OTA Update</h1>
<form method="POST" action="/update" enctype="multipart/form-data">
<input type="file" name="update">
<button type="submit">Update</button>
</form>
</div>
</body>
</html>

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z"/></svg>

After

Width:  |  Height:  |  Size: 307 B

View file

@ -0,0 +1,58 @@
* {
box-sizing: inherit;
}
div, input {
padding: 5px;
font-size: 1em;
}
input {
width: 95%;
}
body {
text-align: center;
font-family: sans-serif;
}
button {
border: 0;
border-radius: 0.3rem;
background-color: #1fa3ec;
color: #fff;
line-height: 2.4rem;
font-size: 1.2rem;
width: 100%;
padding: 0;
}
.main {
text-align: left;
display: inline-block;
min-width: 260px;
}
.network {
display: flex;
justify-content: space-between;
align-items: center;
}
.network-left {
display: flex;
align-items: center;
}
.network-ssid {
margin-bottom: -7px;
margin-left: 10px;
}
.info {
border: 1px solid;
margin: 10px 0px;
padding: 15px 10px;
color: #4f8a10;
background-color: #dff2bf;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.44A16.94 16.94 0 0 1 12 5z"/></svg>

After

Width:  |  Height:  |  Size: 268 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.44A16.94 16.94 0 0 1 12 5z"/></svg>

After

Width:  |  Height:  |  Size: 267 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2.4A16.94 16.94 0 0 1 12 5z"/></svg>

After

Width:  |  Height:  |  Size: 286 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3z"/></svg>

After

Width:  |  Height:  |  Size: 171 B

View file

@ -162,7 +162,6 @@ int32_t HOT interpret_index(int32_t index, int32_t size) {
}
void AddressableLight::call_setup() {
this->setup_internal_();
this->setup();
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE

View file

@ -149,9 +149,6 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai
}
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
void MQTTComponent::call_setup() {
// Call component internal setup.
this->setup_internal_();
if (this->is_internal())
return;
@ -173,8 +170,6 @@ void MQTTComponent::call_setup() {
}
void MQTTComponent::call_loop() {
this->loop_internal_();
if (this->is_internal())
return;

View file

@ -22,6 +22,8 @@ def to_code(config):
cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
yield cg.register_component(var, config)
if config[CONF_SAFE_MODE]:
cg.add(var.start_safe_mode())
@ -29,6 +31,3 @@ def to_code(config):
cg.add_library('Update', None)
elif CORE.is_esp32:
cg.add_library('Hash', None)
# Register at end for safe mode
yield cg.register_component(var, config)

View file

@ -358,7 +358,7 @@ void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
this->safe_mode_start_time_ = millis();
this->safe_mode_enable_time_ = enable_time;
this->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences.make_preference<uint32_t>(233825507UL);
this->rtc_ = global_preferences.make_preference<uint32_t>(233825507UL, false);
this->safe_mode_rtc_value_ = this->read_rtc_();
ESP_LOGCONFIG(TAG, "There have been %u suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_);
@ -369,19 +369,18 @@ void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) {
ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode.");
this->status_set_error();
network_setup();
this->call_setup();
this->set_timeout(enable_time, []() {
ESP_LOGE(TAG, "No OTA attempt made, restarting.");
App.reboot();
});
App.setup();
ESP_LOGI(TAG, "Waiting for OTA attempt.");
uint32_t begin = millis();
while ((millis() - begin) < enable_time) {
this->call_loop();
network_tick();
App.feed_wdt();
yield();
while (true) {
App.loop();
}
ESP_LOGE(TAG, "No OTA attempt made, restarting.");
App.reboot();
} else {
// increment counter
this->write_rtc_(this->safe_mode_rtc_value_ + 1);

View file

@ -12,7 +12,6 @@ static const char *TAG = "time";
RealTimeClock::RealTimeClock() = default;
void RealTimeClock::call_setup() {
this->setup_internal_();
setenv("TZ", this->timezone_.c_str(), 1);
tzset();
this->setup();

View file

@ -1,10 +1,11 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import web_server_base
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
from esphome.const import CONF_CSS_URL, CONF_ID, CONF_JS_URL, CONF_PORT
from esphome.core import CORE, coroutine_with_priority
from esphome.core import coroutine_with_priority
DEPENDENCIES = ['network']
AUTO_LOAD = ['json']
AUTO_LOAD = ['json', 'web_server_base']
web_server_ns = cg.esphome_ns.namespace('web_server')
WebServer = web_server_ns.class_('WebServer', cg.Component, cg.Controller)
@ -14,18 +15,18 @@ CONFIG_SCHEMA = cv.Schema({
cv.Optional(CONF_PORT, default=80): cv.port,
cv.Optional(CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css"): cv.string,
cv.Optional(CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js"): cv.string,
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase),
}).extend(cv.COMPONENT_SCHEMA)
@coroutine_with_priority(40.0)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
var = cg.new_Pvariable(config[CONF_ID], paren)
yield cg.register_component(var, config)
cg.add(var.set_port(config[CONF_PORT]))
cg.add(paren.set_port(config[CONF_PORT]))
cg.add(var.set_css_url(config[CONF_CSS_URL]))
cg.add(var.set_js_url(config[CONF_JS_URL]))
if CORE.is_esp32:
cg.add_library('FS', None)
cg.add_library('ESP Async WebServer', '1.1.1')

View file

@ -6,13 +6,6 @@
#include "StreamString.h"
#ifdef ARDUINO_ARCH_ESP32
#include <Update.h>
#endif
#ifdef ARDUINO_ARCH_ESP8266
#include <Updater.h>
#endif
#include <cstdlib>
#include <esphome/components/logger/logger.h>
@ -66,7 +59,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
void WebServer::setup() {
ESP_LOGCONFIG(TAG, "Setting up web server...");
this->server_ = new AsyncWebServer(this->port_);
this->base_->init();
this->events_.onConnect([this](AsyncEventSourceClient *client) {
// Configure reconnect timeout
@ -114,91 +107,18 @@ void WebServer::setup() {
logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); });
#endif
this->server_->addHandler(this);
this->server_->addHandler(&this->events_);
this->server_->begin();
this->base_->add_handler(&this->events_);
this->base_->add_handler(this);
this->base_->add_ota_handler();
this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); });
}
void WebServer::dump_config() {
ESP_LOGCONFIG(TAG, "Web Server:");
ESP_LOGCONFIG(TAG, " Address: %s:%u", network_get_address().c_str(), this->port_);
ESP_LOGCONFIG(TAG, " Address: %s:%u", network_get_address().c_str(), this->base_->get_port());
}
float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
void WebServer::handle_update_request(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response;
if (!Update.hasError()) {
response = request->beginResponse(200, "text/plain", "Update Successful!");
} else {
StreamString ss;
ss.print("Update Failed: ");
Update.printError(ss);
response = request->beginResponse(200, "text/plain", ss);
}
response->addHeader("Connection", "close");
request->send(response);
}
void report_ota_error() {
StreamString ss;
Update.printError(ss);
ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str());
}
void WebServer::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data,
size_t len, bool final) {
bool success;
if (index == 0) {
ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str());
this->ota_read_length_ = 0;
#ifdef ARDUINO_ARCH_ESP8266
Update.runAsync(true);
success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
#endif
#ifdef ARDUINO_ARCH_ESP32
if (Update.isRunning())
Update.abort();
success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
#endif
if (!success) {
report_ota_error();
return;
}
} else if (Update.hasError()) {
// don't spam logs with errors if something failed at start
return;
}
success = Update.write(data, len) == len;
if (!success) {
report_ota_error();
return;
}
this->ota_read_length_ += len;
const uint32_t now = millis();
if (now - this->last_ota_progress_ > 1000) {
if (request->contentLength() != 0) {
float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
} else {
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
}
this->last_ota_progress_ = now;
}
if (final) {
if (Update.end(true)) {
ESP_LOGI(TAG, "OTA update successful!");
this->set_timeout(100, []() { App.safe_reboot(); });
} else {
report_ota_error();
}
}
}
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/html");
std::string title = App.get_name() + " Web Server";
@ -248,7 +168,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
stream->print(F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/index.html\">ESPHome Web API</a> for "
"REST API documentation.</p>"
"<h2>OTA Update</h2><form method='POST' action=\"/update\" enctype=\"multipart/form-data\"><input "
"<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"
"<h2>Debug Log</h2><pre id=\"log\"></pre>"
"<script src=\""));
@ -531,9 +451,6 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) {
if (request->url() == "/")
return true;
if (request->url() == "/update" && request->method() == HTTP_POST)
return true;
UrlMatch match = match_url(request->url().c_str(), true);
if (!match.valid)
return false;
@ -575,11 +492,6 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
return;
}
if (request->url() == "/update") {
this->handle_update_request(request);
return;
}
UrlMatch match = match_url(request->url().c_str());
#ifdef USE_SENSOR
if (match.domain == "sensor") {

View file

@ -2,9 +2,9 @@
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/components/web_server_base/web_server_base.h"
#include <vector>
#include <ESPAsyncWebServer.h>
namespace esphome {
namespace web_server {
@ -28,8 +28,7 @@ struct UrlMatch {
*/
class WebServer : public Controller, public Component, public AsyncWebHandler {
public:
void set_port(uint16_t port) { port_ = port; }
WebServer(web_server_base::WebServerBase *base) : base_(base) {}
/** Set the URL to the CSS <link> that's sent to each client. Defaults to
* https://esphome.io/_static/webserver-v1.min.css
*
@ -57,8 +56,6 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
/// Handle an index request under '/'.
void handle_index_request(AsyncWebServerRequest *request);
void handle_update_request(AsyncWebServerRequest *request);
#ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj, float state) override;
/// Handle a sensor request under '/sensor/<id>'.
@ -122,19 +119,14 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
bool canHandle(AsyncWebServerRequest *request) override;
/// Override the web handler's handleRequest method.
void handleRequest(AsyncWebServerRequest *request) override;
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
bool final) override;
/// This web handle is not trivial.
bool isRequestHandlerTrivial() override;
protected:
uint16_t port_;
AsyncWebServer *server_;
web_server_base::WebServerBase *base_;
AsyncEventSource events_{"/events"};
const char *css_url_{nullptr};
const char *js_url_{nullptr};
uint32_t last_ota_progress_{0};
uint32_t ota_read_length_{0};
};
} // namespace web_server

View file

@ -0,0 +1,24 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import CONF_ID
from esphome.core import coroutine_with_priority, CORE
DEPENDENCIES = ['network']
web_server_base_ns = cg.esphome_ns.namespace('web_server_base')
WebServerBase = web_server_base_ns.class_('WebServerBase', cg.Component)
CONF_WEB_SERVER_BASE_ID = 'web_server_base_id'
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(WebServerBase),
})
@coroutine_with_priority(65.0)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
if CORE.is_esp32:
cg.add_library('FS', None)
cg.add_library('ESP Async WebServer', '1.1.1')

View file

@ -0,0 +1,96 @@
#include "web_server_base.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <StreamString.h>
#ifdef ARDUINO_ARCH_ESP32
#include <Update.h>
#endif
#ifdef ARDUINO_ARCH_ESP8266
#include <Updater.h>
#endif
namespace esphome {
namespace web_server_base {
static const char *TAG = "web_server_base";
void report_ota_error() {
StreamString ss;
Update.printError(ss);
ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str());
}
void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index,
uint8_t *data, size_t len, bool final) {
bool success;
if (index == 0) {
ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str());
this->ota_read_length_ = 0;
#ifdef ARDUINO_ARCH_ESP8266
Update.runAsync(true);
success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
#endif
#ifdef ARDUINO_ARCH_ESP32
if (Update.isRunning())
Update.abort();
success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH);
#endif
if (!success) {
report_ota_error();
return;
}
} else if (Update.hasError()) {
// don't spam logs with errors if something failed at start
return;
}
success = Update.write(data, len) == len;
if (!success) {
report_ota_error();
return;
}
this->ota_read_length_ += len;
const uint32_t now = millis();
if (now - this->last_ota_progress_ > 1000) {
if (request->contentLength() != 0) {
float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
} else {
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
}
this->last_ota_progress_ = now;
}
if (final) {
if (Update.end(true)) {
ESP_LOGI(TAG, "OTA update successful!");
this->parent_->set_timeout(100, []() { App.safe_reboot(); });
} else {
report_ota_error();
}
}
}
void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response;
if (!Update.hasError()) {
response = request->beginResponse(200, "text/plain", "Update Successful!");
} else {
StreamString ss;
ss.print("Update Failed: ");
Update.printError(ss);
response = request->beginResponse(200, "text/plain", ss);
}
response->addHeader("Connection", "close");
request->send(response);
}
void WebServerBase::add_ota_handler() { this->add_handler(new OTARequestHandler(this)); }
float WebServerBase::get_setup_priority() const {
// Before WiFi (captive portal)
return setup_priority::WIFI + 2.0f;
}
} // namespace web_server_base
} // namespace esphome

View file

@ -0,0 +1,76 @@
#pragma once
#include "esphome/core/component.h"
#include <ESPAsyncWebServer.h>
namespace esphome {
namespace web_server_base {
class WebServerBase : public Component {
public:
void init() {
if (this->initialized_) {
this->initialized_++;
return;
}
this->server_ = new AsyncWebServer(this->port_);
this->server_->begin();
for (auto *handler : this->handlers_)
this->server_->addHandler(handler);
this->initialized_++;
}
void deinit() {
this->initialized_--;
if (this->initialized_ == 0) {
delete this->server_;
this->server_ = nullptr;
}
}
AsyncWebServer *get_server() const { return server_; }
float get_setup_priority() const override;
void add_handler(AsyncWebHandler *handler) {
// remove all handlers
this->handlers_.push_back(handler);
if (this->server_ != nullptr)
this->server_->addHandler(handler);
}
void add_ota_handler();
void set_port(uint16_t port) { port_ = port; }
uint16_t get_port() const { return port_; }
protected:
friend class OTARequestHandler;
int initialized_{0};
uint16_t port_{80};
AsyncWebServer *server_{nullptr};
std::vector<AsyncWebHandler *> handlers_;
};
class OTARequestHandler : public AsyncWebHandler {
public:
OTARequestHandler(WebServerBase *parent) : parent_(parent) {}
void handleRequest(AsyncWebServerRequest *request) override;
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len,
bool final) override;
bool canHandle(AsyncWebServerRequest *request) override {
return request->url() == "/update" && request->method() == HTTP_POST;
}
bool isRequestHandlerTrivial() override { return false; }
protected:
uint32_t last_ota_progress_{0};
uint32_t ota_read_length_{0};
WebServerBase *parent_;
};
} // namespace web_server_base
} // namespace esphome

View file

@ -64,8 +64,9 @@ WIFI_NETWORK_BASE = cv.Schema({
cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA,
})
CONF_AP_TIMEOUT = 'ap_timeout'
WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend({
cv.Optional(CONF_AP_TIMEOUT, default='1min'): cv.positive_time_period_milliseconds,
})
WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({
@ -118,7 +119,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({
cv.Optional(CONF_AP): WIFI_NETWORK_AP,
cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name,
cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds,
cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds,
cv.Optional(CONF_POWER_SAVE_MODE, default='NONE'): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
@ -166,19 +167,20 @@ def wifi_network(config, static_ip):
@coroutine_with_priority(60.0)
def to_code(config):
rhs = WiFiComponent.new()
wifi = cg.Pvariable(config[CONF_ID], rhs)
cg.add(wifi.set_use_address(config[CONF_USE_ADDRESS]))
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
for network in config.get(CONF_NETWORKS, []):
cg.add(wifi.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP))))
cg.add(var.add_sta(wifi_network(network, config.get(CONF_MANUAL_IP))))
if CONF_AP in config:
cg.add(wifi.set_ap(wifi_network(config[CONF_AP], config.get(CONF_MANUAL_IP))))
conf = config[CONF_AP]
cg.add(var.set_ap(wifi_network(conf, config.get(CONF_MANUAL_IP))))
cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT]))
cg.add(wifi.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(wifi.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
cg.add(wifi.set_fast_connect(config[CONF_FAST_CONNECT]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT]))
if CORE.is_esp8266:
cg.add_library('ESP8266WiFi', None)
@ -186,7 +188,7 @@ def to_code(config):
cg.add_define('USE_WIFI')
# Register at end for OTA safe mode
yield cg.register_component(wifi, config)
yield cg.register_component(var, config)
@automation.register_condition('wifi.connected', WiFiConnectedCondition, cv.Schema({}))

View file

@ -18,6 +18,10 @@
#include "esphome/core/util.h"
#include "esphome/core/application.h"
#ifdef USE_CAPTIVE_PORTAL
#include "esphome/components/captive_portal/captive_portal.h"
#endif
namespace esphome {
namespace wifi {
@ -28,6 +32,8 @@ float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
void WiFiComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up WiFi...");
this->last_connected_ = millis();
this->wifi_register_callbacks_();
bool ret = this->wifi_mode_(this->has_sta(), false);
@ -99,6 +105,16 @@ void WiFiComponent::loop() {
break;
}
if (this->has_ap() && !this->ap_setup_) {
if (now - this->last_connected_ > this->ap_timeout_) {
ESP_LOGI(TAG, "Starting fallback AP!");
this->setup_ap_config_();
#ifdef USE_CAPTIVE_PORTAL
captive_portal::global_captive_portal->start();
#endif
}
}
if (!this->has_ap() && this->reboot_timeout_ != 0) {
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "Can't connect to WiFi, rebooting...");
@ -119,7 +135,7 @@ IPAddress WiFiComponent::get_ip_address() {
if (this->has_sta())
return this->wifi_sta_ip_();
if (this->has_ap())
return this->wifi_soft_ap_ip_();
return this->wifi_soft_ap_ip();
return {};
}
std::string WiFiComponent::get_use_address() const {
@ -147,7 +163,7 @@ void WiFiComponent::setup_ap_config_() {
}
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip_().toString().c_str());
ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().toString().c_str());
if (!this->has_sta()) {
this->state_ = WIFI_COMPONENT_STATE_AP;
@ -159,6 +175,10 @@ float WiFiComponent::get_loop_priority() const {
}
void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; }
void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
void WiFiComponent::set_sta(const WiFiAP &ap) {
this->sta_.clear();
this->add_sta(ap);
}
void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
ESP_LOGI(TAG, "WiFi Connecting to '%s'...", ap.get_ssid().c_str());
@ -377,10 +397,15 @@ void WiFiComponent::check_connecting_finished() {
wl_status_t status = this->wifi_sta_status_();
if (status == WL_CONNECTED) {
ESP_LOGI(TAG, "WiFi connected!");
ESP_LOGI(TAG, "WiFi Connected!");
this->print_connect_params_();
if (this->has_ap()) {
#ifdef USE_CAPTIVE_PORTAL
if (this->is_captive_portal_active_()) {
captive_portal::global_captive_portal->end();
}
#endif
ESP_LOGD(TAG, "Disabling AP...");
this->wifi_mode_({}, false);
}
@ -426,7 +451,7 @@ void WiFiComponent::check_connecting_finished() {
void WiFiComponent::retry_connect() {
delay(10);
if (this->num_retried_ > 5 || this->error_from_callback_) {
if (!this->is_captive_portal_active_() && (this->num_retried_ > 5 || this->error_from_callback_)) {
// If retry failed for more than 5 times, let's restart STA
ESP_LOGW(TAG, "Restarting WiFi adapter...");
this->wifi_mode_(false, {});
@ -443,9 +468,6 @@ void WiFiComponent::retry_connect() {
return;
}
if (this->has_ap()) {
this->setup_ap_config_();
}
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
this->action_started_ = millis();
}
@ -461,11 +483,6 @@ bool WiFiComponent::is_connected() {
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_status_() == WL_CONNECTED &&
!this->error_from_callback_;
}
bool WiFiComponent::ready_for_ota() {
if (this->has_ap())
return true;
return this->is_connected();
}
void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; }
std::string WiFiComponent::format_mac_addr(const uint8_t *mac) {
@ -473,20 +490,12 @@ std::string WiFiComponent::format_mac_addr(const uint8_t *mac) {
sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return buf;
}
bool sta_field_equal(const uint8_t *field_a, const uint8_t *field_b, int len) {
for (int i = 0; i < len; i++) {
uint8_t a = field_a[i];
uint8_t b = field_b[i];
if (a == b && a == 0)
break;
if (a == b)
continue;
return false;
}
return true;
bool WiFiComponent::is_captive_portal_active_() {
#ifdef USE_CAPTIVE_PORTAL
return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active();
#else
return false;
#endif
}
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }

View file

@ -122,6 +122,7 @@ class WiFiComponent : public Component {
/// Construct a WiFiComponent.
WiFiComponent();
void set_sta(const WiFiAP &ap);
void add_sta(const WiFiAP &ap);
/** Setup an Access Point that should be created if no connection to a station can be made.
@ -137,6 +138,7 @@ class WiFiComponent : public Component {
void check_scanning_finished();
void start_connecting(const WiFiAP &ap, bool two);
void set_fast_connect(bool fast_connect);
void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; }
void check_connecting_finished();
@ -144,8 +146,6 @@ class WiFiComponent : public Component {
bool can_proceed() override;
bool ready_for_ota();
void set_reboot_timeout(uint32_t reboot_timeout);
bool is_connected();
@ -171,6 +171,10 @@ class WiFiComponent : public Component {
std::string get_use_address() const;
void set_use_address(const std::string &use_address);
const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
IPAddress wifi_soft_ap_ip();
protected:
static std::string format_mac_addr(const uint8_t mac[6]);
void setup_ap_config_();
@ -188,7 +192,8 @@ class WiFiComponent : public Component {
bool wifi_scan_start_();
bool wifi_ap_ip_config_(optional<ManualIP> manual_ip);
bool wifi_start_ap_(const WiFiAP &ap);
IPAddress wifi_soft_ap_ip_();
bool is_captive_portal_active_();
#ifdef ARDUINO_ARCH_ESP8266
static void wifi_event_callback(System_Event_t *event);
@ -211,7 +216,8 @@ class WiFiComponent : public Component {
uint32_t action_started_;
uint8_t num_retried_{0};
uint32_t last_connected_{0};
uint32_t reboot_timeout_{300000};
uint32_t reboot_timeout_{};
uint32_t ap_timeout_{};
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
bool error_from_callback_{false};
std::vector<WiFiScanResult> scan_result_;

View file

@ -359,7 +359,7 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i
}
case SYSTEM_EVENT_AP_PROBEREQRECVED: {
auto it = info.ap_probereqrecved;
ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
break;
}
default:
@ -517,7 +517,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true;
}
IPAddress WiFiComponent::wifi_soft_ap_ip_() {
IPAddress WiFiComponent::wifi_soft_ap_ip() {
tcpip_adapter_ip_info_t ip;
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip);
return IPAddress(ip.ip.addr);

View file

@ -382,7 +382,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
}
case EVENT_SOFTAPMODE_PROBEREQRECVED: {
auto it = event->event_info.ap_probereqrecved;
ESP_LOGV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
break;
}
#ifndef ARDUINO_ESP8266_RELEASE_2_3_0
@ -583,7 +583,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true;
}
IPAddress WiFiComponent::wifi_soft_ap_ip_() {
IPAddress WiFiComponent::wifi_soft_ap_ip() {
struct ip_info ip {};
wifi_get_ip_info(SOFTAP_IF, &ip);
return {ip.ip.addr};

View file

@ -33,10 +33,8 @@ void Application::setup() {
for (uint32_t i = 0; i < this->components_.size(); i++) {
Component *component = this->components_[i];
if (component->is_failed())
continue;
component->call_setup();
component->call();
this->scheduler.process_to_add();
if (component->can_proceed())
continue;
@ -48,9 +46,7 @@ void Application::setup() {
uint32_t new_app_state = STATUS_LED_WARNING;
this->scheduler.call();
for (uint32_t j = 0; j <= i; j++) {
if (!this->components_[j]->is_failed()) {
this->components_[j]->call_loop();
}
this->components_[j]->call();
new_app_state |= this->components_[j]->get_component_state();
this->app_state_ |= new_app_state;
}
@ -68,9 +64,7 @@ void Application::loop() {
this->scheduler.call();
for (Component *component : this->components_) {
if (!component->is_failed()) {
component->call_loop();
}
component->call();
new_app_state |= component->get_component_state();
this->app_state_ |= new_app_state;
this->feed_wdt();
@ -125,7 +119,7 @@ void ICACHE_RAM_ATTR HOT Application::feed_wdt() {
LAST_FEED = now;
#ifdef USE_STATUS_LED
if (status_led::global_status_led != nullptr) {
status_led::global_status_led->call_loop();
status_led::global_status_led->call();
}
#endif
}

View file

@ -58,23 +58,35 @@ bool Component::cancel_timeout(const std::string &name) { // NOLINT
return App.scheduler.cancel_timeout(this, name);
}
void Component::call_loop() {
this->loop_internal_();
this->loop();
}
void Component::call_loop() { this->loop(); }
void Component::call_setup() {
this->setup_internal_();
this->setup();
}
void Component::call_setup() { this->setup(); }
uint32_t Component::get_component_state() const { return this->component_state_; }
void Component::loop_internal_() {
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_LOOP;
}
void Component::setup_internal_() {
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_SETUP;
void Component::call() {
uint32_t state = this->component_state_ & COMPONENT_STATE_MASK;
switch (state) {
case COMPONENT_STATE_CONSTRUCTION:
// State Construction: Call setup and set state to setup
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_SETUP;
this->call_setup();
break;
case COMPONENT_STATE_SETUP:
// State setup: Call first loop and set state to loop
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_LOOP;
this->call_loop();
break;
case COMPONENT_STATE_LOOP:
// State loop: Call loop
this->call_loop();
break;
case COMPONENT_STATE_FAILED:
// State failed: Do nothing
break;
default:
break;
}
}
void Component::mark_failed() {
ESP_LOGE(TAG, "Component was marked as failed.");
@ -130,9 +142,6 @@ void Component::set_setup_priority(float priority) { this->setup_priority_overri
PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {}
void PollingComponent::call_setup() {
// Call component internal setup.
this->setup_internal_();
// Let the polling component subclass setup their HW.
this->setup();

View file

@ -91,18 +91,7 @@ class Component {
*/
virtual float get_loop_priority() const;
/** Public loop() functions. These will be called by the Application instance.
*
* Note: This should normally not be overriden, unless you know what you're doing.
* They're basically to make creating custom components easier. For example the
* SensorComponent can override these methods to not have the user call some super
* methods within their custom sensors. These methods should ALWAYS call the loop_internal()
* and setup_internal() methods.
*
* Basically, it handles stuff like interval/timeout functions and eventually calls loop().
*/
virtual void call_loop();
virtual void call_setup();
void call();
virtual void on_shutdown() {}
virtual void on_safe_shutdown() {}
@ -138,6 +127,8 @@ class Component {
void status_momentary_error(const std::string &name, uint32_t length = 5000);
protected:
virtual void call_loop();
virtual void call_setup();
/** Set an interval function with a unique name. Empty name means no cancelling possible.
*
* This will call f every interval ms. Can be cancelled via CancelInterval().
@ -204,9 +195,6 @@ class Component {
/// Cancel a defer callback using the specified name, name must not be empty.
bool cancel_defer(const std::string &name); // NOLINT
void loop_internal_();
void setup_internal_();
uint32_t component_state_{0x0000}; ///< State of this component.
float setup_priority_override_{NAN};
};

View file

@ -22,3 +22,4 @@
#endif
#define USE_TIME
#define USE_DEEP_SLEEP
#define USE_CAPTIVE_PORTAL

View file

@ -2,7 +2,7 @@
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP8266_PREFERENCES_FLASH
#ifdef ARDUINO_ARCH_ESP8266
extern "C" {
#include "spi_flash.h"
}
@ -12,9 +12,9 @@ namespace esphome {
static const char *TAG = "preferences";
ESPPreferenceObject::ESPPreferenceObject() : rtc_offset_(0), length_words_(0), type_(0), data_(nullptr) {}
ESPPreferenceObject::ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type)
: rtc_offset_(rtc_offset), length_words_(length), type_(type) {
ESPPreferenceObject::ESPPreferenceObject() : offset_(0), length_words_(0), type_(0), data_(nullptr) {}
ESPPreferenceObject::ESPPreferenceObject(size_t offset, size_t length, uint32_t type)
: offset_(offset), length_words_(length), type_(type) {
this->data_ = new uint32_t[this->length_words_ + 1];
for (uint32_t i = 0; i < this->length_words_ + 1; i++)
this->data_[i] = 0;
@ -29,7 +29,7 @@ bool ESPPreferenceObject::load_() {
bool valid = this->data_[this->length_words_] == this->calculate_crc_();
ESP_LOGVV(TAG, "LOAD %u: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_, // NOLINT
ESP_LOGVV(TAG, "LOAD %u: valid=%s, 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_, // NOLINT
YESNO(valid), this->data_[0], this->data_[1], this->type_, this->calculate_crc_());
return valid;
}
@ -42,7 +42,7 @@ bool ESPPreferenceObject::save_() {
this->data_[this->length_words_] = this->calculate_crc_();
if (!this->save_internal_())
return false;
ESP_LOGVV(TAG, "SAVE %u: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->rtc_offset_, // NOLINT
ESP_LOGVV(TAG, "SAVE %u: 0=0x%08X 1=0x%08X (Type=%u, CRC=0x%08X)", this->offset_, // NOLINT
this->data_[0], this->data_[1], this->type_, this->calculate_crc_());
return true;
}
@ -54,6 +54,12 @@ bool ESPPreferenceObject::save_() {
#define ESP_RTC_USER_MEM_SIZE_WORDS 128
#define ESP_RTC_USER_MEM_SIZE_BYTES ESP_RTC_USER_MEM_SIZE_WORDS * 4
#ifdef USE_ESP8266_PREFERENCES_FLASH
#define ESP8266_FLASH_STORAGE_SIZE 128
#else
#define ESP8266_FLASH_STORAGE_SIZE 64
#endif
static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
return false;
@ -62,9 +68,7 @@ static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
return true;
}
#ifdef USE_ESP8266_PREFERENCES_FLASH
static bool esp8266_preferences_modified = false;
#endif
static bool esp8266_flash_dirty = false;
static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
@ -75,29 +79,24 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
}
auto *ptr = &ESP_RTC_USER_MEM[index];
#ifdef USE_ESP8266_PREFERENCES_FLASH
if (*ptr != value) {
esp8266_preferences_modified = true;
}
#endif
*ptr = value;
return true;
}
#ifdef USE_ESP8266_PREFERENCES_FLASH
extern "C" uint32_t _SPIFFS_end;
static const uint32_t get_esp8266_flash_sector() { return (uint32_t(&_SPIFFS_end) - 0x40200000) / SPI_FLASH_SEC_SIZE; }
static const uint32_t get_esp8266_flash_sector() {
union {
uint32_t *ptr;
uint32_t uint;
} data{};
data.ptr = &_SPIFFS_end;
return (data.uint - 0x40200000) / SPI_FLASH_SEC_SIZE;
}
static const uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; }
static void load_esp8266_flash() {
ESP_LOGVV(TAG, "Loading preferences from flash...");
disable_interrupts();
spi_flash_read(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES);
enable_interrupts();
}
static void save_esp8266_flash() {
if (!esp8266_preferences_modified)
void ESPPreferences::save_esp8266_flash_() {
if (!esp8266_flash_dirty)
return;
ESP_LOGVV(TAG, "Saving preferences to flash...");
@ -109,31 +108,53 @@ static void save_esp8266_flash() {
return;
}
auto write_res = spi_flash_write(get_esp8266_flash_address(), ESP_RTC_USER_MEM, ESP_RTC_USER_MEM_SIZE_BYTES);
auto write_res = spi_flash_write(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4);
enable_interrupts();
if (write_res != SPI_FLASH_RESULT_OK) {
ESP_LOGV(TAG, "Write ESP8266 flash failed!");
return;
}
esp8266_preferences_modified = false;
esp8266_flash_dirty = false;
}
#endif
bool ESPPreferenceObject::save_internal_() {
if (this->in_flash_) {
for (uint32_t i = 0; i <= this->length_words_; i++) {
uint32_t j = this->offset_ + i;
if (j >= ESP8266_FLASH_STORAGE_SIZE)
return false;
uint32_t v = this->data_[i];
uint32_t *ptr = &global_preferences.flash_storage_[j];
if (*ptr != v)
esp8266_flash_dirty = true;
*ptr = v;
}
global_preferences.save_esp8266_flash_();
return true;
}
for (uint32_t i = 0; i <= this->length_words_; i++) {
if (!esp_rtc_user_mem_write(this->rtc_offset_ + i, this->data_[i]))
if (!esp_rtc_user_mem_write(this->offset_ + i, this->data_[i]))
return false;
}
#ifdef USE_ESP8266_PREFERENCES_FLASH
save_esp8266_flash();
#endif
return true;
}
bool ESPPreferenceObject::load_internal_() {
if (this->in_flash_) {
for (uint32_t i = 0; i <= this->length_words_; i++) {
uint32_t j = this->offset_ + i;
if (j >= ESP8266_FLASH_STORAGE_SIZE)
return false;
this->data_[i] = global_preferences.flash_storage_[j];
}
return true;
}
for (uint32_t i = 0; i <= this->length_words_; i++) {
if (!esp_rtc_user_mem_read(this->rtc_offset_ + i, &this->data_[i]))
if (!esp_rtc_user_mem_read(this->offset_ + i, &this->data_[i]))
return false;
}
return true;
@ -145,12 +166,25 @@ ESPPreferences::ESPPreferences()
: current_offset_(0) {}
void ESPPreferences::begin(const std::string &name) {
#ifdef USE_ESP8266_PREFERENCES_FLASH
load_esp8266_flash();
#endif
this->flash_storage_ = new uint32_t[ESP8266_FLASH_STORAGE_SIZE];
ESP_LOGVV(TAG, "Loading preferences from flash...");
disable_interrupts();
spi_flash_read(get_esp8266_flash_address(), this->flash_storage_, ESP8266_FLASH_STORAGE_SIZE * 4);
enable_interrupts();
}
ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) {
ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) {
if (in_flash) {
uint32_t start = this->current_flash_offset_;
uint32_t end = start + length + 1;
if (end > ESP8266_FLASH_STORAGE_SIZE)
return {};
auto pref = ESPPreferenceObject(start, length, type);
pref.in_flash_ = true;
this->current_flash_offset_ = end;
return pref;
}
uint32_t start = this->current_offset_;
uint32_t end = start + length + 1;
bool in_normal = start < 96;
@ -165,7 +199,7 @@ ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type
if (end > 128) {
// Doesn't fit in data, return uninitialized preference obj.
return ESPPreferenceObject();
return {};
}
uint32_t rtc_offset;
@ -186,7 +220,7 @@ bool ESPPreferences::is_prevent_write() { return this->prevent_write_; }
#ifdef ARDUINO_ARCH_ESP32
bool ESPPreferenceObject::save_internal_() {
char key[32];
sprintf(key, "%u", this->rtc_offset_);
sprintf(key, "%u", this->offset_);
uint32_t len = (this->length_words_ + 1) * 4;
size_t ret = global_preferences.preferences_.putBytes(key, this->data_, len);
if (ret != len) {
@ -197,7 +231,7 @@ bool ESPPreferenceObject::save_internal_() {
}
bool ESPPreferenceObject::load_internal_() {
char key[32];
sprintf(key, "%u", this->rtc_offset_);
sprintf(key, "%u", this->offset_);
uint32_t len = (this->length_words_ + 1) * 4;
size_t ret = global_preferences.preferences_.getBytes(key, this->data_, len);
if (ret != len) {
@ -213,7 +247,7 @@ void ESPPreferences::begin(const std::string &name) {
this->preferences_.begin(key.c_str());
}
ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type) {
ESPPreferenceObject ESPPreferences::make_preference(size_t length, uint32_t type, bool in_flash) {
auto pref = ESPPreferenceObject(this->current_offset_, length, type);
this->current_offset_++;
return pref;

View file

@ -7,13 +7,14 @@
#endif
#include "esphome/core/esphal.h"
#include "esphome/core/defines.h"
namespace esphome {
class ESPPreferenceObject {
public:
ESPPreferenceObject();
ESPPreferenceObject(size_t rtc_offset, size_t length, uint32_t type);
ESPPreferenceObject(size_t offset, size_t length, uint32_t type);
template<typename T> bool save(T *src);
@ -22,6 +23,8 @@ class ESPPreferenceObject {
bool is_initialized() const;
protected:
friend class ESPPreferences;
bool save_();
bool load_();
bool save_internal_();
@ -29,18 +32,33 @@ class ESPPreferenceObject {
uint32_t calculate_crc_() const;
size_t rtc_offset_;
size_t offset_;
size_t length_words_;
uint32_t type_;
uint32_t *data_;
#ifdef ARDUINO_ARCH_ESP8266
bool in_flash_{false};
#endif
};
#ifdef ARDUINO_ARCH_ESP8266
#ifdef USE_ESP8266_PREFERENCES_FLASH
static bool DEFAULT_IN_FLASH = true;
#else
static bool DEFAULT_IN_FLASH = false;
#endif
#endif
#ifdef ARDUINO_ARCH_ESP32
static bool DEFAULT_IN_FLASH = true;
#endif
class ESPPreferences {
public:
ESPPreferences();
void begin(const std::string &name);
ESPPreferenceObject make_preference(size_t length, uint32_t type);
template<typename T> ESPPreferenceObject make_preference(uint32_t type);
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash = DEFAULT_IN_FLASH);
template<typename T> ESPPreferenceObject make_preference(uint32_t type, bool in_flash = DEFAULT_IN_FLASH);
#ifdef ARDUINO_ARCH_ESP8266
/** On the ESP8266, we can't override the first 128 bytes during OTA uploads
@ -62,14 +80,17 @@ class ESPPreferences {
Preferences preferences_;
#endif
#ifdef ARDUINO_ARCH_ESP8266
void save_esp8266_flash_();
bool prevent_write_{false};
uint32_t *flash_storage_;
uint32_t current_flash_offset_;
#endif
};
extern ESPPreferences global_preferences;
template<typename T> ESPPreferenceObject ESPPreferences::make_preference(uint32_t type) {
return this->make_preference((sizeof(T) + 3) / 4, type);
template<typename T> ESPPreferenceObject ESPPreferences::make_preference(uint32_t type, bool in_flash) {
return this->make_preference((sizeof(T) + 3) / 4, type, in_flash);
}
template<typename T> bool ESPPreferenceObject::save(T *src) {

View file

@ -38,50 +38,6 @@ bool network_is_connected() {
return false;
}
void network_setup() {
bool ready = true;
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr) {
ethernet::global_eth_component->call_setup();
ready = false;
}
#endif
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr) {
wifi::global_wifi_component->call_setup();
ready = false;
}
#endif
while (!ready) {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr) {
ethernet::global_eth_component->call_loop();
ready = ready || ethernet::global_eth_component->can_proceed();
}
#endif
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr) {
wifi::global_wifi_component->call_loop();
ready = ready || wifi::global_wifi_component->can_proceed();
}
#endif
App.feed_wdt();
}
}
void network_tick() {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr)
ethernet::global_eth_component->call_loop();
#endif
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr)
wifi::global_wifi_component->call_loop();
#endif
}
void network_setup_mdns() {
MDNS.begin(App.get_name().c_str());
#ifdef USE_API

View file

@ -10,8 +10,6 @@ bool network_is_connected();
std::string network_get_address();
/// Manually set up the network stack (outside of the App.setup() loop, for example in OTA safe mode)
void network_setup();
void network_tick();
void network_setup_mdns();
void network_tick_mdns();

View file

@ -370,14 +370,14 @@ def _list_dashboard_entries():
class DashboardEntry(object):
def __init__(self, filename):
self.filename = filename
def __init__(self, path):
self.path = path
self._storage = None
self._loaded_storage = False
@property
def full_path(self): # type: () -> str
return os.path.join(settings.config_dir, self.filename)
def filename(self):
return os.path.basename(self.path)
@property
def storage(self): # type: () -> Optional[StorageJSON]

View file

@ -82,7 +82,7 @@
<span class="status-indicator unknown" data-node="{{ entry.filename }}">
<span class="status-indicator-icon"></span>
<span class="status-indicator-text"></span></span>.
Full path: <code class="inlinecode">{{ escape(entry.full_path) }}</code>
Full path: <code class="inlinecode">{{ escape(entry.path) }}</code>
</p>
{% if entry.update_available %}
<p class="update-available" data-node="{{ entry.filename }}">

View file

@ -2,6 +2,8 @@ from __future__ import print_function
import codecs
import os
import random
import string
import unicodedata
import voluptuous as vol
@ -52,6 +54,13 @@ wifi:
ssid: "{ssid}"
password: "{psk}"
# Enable fallback network (captive portal)
ap:
ssid: "{name} Fallback AP"
password: "{fallback_psk}"
captive_portal:
# Enable logging
logger:
@ -65,6 +74,9 @@ def sanitize_double_quotes(value):
def wizard_file(**kwargs):
letters = string.ascii_letters + string.digits
kwargs['fallback_psk'] = ''.join(random.choice(letters) for _ in range(12))
config = BASE_CONFIG.format(**kwargs)
if kwargs['password']:
@ -117,8 +129,8 @@ def default_input(text, default):
# From https://stackoverflow.com/a/518232/8924614
def strip_accents(string):
return u''.join(c for c in unicodedata.normalize('NFD', text_type(string))
def strip_accents(value):
return u''.join(c for c in unicodedata.normalize('NFD', text_type(value))
if unicodedata.category(c) != 'Mn')

View file

@ -30,7 +30,7 @@ EXECUTABLE_BIT = {
files = [s[3].strip() for s in lines]
files.sort()
file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico',
file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', '.svg',
'.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg')
cpp_include = ('*.h', '*.c', '*.cpp', '*.tcc')
ignore_types = ('.ico',)
@ -104,7 +104,7 @@ def lint_ino(fname):
@lint_file_check(exclude=['*{}'.format(f) for f in file_types] + [
'.clang-*', '.dockerignore', '.editorconfig', '*.gitignore', 'LICENSE', 'pylintrc',
'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*'
'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*',
])
def lint_ext_check(fname):
return "This file extension is not a registered file type. If this is an error, please " \
@ -135,7 +135,7 @@ def lint_newline(fname):
return "File contains windows newline. Please set your editor to unix newline mode."
@lint_content_check()
@lint_content_check(exclude=['*.svg'])
def lint_end_newline(fname, content):
if content and not content.endswith('\n'):
return "File does not end with a newline, please add an empty line at the end of the file."
@ -164,7 +164,8 @@ def relative_py_search_text(fname, content):
return 'esphome.components.{}'.format(integration)
@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'])
@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'],
exclude=['esphome/components/web_server/__init__.py'])
def lint_relative_py_import(fname):
return ("Component contains absolute import - Components must always use "
"relative imports within the integration.\n"