aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--PLF10 Manual.pdfbin0 -> 370065 bytes
-rw-r--r--captive_portal.cpp160
-rw-r--r--kauf-plug.binbin0 -> 460048 bytes
-rw-r--r--kauf-plug.bin.gzbin0 -> 314329 bytes
-rw-r--r--kauf-plug.yaml218
-rw-r--r--web_server.cpp840
-rw-r--r--web_server_base.cpp114
7 files changed, 1332 insertions, 0 deletions
diff --git a/PLF10 Manual.pdf b/PLF10 Manual.pdf
new file mode 100644
index 0000000..a0897e7
--- /dev/null
+++ b/PLF10 Manual.pdf
Binary files differ
diff --git a/captive_portal.cpp b/captive_portal.cpp
new file mode 100644
index 0000000..e00e7cc
--- /dev/null
+++ b/captive_portal.cpp
@@ -0,0 +1,160 @@
+#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 *const 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>"));
+
+ // KAUF edit
+ // add warning about not flashing tasmota-minimal.
+ stream->print(F("<h1>OTA Update: </h1>"
+ "<br />**** DO NOT USE <b>TASMOTA-MINIMAL</b>.BIN or .BIN.GZ. **** Use tasmota.bin.gz."
+ "<form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
+ "type=\"file\" name=\"update\"><button type=\"submit\">Update</button></form>"));
+ // KAUF edit end
+
+ 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());
+ wifi::global_wifi_component->save_wifi_sta(ssid, psk);
+ request->redirect("/?save=true");
+}
+
+void CaptivePortal::setup() {}
+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;
+}
+void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); }
+
+CaptivePortal *global_captive_portal = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+
+} // namespace captive_portal
+} // namespace esphome
diff --git a/kauf-plug.bin b/kauf-plug.bin
new file mode 100644
index 0000000..d17fe0e
--- /dev/null
+++ b/kauf-plug.bin
Binary files differ
diff --git a/kauf-plug.bin.gz b/kauf-plug.bin.gz
new file mode 100644
index 0000000..020c0d2
--- /dev/null
+++ b/kauf-plug.bin.gz
Binary files differ
diff --git a/kauf-plug.yaml b/kauf-plug.yaml
new file mode 100644
index 0000000..f3fbfc0
--- /dev/null
+++ b/kauf-plug.yaml
@@ -0,0 +1,218 @@
+substitutions:
+
+ # **** CHANGE FRIENDLY NAME TO SOMETHING UNIQUE PER DEVICE ****
+ friendly_name: Kauf Plug
+
+
+
+esphome:
+
+ # **** CHANGE DEVICE NAME TO SOMETHING UNIQUE PER DEVICE ****
+ name: kauf-plug # **** RENAME YAML FILE TO SAME NAME ****
+
+ platform: ESP8266
+ board: esp01_1m
+
+ esp8266_restore_from_flash: true
+
+ # remove next line if renaming to something unique above
+ name_add_mac_suffix: true
+
+
+ project:
+ name: "kauf.plf10"
+ version: "1.5"
+
+wifi:
+ ssid: initial_ap2
+ password: asdfasdfasdfasdf
+
+ # Uncomment below to set a static IP
+ # manual_ip:
+ # static_ip: !secret kauf_bulb_ip_address
+ # gateway: !secret wifi_gateway
+ # subnet: !secret wifi_subnet
+ # dns1: !secret wifi_dns1
+
+ # use_address allows wireless programming through dashboard. remove after programming.
+ # use_address: 192.168.1.3
+
+ # default is 20, 17 is recommended.
+ output_power: 17
+
+
+ ap:
+ ssid: ${friendly_name} Hotspot
+ ap_timeout: 10s
+
+
+captive_portal: # for fallback wifi hotspot
+
+logger: # Enable logging
+# baud_rate: 0 # Disable UART logging since TX pad not easily available
+
+api: # Enable Home Assistant API
+ # password: !secret api_password # optional password field for Home Assistant API.
+
+ota:
+ # password: !secret ota_password # optional password for OTA updates.
+
+
+debug: # outputs additional debug info when logs start
+
+
+
+web_server: # web server allows access to device with a web browser
+ # auth: # optional login details for web interface
+ # username: admin
+ # password: !secret web_server_password
+
+
+
+
+# red led, blink when not connected to wifi or Home Assistant
+status_led:
+ pin:
+ number: GPIO0
+ inverted: true
+
+
+
+
+
+binary_sensor:
+
+ # button input toggles relay and thereby blue led
+ - platform: gpio
+ id: button
+ pin:
+ number: GPIO13
+ mode: INPUT_PULLUP
+ inverted: true
+ on_press:
+ then:
+ - switch.toggle: relay
+
+ - platform: template
+ id: in_use
+ name: ${friendly_name} Device In Use
+
+
+
+
+
+switch:
+
+ # blue LED follows relay power state
+ - platform: gpio
+ id: blue_led
+ pin:
+ number: GPIO2
+ inverted: true
+
+ # relay output
+ - platform: gpio
+ id: relay
+ name: $friendly_name
+ pin: GPIO4
+
+ # automatically make blue led equal relay state
+ on_turn_on:
+ - switch.turn_on: blue_led
+ on_turn_off:
+ - switch.turn_off: blue_led
+
+
+
+# clock input from Home Assistant used for total daily energy
+time:
+ - platform: homeassistant
+ id: homeassistant_time
+
+
+
+sensor: # Power monitoring sensors output to Home Assistant
+ - platform: hlw8012
+ sel_pin:
+ number: GPIO12
+ inverted: True
+ cf_pin: GPIO5
+ cf1_pin: GPIO14
+ current_resistor: 0.001 # The value of the shunt resistor for current measurement.
+ voltage_divider: 2401 # The value of the voltage divider on the board as (R_upstream + R_downstream) / R_downstream.
+ power:
+ name: ${friendly_name} Power
+ unit_of_measurement: W
+ id: wattage
+ filters:
+ - calibrate_linear:
+ - 0.0 -> 0.0
+ - 333.8 -> 60 # value with 60W bulb.
+ on_value: # set or clear in_use template binary sensor depending on whether power usage is over threshold
+ - if:
+ condition:
+ lambda: 'return (x >= id(threshold).state);'
+ then:
+ - binary_sensor.template.publish:
+ id: in_use
+ state: ON
+ else:
+ - binary_sensor.template.publish:
+ id: in_use
+ state: OFF
+ current:
+ name: ${friendly_name} Current
+ unit_of_measurement: A
+ filters:
+ - calibrate_linear:
+ - 0.0 -> 0.0
+ - 0.6 -> 0.515 # value with 60W bulb.
+ voltage:
+ name: ${friendly_name} Voltage
+ unit_of_measurement: V
+ filters:
+ - calibrate_linear:
+ - 0.0 -> 0.0
+ - 302.1 -> 117.1 # Tested using a meter
+ change_mode_every: 1
+ update_interval: 10s # 20 second effective update rate for Power, 40 second for Current and Voltage.
+
+# Reports the total Power so-far each day, resets at midnight, see https://esphome.io/components/sensor/total_daily_energy.html
+ - platform: total_daily_energy
+ name: ${friendly_name} Total Daily Energy
+ power_id: wattage
+ filters:
+ - multiply: 0.001 ## convert Wh to kWh
+ unit_of_measurement: kWh
+
+
+number:
+ - platform: template
+ name: ${friendly_name} Use Threshold
+ min_value: 1
+ max_value: 100
+ step: 1
+ initial_value: 2
+ id: threshold
+ optimistic: true # required for changing value from home assistant
+ restore_value: true
+ on_value:
+ - if: # set or clear in_use template binary sensor depending on whether power usage is above threshold
+ condition:
+ lambda: 'return (id(wattage).state >= x);'
+ then:
+ - binary_sensor.template.publish:
+ id: in_use
+ state: ON
+ else:
+ - binary_sensor.template.publish:
+ id: in_use
+ state: OFF
+
+
+
+# Send IP Address to HA
+text_sensor:
+ - platform: wifi_info
+ ip_address:
+ name: $friendly_name IP Address \ No newline at end of file
diff --git a/web_server.cpp b/web_server.cpp
new file mode 100644
index 0000000..f652b3b
--- /dev/null
+++ b/web_server.cpp
@@ -0,0 +1,840 @@
+#include "web_server.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include "esphome/core/util.h"
+#include "esphome/components/json/json_util.h"
+
+#include "StreamString.h"
+
+#include <cstdlib>
+
+#ifdef USE_LIGHT
+#include "esphome/components/light/light_json_schema.h"
+#endif
+
+#ifdef USE_LOGGER
+#include <esphome/components/logger/logger.h>
+#endif
+
+#ifdef USE_FAN
+#include "esphome/components/fan/fan_helpers.h"
+#endif
+
+namespace esphome {
+namespace web_server {
+
+static const char *const TAG = "web_server";
+
+void write_row(AsyncResponseStream *stream, Nameable *obj, const std::string &klass, const std::string &action) {
+ if (obj->is_internal())
+ return;
+ stream->print("<tr class=\"");
+ stream->print(klass.c_str());
+ stream->print("\" id=\"");
+ stream->print(klass.c_str());
+ stream->print("-");
+ stream->print(obj->get_object_id().c_str());
+ stream->print("\"><td>");
+ stream->print(obj->get_name().c_str());
+ stream->print("</td><td></td><td>");
+ stream->print(action.c_str());
+ stream->print("</td>");
+ stream->print("</tr>");
+}
+
+UrlMatch match_url(const std::string &url, bool only_domain = false) {
+ UrlMatch match;
+ match.valid = false;
+ size_t domain_end = url.find('/', 1);
+ if (domain_end == std::string::npos)
+ return match;
+ match.domain = url.substr(1, domain_end - 1);
+ if (only_domain) {
+ match.valid = true;
+ return match;
+ }
+ if (url.length() == domain_end - 1)
+ return match;
+ size_t id_begin = domain_end + 1;
+ size_t id_end = url.find('/', id_begin);
+ match.valid = true;
+ if (id_end == std::string::npos) {
+ match.id = url.substr(id_begin, url.length() - id_begin);
+ return match;
+ }
+ match.id = url.substr(id_begin, id_end - id_begin);
+ size_t method_begin = id_end + 1;
+ match.method = url.substr(method_begin, url.length() - method_begin);
+ return match;
+}
+
+void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; }
+void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
+void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
+void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
+
+void WebServer::setup() {
+ ESP_LOGCONFIG(TAG, "Setting up web server...");
+ this->setup_controller();
+ this->base_->init();
+
+ this->events_.onConnect([this](AsyncEventSourceClient *client) {
+ // Configure reconnect timeout
+ client->send("", "ping", millis(), 30000);
+
+#ifdef USE_SENSOR
+ for (auto *obj : App.get_sensors())
+ if (!obj->is_internal())
+ client->send(this->sensor_json(obj, obj->state).c_str(), "state");
+#endif
+
+#ifdef USE_SWITCH
+ for (auto *obj : App.get_switches())
+ if (!obj->is_internal())
+ client->send(this->switch_json(obj, obj->state).c_str(), "state");
+#endif
+
+#ifdef USE_BINARY_SENSOR
+ for (auto *obj : App.get_binary_sensors())
+ if (!obj->is_internal())
+ client->send(this->binary_sensor_json(obj, obj->state).c_str(), "state");
+#endif
+
+#ifdef USE_FAN
+ for (auto *obj : App.get_fans())
+ if (!obj->is_internal())
+ client->send(this->fan_json(obj).c_str(), "state");
+#endif
+
+#ifdef USE_LIGHT
+ for (auto *obj : App.get_lights())
+ if (!obj->is_internal())
+ client->send(this->light_json(obj).c_str(), "state");
+#endif
+
+#ifdef USE_TEXT_SENSOR
+ for (auto *obj : App.get_text_sensors())
+ if (!obj->is_internal())
+ client->send(this->text_sensor_json(obj, obj->state).c_str(), "state");
+#endif
+
+#ifdef USE_COVER
+ for (auto *obj : App.get_covers())
+ if (!obj->is_internal())
+ client->send(this->cover_json(obj).c_str(), "state");
+#endif
+
+#ifdef USE_NUMBER
+ for (auto *obj : App.get_numbers())
+ if (!obj->is_internal())
+ client->send(this->number_json(obj, obj->state).c_str(), "state");
+#endif
+
+#ifdef USE_SELECT
+ for (auto *obj : App.get_selects())
+ if (!obj->is_internal())
+ client->send(this->select_json(obj, obj->state).c_str(), "state");
+#endif
+ });
+
+#ifdef USE_LOGGER
+ if (logger::global_logger != nullptr)
+ logger::global_logger->add_on_log_callback(
+ [this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); });
+#endif
+ 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->base_->get_port());
+ if (this->using_auth()) {
+ ESP_LOGCONFIG(TAG, " Basic authentication enabled");
+ }
+}
+float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; }
+
+void WebServer::handle_index_request(AsyncWebServerRequest *request) {
+ AsyncResponseStream *stream = request->beginResponseStream("text/html");
+ std::string title = App.get_name() + " Web Server";
+ stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><title>"));
+ stream->print(title.c_str());
+ stream->print(F("</title>"));
+#ifdef WEBSERVER_CSS_INCLUDE
+ stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">"));
+#endif
+ if (strlen(this->css_url_) > 0) {
+ stream->print(F("<link rel=\"stylesheet\" href=\""));
+ stream->print(this->css_url_);
+ stream->print(F("\">"));
+ }
+ stream->print(F("</head><body><article class=\"markdown-body\"><h1>"));
+ stream->print(title.c_str());
+
+ // KAUF edit
+ // print links to kaufha and esphome, list version numbers
+ stream->print(F("</h1>"));
+
+ stream->print(F("<p>KAUF Plug by <a href=\"https://kaufha.com\" target=\"_blank\" rel=\"noopener noreferrer\">Kaufman Home Automation</a></p>"));
+ stream->print(F("<p>Firmware made using <a href=\"https://esphome.io\" target=\"_blank\" rel=\"noopener noreferrer\">ESPHome</a></p>"));
+ stream->print(F("<p>KAUF Plug firmware version 1.5, ESPHome version 2021.08.0</p>"));
+
+
+
+ stream->print(F("<h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>"));
+ // KAUF edit end
+
+ // All content is controlled and created by user - so allowing all origins is fine here.
+ stream->addHeader("Access-Control-Allow-Origin", "*");
+
+#ifdef USE_SENSOR
+ for (auto *obj : App.get_sensors())
+ write_row(stream, obj, "sensor", "");
+#endif
+
+#ifdef USE_SWITCH
+ for (auto *obj : App.get_switches())
+ write_row(stream, obj, "switch", "<button>Toggle</button>");
+#endif
+
+#ifdef USE_BINARY_SENSOR
+ for (auto *obj : App.get_binary_sensors())
+ write_row(stream, obj, "binary_sensor", "");
+#endif
+
+#ifdef USE_FAN
+ for (auto *obj : App.get_fans())
+ write_row(stream, obj, "fan", "<button>Toggle</button>");
+#endif
+
+#ifdef USE_LIGHT
+ for (auto *obj : App.get_lights())
+ write_row(stream, obj, "light", "<button>Toggle</button>");
+#endif
+
+#ifdef USE_TEXT_SENSOR
+ for (auto *obj : App.get_text_sensors())
+ write_row(stream, obj, "text_sensor", "");
+#endif
+
+#ifdef USE_COVER
+ for (auto *obj : App.get_covers())
+ write_row(stream, obj, "cover", "<button>Open</button><button>Close</button>");
+#endif
+
+#ifdef USE_NUMBER
+ for (auto *obj : App.get_numbers())
+ write_row(stream, obj, "number", "");
+#endif
+
+#ifdef USE_SELECT
+ for (auto *obj : App.get_selects())
+ write_row(stream, obj, "select", "");
+#endif
+
+ // KAUF edit
+ // warn not to load tasmota-minimal
+ 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>"
+ "<br />**** DO NOT USE <b>TASMOTA-MINIMAL</b>.BIN or .BIN.GZ. **** Use tasmota.bin.gz."
+ "<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>"));
+ // KAUF edit end
+
+#ifdef WEBSERVER_JS_INCLUDE
+ if (this->js_include_ != nullptr) {
+ stream->print(F("<script src=\"/0.js\"></script>"));
+ }
+#endif
+ if (strlen(this->js_url_) > 0) {
+ stream->print(F("<script src=\""));
+ stream->print(this->js_url_);
+ stream->print(F("\"></script>"));
+ }
+ stream->print(F("</article></body></html>"));
+
+ request->send(stream);
+}
+
+#ifdef WEBSERVER_CSS_INCLUDE
+void WebServer::handle_css_request(AsyncWebServerRequest *request) {
+ AsyncResponseStream *stream = request->beginResponseStream("text/css");
+ if (this->css_include_ != nullptr) {
+ stream->print(this->css_include_);
+ }
+
+ request->send(stream);
+}
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+void WebServer::handle_js_request(AsyncWebServerRequest *request) {
+ AsyncResponseStream *stream = request->beginResponseStream("text/javascript");
+ if (this->js_include_ != nullptr) {
+ stream->print(this->js_include_);
+ }
+
+ request->send(stream);
+}
+#endif
+
+#ifdef USE_SENSOR
+void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
+ this->events_.send(this->sensor_json(obj, state).c_str(), "state");
+}
+void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+ for (sensor::Sensor *obj : App.get_sensors()) {
+ if (obj->is_internal())
+ continue;
+ if (obj->get_object_id() != match.id)
+ continue;
+ std::string data = this->sensor_json(obj, obj->state);
+ request->send(200, "text/json", data.c_str());
+ return;
+ }
+ request->send(404);
+}
+std::string WebServer::sensor_json(sensor::Sensor *obj, float value) {
+ return json::build_json([obj, value](JsonObject &root) {
+ root["id"] = "sensor-" + obj->get_object_id();
+ std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals());
+ if (!obj->get_unit_of_measurement().empty())
+ state += " " + obj->get_unit_of_measurement();
+ root["state"] = state;
+ root["value"] = value;
+ });
+}
+#endif
+
+#ifdef USE_TEXT_SENSOR
+void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
+ this->events_.send(this->text_sensor_json(obj, state).c_str(), "state");
+}
+void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+ for (text_sensor::TextSensor *obj : App.get_text_sensors()) {
+ if (obj->is_internal())
+ continue;
+ if (obj->get_object_id() != match.id)
+ continue;
+ std::string data = this->text_sensor_json(obj, obj->state);
+ request->send(200, "text/json", data.c_str());
+ return;
+ }
+ request->send(404);
+}
+std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value) {
+ return json::build_json([obj, value](JsonObject &root) {
+ root["id"] = "text_sensor-" + obj->get_object_id();
+ root["state"] = value;
+ root["value"] = value;
+ });
+}
+#endif
+
+#ifdef USE_SWITCH
+void WebServer::on_switch_update(switch_::Switch *obj, bool state) {
+ this->events_.send(this->switch_json(obj, state).c_str(), "state");
+}
+std::string WebServer::switch_json(switch_::Switch *obj, bool value) {
+ return json::build_json([obj, value](JsonObject &root) {
+ root["id"] = "switch-" + obj->get_object_id();
+ root["state"] = value ? "ON" : "OFF";
+ root["value"] = value;
+ });
+}
+void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+ for (switch_::Switch *obj : App.get_switches()) {
+ if (obj->is_internal())
+ continue;
+ if (obj->get_object_id() != match.id)
+ continue;
+
+ if (request->method() == HTTP_GET) {
+ std::string data = this->switch_json(obj, obj->state);
+ request->send(200, "text/json", data.c_str());
+ } else if (match.method == "toggle") {
+ this->defer([obj]() { obj->toggle(); });
+ request->send(200);
+ } else if (match.method == "turn_on") {
+ this->defer([obj]() { obj->turn_on(); });
+ request->send(200);
+ } else if (match.method == "turn_off") {
+ this->defer([obj]() { obj->turn_off(); });
+ request->send(200);
+ } else {
+ request->send(404);
+ }
+ return;
+ }
+ request->send(404);
+}
+#endif
+
+#ifdef USE_BINARY_SENSOR
+void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
+ if (obj->is_internal())
+ return;
+ this->events_.send(this->binary_sensor_json(obj, state).c_str(), "state");
+}
+std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value) {
+ return json::build_json([obj, value](JsonObject &root) {
+ root["id"] = "binary_sensor-" + obj->get_object_id();
+ root["state"] = value ? "ON" : "OFF";
+ root["value"] = value;
+ });
+}
+void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+ for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) {
+ if (obj->is_internal())
+ continue;
+ if (obj->get_object_id() != match.id)
+ continue;
+ std::string data = this->binary_sensor_json(obj, obj->state);
+ request->send(200, "text/json", data.c_str());
+ return;
+ }
+ request->send(404);
+}
+#endif
+
+#ifdef USE_FAN
+void WebServer::on_fan_update(fan::FanState *obj) {
+ if (obj->is_internal())
+ return;
+ this->events_.send(this->fan_json(obj).c_str(), "state");
+}
+std::string WebServer::fan_json(fan::FanState *obj) {
+ return json::build_json([obj](JsonObject &root) {
+ root["id"] = "fan-" + obj->get_object_id();
+ root["state"] = obj->state ? "ON" : "OFF";
+ root["value"] = obj->state;
+ const auto traits = obj->get_traits();
+ if (traits.supports_speed()) {
+ root["speed_level"] = obj->speed;
+ switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) {
+ case fan::FAN_SPEED_LOW:
+ root["speed"] = "low";
+ break;
+ case fan::FAN_SPEED_MEDIUM:
+ root["speed"] = "medium";
+ break;
+ case fan::FAN_SPEED_HIGH:
+ root["speed"] = "high";
+ break;
+ }
+ }
+ if (obj->get_traits().supports_oscillation())
+ root["oscillation"] = obj->oscillating;
+ });
+}
+void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+ for (fan::FanState *obj : App.get_fans()) {
+ if (obj->is_internal())
+ continue;
+ if (obj->get_object_id() != match.id)
+ continue;
+
+ if (request->method() == HTTP_GET) {
+ std::string data = this->fan_json(obj);
+ request->send(200, "text/json", data.c_str());
+ } else if (match.method == "toggle") {
+ this->defer([obj]() { obj->toggle().perform(); });
+ request->send(200);
+ } else if (match.method == "turn_on") {
+ auto call = obj->turn_on();
+ if (request->hasParam("speed")) {
+ String speed = request->getParam("speed")->value();
+ call.set_speed(speed.c_str());
+ }
+ if (request->hasParam("speed_level")) {
+ String speed_level = request->getParam("speed_level")->value();
+ auto val = parse_int(speed_level.c_str());
+ if (!val.has_value()) {
+ ESP_LOGW(TAG, "Can't convert '%s' to number!", speed_level.c_str());
+ return;
+ }
+ call.set_speed(*val);
+ }
+ if (request->hasParam("oscillation")) {
+ String speed = request->getParam("oscillation")->value();
+ auto val = parse_on_off(speed.c_str());
+ switch (val) {
+ case PARSE_ON:
+ call.set_oscillating(true);
+ break;
+ case PARSE_OFF:
+ call.set_oscillating(false);
+ break;
+ case PARSE_TOGGLE:
+ call.set_oscillating(!obj->oscillating);
+ break;
+ case PARSE_NONE:
+ request->send(404);
+ return;
+ }
+ }
+ this->defer([call]() { call.perform(); });
+ request->send(200);
+ } else if (match.method == "turn_off") {
+ this->defer([obj]() { obj->turn_off().perform(); });
+ request->send(200);
+ } else {
+ request->send(404);
+ }
+ return;
+ }
+ request->send(404);
+}
+#endif
+
+#ifdef USE_LIGHT
+void WebServer::on_light_update(light::LightState *obj) {
+ if (obj->is_internal())
+ return;
+ this->events_.send(this->light_json(obj).c_str(), "state");
+}
+void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+ for (light::LightState *obj : App.get_lights()) {
+ if (obj->is_internal())
+ continue;
+ if (obj->get_object_id() != match.id)
+ continue;
+
+ if (request->method() == HTTP_GET) {
+ std::string data = this->light_json(obj);
+ request->send(200, "text/json", data.c_str());
+ } else if (match.method == "toggle") {
+ this->defer([obj]() { obj->toggle().perform(); });
+ request->send(200);
+ } else if (match.method == "turn_on") {
+ auto call = obj->turn_on();
+ if (request->hasParam("brightness"))
+ call.set_brightness(request->getParam("brightness")->value().toFloat() / 255.0f);
+ if (request->hasParam("r"))
+ call.set_red(request->getParam("r")->value().toFloat() / 255.0f);
+ if (request->hasParam("g"))
+ call.set_green(request->getParam("g")->value().toFloat() / 255.0f);
+ if (request->hasParam("b"))
+ call.set_blue(request->getParam("b")->value().toFloat() / 255.0f);
+ if (request->hasParam("white_value"))
+ call.set_white(request->getParam("white_value")->value().toFloat() / 255.0f);
+ if (request->hasParam("color_temp"))
+ call.set_color_temperature(request->getParam("color_temp")->value().toFloat());
+
+ if (request->hasParam("flash")) {
+ float length_s = request->getParam("flash")->value().toFloat();
+ call.set_flash_length(static_cast<uint32_t>(length_s * 1000));
+ }
+
+ if (request->hasParam("transition")) {
+ float length_s = request->getParam("transition")->value().toFloat();
+ call.set_transition_length(static_cast<uint32_t>(length_s * 1000));
+ }
+
+ if (request->hasParam("effect")) {
+ const char *effect = request->getParam("effect")->value().c_str();
+ call.set_effect(effect);
+ }
+
+ this->defer([call]() mutable { call.perform(); });
+ request->send(200);
+ } else if (match.method == "turn_off") {
+ auto call = obj->turn_off();
+ if (request->hasParam("transition")) {
+ auto length = (uint32_t) request->getParam("transition")->value().toFloat() * 1000;
+ call.set_transition_length(length);
+ }
+ this->defer([call]() mutable { call.perform(); });
+ request->send(200);
+ } else {
+ request->send(404);
+ }
+ return;
+ }
+ request->send(404);
+}
+std::string WebServer::light_json(light::LightState *obj) {
+ return json::build_json([obj](JsonObject &root) {
+ root["id"] = "light-" + obj->get_object_id();
+ root["state"] = obj->remote_values.is_on() ? "ON" : "OFF";
+ light::LightJSONSchema::dump_json(*obj, root);
+ });
+}
+#endif
+
+#ifdef USE_COVER
+void WebServer::on_cover_update(cover::Cover *obj) {
+ if (obj->is_internal())
+ return;
+ this->events_.send(this->cover_json(obj).c_str(), "state");
+}
+void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+ for (cover::Cover *obj : App.get_covers()) {
+ if (obj->is_internal())
+ continue;
+ if (obj->get_object_id() != match.id)
+ continue;
+
+ if (request->method() == HTTP_GET) {
+ std::string data = this->cover_json(obj);
+ request->send(200, "text/json", data.c_str());
+ continue;
+ }
+
+ auto call = obj->make_call();
+ if (match.method == "open") {
+ call.set_command_open();
+ } else if (match.method == "close") {
+ call.set_command_close();
+ } else if (match.method == "stop") {
+ call.set_command_stop();
+ } else if (match.method != "set") {
+ request->send(404);
+ return;
+ }
+
+ auto traits = obj->get_traits();
+ if ((request->hasParam("position") && !traits.get_supports_position()) ||
+ (request->hasParam("tilt") && !traits.get_supports_tilt())) {
+ request->send(409);
+ return;
+ }
+
+ if (request->hasParam("position"))
+ call.set_position(request->getParam("position")->value().toFloat());
+ if (request->hasParam("tilt"))
+ call.set_tilt(request->getParam("tilt")->value().toFloat());
+
+ this->defer([call]() mutable { call.perform(); });
+ request->send(200);
+ return;
+ }
+ request->send(404);
+}
+std::string WebServer::cover_json(cover::Cover *obj) {
+ return json::build_json([obj](JsonObject &root) {
+ root["id"] = "cover-" + obj->get_object_id();
+ root["state"] = obj->is_fully_closed() ? "CLOSED" : "OPEN";
+ root["value"] = obj->position;
+ root["current_operation"] = cover::cover_operation_to_str(obj->current_operation);
+
+ if (obj->get_traits().get_supports_tilt())
+ root["tilt"] = obj->tilt;
+ });
+}
+#endif
+
+#ifdef USE_NUMBER
+void WebServer::on_number_update(number::Number *obj, float state) {
+ this->events_.send(this->number_json(obj, state).c_str(), "state");
+}
+void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+ for (auto *obj : App.get_numbers()) {
+ if (obj->is_internal())
+ continue;
+ if (obj->get_object_id() != match.id)
+ continue;
+ std::string data = this->number_json(obj, obj->state);
+ request->send(200, "text/json", data.c_str());
+ return;
+ }
+ request->send(404);
+}
+std::string WebServer::number_json(number::Number *obj, float value) {
+ return json::build_json([obj, value](JsonObject &root) {
+ root["id"] = "number-" + obj->get_object_id();
+ char buffer[64];
+ snprintf(buffer, sizeof(buffer), "%f", value);
+ root["state"] = buffer;
+ root["value"] = value;
+ });
+}
+#endif
+
+#ifdef USE_SELECT
+void WebServer::on_select_update(select::Select *obj, const std::string &state) {
+ this->events_.send(this->select_json(obj, state).c_str(), "state");
+}
+void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
+ for (auto *obj : App.get_selects()) {
+ if (obj->is_internal())
+ continue;
+ if (obj->get_object_id() != match.id)
+ continue;
+ std::string data = this->select_json(obj, obj->state);
+ request->send(200, "text/json", data.c_str());
+ return;
+ }
+ request->send(404);
+}
+std::string WebServer::select_json(select::Select *obj, const std::string &value) {
+ return json::build_json([obj, value](JsonObject &root) {
+ root["id"] = "select-" + obj->get_object_id();
+ root["state"] = value;
+ root["value"] = value;
+ });
+}
+#endif
+
+bool WebServer::canHandle(AsyncWebServerRequest *request) {
+ if (request->url() == "/")
+ return true;
+
+#ifdef WEBSERVER_CSS_INCLUDE
+ if (request->url() == "/0.css")
+ return true;
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+ if (request->url() == "/0.js")
+ return true;
+#endif
+
+ UrlMatch match = match_url(request->url().c_str(), true);
+ if (!match.valid)
+ return false;
+#ifdef USE_SENSOR
+ if (request->method() == HTTP_GET && match.domain == "sensor")
+ return true;
+#endif
+
+#ifdef USE_SWITCH
+ if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "switch")
+ return true;
+#endif
+
+#ifdef USE_BINARY_SENSOR
+ if (request->method() == HTTP_GET && match.domain == "binary_sensor")
+ return true;
+#endif
+
+#ifdef USE_FAN
+ if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "fan")
+ return true;
+#endif
+
+#ifdef USE_LIGHT
+ if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "light")
+ return true;
+#endif
+
+#ifdef USE_TEXT_SENSOR
+ if (request->method() == HTTP_GET && match.domain == "text_sensor")
+ return true;
+#endif
+
+#ifdef USE_COVER
+ if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "cover")
+ return true;
+#endif
+
+#ifdef USE_NUMBER
+ if (request->method() == HTTP_GET && match.domain == "number")
+ return true;
+#endif
+
+#ifdef USE_SELECT
+ if (request->method() == HTTP_GET && match.domain == "select")
+ return true;
+#endif
+
+ return false;
+}
+void WebServer::handleRequest(AsyncWebServerRequest *request) {
+ if (this->using_auth() && !request->authenticate(this->username_, this->password_)) {
+ return request->requestAuthentication();
+ }
+
+ if (request->url() == "/") {
+ this->handle_index_request(request);
+ return;
+ }
+
+#ifdef WEBSERVER_CSS_INCLUDE
+ if (request->url() == "/0.css") {
+ this->handle_css_request(request);
+ return;
+ }
+#endif
+
+#ifdef WEBSERVER_JS_INCLUDE
+ if (request->url() == "/0.js") {
+ this->handle_js_request(request);
+ return;
+ }
+#endif
+
+ UrlMatch match = match_url(request->url().c_str());
+#ifdef USE_SENSOR
+ if (match.domain == "sensor") {
+ this->handle_sensor_request(request, match);
+ return;
+ }
+#endif
+
+#ifdef USE_SWITCH
+ if (match.domain == "switch") {
+ this->handle_switch_request(request, match);
+ return;
+ }
+#endif
+
+#ifdef USE_BINARY_SENSOR
+ if (match.domain == "binary_sensor") {
+ this->handle_binary_sensor_request(request, match);
+ return;
+ }
+#endif
+
+#ifdef USE_FAN
+ if (match.domain == "fan") {
+ this->handle_fan_request(request, match);
+ return;
+ }
+#endif
+
+#ifdef USE_LIGHT
+ if (match.domain == "light") {
+ this->handle_light_request(request, match);
+ return;
+ }
+#endif
+
+#ifdef USE_TEXT_SENSOR
+ if (match.domain == "text_sensor") {
+ this->handle_text_sensor_request(request, match);
+ return;
+ }
+#endif
+
+#ifdef USE_COVER
+ if (match.domain == "cover") {
+ this->handle_cover_request(request, match);
+ return;
+ }
+#endif
+
+#ifdef USE_NUMBER
+ if (match.domain == "number") {
+ this->handle_number_request(request, match);
+ return;
+ }
+#endif
+
+#ifdef USE_SELECT
+ if (match.domain == "select") {
+ this->handle_select_request(request, match);
+ return;
+ }
+#endif
+}
+
+bool WebServer::isRequestHandlerTrivial() { return false; }
+
+} // namespace web_server
+} // namespace esphome
diff --git a/web_server_base.cpp b/web_server_base.cpp
new file mode 100644
index 0000000..86f6643
--- /dev/null
+++ b/web_server_base.cpp
@@ -0,0 +1,114 @@
+#include "web_server_base.h"
+#include "esphome/core/log.h"
+#include "esphome/core/application.h"
+#include <StreamString.h>
+
+// KAUF edit
+// needed for string searching below
+#include <string> // std::string
+// KAUF edit end
+
+#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 *const 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;
+
+// KAUF edit
+// kill update process if "minimal" is found in string
+ std::string str = filename.c_str();
+ std::size_t found = str.find("minimal");
+
+ if (found!=std::string::npos) {
+ ESP_LOGI(TAG, "***** DO NOT TRY TO FLASH TASMOTA-MINIMAL *****");
+ report_ota_error();
+ return;
+ }
+// KAUF edit end
+
+ 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