diff options
-rw-r--r-- | PLF10 Manual.pdf | bin | 0 -> 370065 bytes | |||
-rw-r--r-- | captive_portal.cpp | 160 | ||||
-rw-r--r-- | kauf-plug.bin | bin | 0 -> 460048 bytes | |||
-rw-r--r-- | kauf-plug.bin.gz | bin | 0 -> 314329 bytes | |||
-rw-r--r-- | kauf-plug.yaml | 218 | ||||
-rw-r--r-- | web_server.cpp | 840 | ||||
-rw-r--r-- | web_server_base.cpp | 114 |
7 files changed, 1332 insertions, 0 deletions
diff --git a/PLF10 Manual.pdf b/PLF10 Manual.pdf Binary files differnew file mode 100644 index 0000000..a0897e7 --- /dev/null +++ b/PLF10 Manual.pdf 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 Binary files differnew file mode 100644 index 0000000..d17fe0e --- /dev/null +++ b/kauf-plug.bin diff --git a/kauf-plug.bin.gz b/kauf-plug.bin.gz Binary files differnew file mode 100644 index 0000000..020c0d2 --- /dev/null +++ b/kauf-plug.bin.gz 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 |