/* ; Project: Open Vehicle Monitor System ; Date: 14th March 2017 ; ; Changes: ; 1.0 Initial release ; ; (C) 2018 Michael Balzer ; ; Permission is hereby granted, free of charge, to any person obtaining a copy ; of this software and associated documentation files (the "Software"), to deal ; in the Software without restriction, including without limitation the rights ; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ; copies of the Software, and to permit persons to whom the Software is ; furnished to do so, subject to the following conditions: ; ; The above copyright notice and this permission notice shall be included in ; all copies or substantial portions of the Software. ; ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ; THE SOFTWARE. */ #include "ovms_log.h" #ifdef CONFIG_OVMS_COMP_CELLULAR static const char *TAG = "webserver"; #endif #include #include #include #include #include #include #include "ovms_webserver.h" #include "ovms_config.h" #include "ovms_metrics.h" #include "metrics_standard.h" #include "vehicle.h" #include "ovms_housekeeping.h" #include "ovms_peripherals.h" #include "ovms_version.h" #ifdef CONFIG_OVMS_COMP_OTA #include "ovms_ota.h" #endif #ifdef CONFIG_OVMS_COMP_PUSHOVER #include "pushover.h" #endif #define _attr(text) (c.encode_html(text).c_str()) #define _html(text) (c.encode_html(text).c_str()) /** * HandleStatus: show status overview */ void OvmsWebServer::HandleStatus(PageEntry_t& p, PageContext_t& c) { std::string cmd, output; c.head(200); if (c.method == "POST") { cmd = c.getvar("action"); if (cmd == "reboot") { OutputReboot(p, c); c.done(); return; } else { // "network restart", "wifi reconnect" OutputReconnect(p, c, NULL, cmd.c_str()); c.done(); return; } } PAGE_HOOK("body.pre"); c.print( "
" "
" "
"); c.panel_start("primary", "Live"); c.print( "
" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "
Module" "
?bytes free
" "
?tasks running
" "
Network" "
??
" "
?dBm
" "
GPS" "
?Satellites
" "
?%
" "
Main battery" "
?%
" "
?V
" "
?A
" "
12V battery" "
?V
" "
?A
" "
Events" "
    " "
" "
" "
" ); c.panel_end(); c.print( "
" "
"); c.panel_start("primary", "Vehicle"); output = ExecuteCommand("stat"); c.printf("%s", _html(output)); output = ExecuteCommand("location status"); c.printf("%s", _html(output)); c.panel_end( "
    " "
  • " "
  • " "
  • " "
"); c.print( "
" "
"); c.panel_start("primary", "Server"); output = ExecuteCommand("server v2 status"); if (!startsWith(output, "Unrecognised")) c.printf("%s", _html(output)); output = ExecuteCommand("server v3 status"); if (!startsWith(output, "Unrecognised")) c.printf("%s", _html(output)); c.panel_end( "
    " "
  • " "
  • " "
  • " "
  • " "
  • " "
"); c.print( "
" "
"); c.panel_start("primary", "SD Card"); output = ExecuteCommand("sd status"); c.printf("%s", _html(output)); c.panel_end( "
    " "
  • " "
  • " "
  • " "
"); c.print( "
" "
"); c.panel_start("primary", "Module"); output = ExecuteCommand("boot status"); c.printf("%s", _html(output)); c.print("
"); output = ExecuteCommand("ota status nocheck"); c.printf("%s", _html(output)); c.panel_end( "
    " "
  • " "
  • " "
"); c.print( "
" "
"); c.panel_start("primary", "Network"); output = ExecuteCommand("network status"); c.printf("%s", _html(output)); c.panel_end( "
    " "
  • " "
"); c.print( "
" "
"); c.panel_start("primary", "Wifi"); output = ExecuteCommand("wifi status"); c.printf("%s", _html(output)); c.panel_end( "
    " "
  • " "
"); c.print( "
" "
"); c.panel_start("primary", "Cellular Modem"); output = ExecuteCommand("cellular status"); c.printf("%s", _html(output)); c.panel_end( "
    " "
  • " "
  • " "
  • " "
  • " "
  • " "
"); c.print( "
" "
" "" ); PAGE_HOOK("body.post"); c.done(); } /** * HandleCommand: execute command, stream output */ void OvmsWebServer::HandleCommand(PageEntry_t& p, PageContext_t& c) { std::string type = c.getvar("type"); bool javascript = (type == "js"); std::string output = c.getvar("output"); extram::string command; c.getvar("command", command); #ifndef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE if (javascript) { c.head(400); c.print("ERROR: Javascript support disabled"); c.done(); return; } #endif if (!javascript && command.length() > 2000) { c.head(400); c.print("ERROR: command too long (max 2000 chars)"); c.done(); return; } // Note: application/octet-stream default instead of text/plain is a workaround for an *old* // Chrome/Webkit bug: chunked text/plain is always buffered for the first 1024 bytes. if (output == "text") { c.head(200, "Content-Type: text/plain; charset=utf-8\r\n" "Cache-Control: no-cache"); } else if (output == "json") { c.head(200, "Content-Type: application/json; charset=utf-8\r\n" "Cache-Control: no-cache"); } else if (output == "binary") { // As Safari on iOS still doesn't support responseType=ArrayBuffer on XMLHttpRequests, // this is meant to be used for binary data transmissions. // Based on Marcus Granado's approach, see… // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data c.head(200, "Content-Type: application/octet-stream; charset=x-user-defined\r\n" "Cache-Control: no-cache"); } else { c.head(200, "Content-Type: application/octet-stream; charset=utf-8\r\n" "Cache-Control: no-cache"); } if (command.empty()) c.done(); else new HttpCommandStream(c.nc, command, javascript); } /** * HandleShell: command shell */ void OvmsWebServer::HandleShell(PageEntry_t& p, PageContext_t& c) { std::string command = c.getvar("command", 2000); std::string output; if (command != "") output = ExecuteCommand(command); // generate form: c.head(200); PAGE_HOOK("body.pre"); c.print( ""); c.panel_start("primary panel-minpad", "Shell" "
" "" "
"); c.printf( "
%s
" "
" "
" "" "" "
" "" "
" "
" "
" , _html(output.c_str()), _attr(command.c_str())); c.print( ""); c.panel_end(); PAGE_HOOK("body.post"); c.done(); } /** * HandleCfgPassword: change admin password */ void OvmsWebServer::HandleCfgPassword(PageEntry_t& p, PageContext_t& c) { std::string error, info; std::string oldpass, newpass1, newpass2; if (c.method == "POST") { // process form submission: oldpass = c.getvar("oldpass"); newpass1 = c.getvar("newpass1"); newpass2 = c.getvar("newpass2"); if (oldpass != MyConfig.GetParamValue("password", "module")) error += "
  • Old password is not correct
  • "; if (newpass1 == oldpass) error += "
  • New password identical to old password
  • "; if (newpass1.length() < 8) error += "
  • New password must have at least 8 characters
  • "; if (newpass2 != newpass1) error += "
  • Passwords do not match
  • "; if (error == "") { // success: if (MyConfig.GetParamValue("password", "module") == MyConfig.GetParamValue("wifi.ap", "OVMS")) { MyConfig.SetParamValue("wifi.ap", "OVMS", newpass1); info += "
  • New Wifi AP password for network OVMS has been set.
  • "; } MyConfig.SetParamValue("password", "module", newpass1); info += "
  • New module & admin password has been set.
  • "; info = "

    Success!

      " + info + "
    "; c.head(200); c.alert("success", info.c_str()); OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { c.head(200); } // show password warning: if (MyConfig.GetParamValue("password", "module").empty()) { c.alert("danger", "

    Warning: no admin password set. Web access is open to the public.

    " "

    Please change your password now.

    "); } // create some random passwords: std::ostringstream pwsugg; srand48(StdMetrics.ms_m_monotonic->AsInt() * StdMetrics.ms_m_freeram->AsInt()); pwsugg << "

    Inspiration:"; for (int i=0; i<5; i++) pwsugg << " " << c.encode_html(pwgen(12)) << ""; pwsugg << "

    "; // generate form: c.panel_start("primary", "Change module & admin password"); c.form_start(p.uri); c.input_password("Old password", "oldpass", "", NULL, NULL, "autocomplete=\"section-login current-password\""); c.input_password("New password", "newpass1", "", "Enter new password, min. 8 characters", pwsugg.str().c_str(), "autocomplete=\"section-login new-password\""); c.input_password("…repeat", "newpass2", "", "Repeat new password", NULL, "autocomplete=\"section-login new-password\""); c.input_button("default", "Submit"); c.form_end(); c.panel_end( (MyConfig.GetParamValue("password", "module") == MyConfig.GetParamValue("wifi.ap", "OVMS")) ? "

    Note: this changes both the module and the Wifi access point password for network OVMS, as they are identical right now.

    " "

    You can set a separate Wifi password on the Wifi configuration page.

    " : NULL); c.alert("info", "

    Note: if you lose your password, you may need to erase your configuration to restore access to the module.

    " "

    To set a new password via console, registered App or SMS, execute command config set password module ….

    "); c.done(); } /** * HandleCfgVehicle: configure vehicle type & identity (URL: /cfg/vehicle) */ void OvmsWebServer::HandleCfgVehicle(PageEntry_t& p, PageContext_t& c) { std::string error, info; std::string vehicleid, vehicletype, vehiclename, timezone, timezone_region, pin; std::string bat12v_factor, bat12v_ref, bat12v_alert; std::map units_values; metric_group_list_t unit_groups; OvmsMetricGroupConfigList(unit_groups); if (c.method == "POST") { // process form submission: vehicleid = c.getvar("vehicleid"); vehicletype = c.getvar("vehicletype"); vehiclename = c.getvar("vehiclename"); timezone = c.getvar("timezone"); timezone_region = c.getvar("timezone_region"); for ( auto grpiter = unit_groups.begin(); grpiter != unit_groups.end(); ++grpiter) { std::string name = OvmsMetricGroupName(*grpiter); std::string cfg = "units_"; cfg += name; units_values[*grpiter] = c.getvar(cfg); } bat12v_factor = c.getvar("bat12v_factor"); bat12v_ref = c.getvar("bat12v_ref"); bat12v_alert = c.getvar("bat12v_alert"); pin = c.getvar("pin"); if (vehicleid.length() == 0) error += "
  • Vehicle ID must not be empty
  • "; if (vehicleid.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-") != std::string::npos) error += "
  • Vehicle ID may only contain ASCII letters, digits and '-'
  • "; if (error == "" && StdMetrics.ms_v_type->AsString() != vehicletype) { MyVehicleFactory.SetVehicle(vehicletype.c_str()); if (!MyVehicleFactory.ActiveVehicle()) error += "
  • Cannot set vehicle type " + vehicletype + "
  • "; else info += "
  • New vehicle type " + vehicletype + " has been set.
  • "; } if (error == "") { // success: MyConfig.SetParamValue("vehicle", "id", vehicleid); MyConfig.SetParamValue("auto", "vehicle.type", vehicletype); MyConfig.SetParamValue("vehicle", "name", vehiclename); MyConfig.SetParamValue("vehicle", "timezone", timezone); MyConfig.SetParamValue("vehicle", "timezone_region", timezone_region); for ( auto grpiter = unit_groups.begin(); grpiter != unit_groups.end(); ++grpiter) { std::string name = OvmsMetricGroupName(*grpiter); std::string value = units_values[*grpiter]; OvmsMetricSetUserConfig(*grpiter, value); } MyConfig.SetParamValue("system.adc", "factor12v", bat12v_factor); MyConfig.SetParamValue("vehicle", "12v.ref", bat12v_ref); MyConfig.SetParamValue("vehicle", "12v.alert", bat12v_alert); if (!pin.empty()) MyConfig.SetParamValue("password", "pin", pin); info = "

    Success!

      " + info + "
    "; info += ""; c.head(200); c.alert("success", info.c_str()); OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { // read configuration: vehicleid = MyConfig.GetParamValue("vehicle", "id"); vehicletype = MyConfig.GetParamValue("auto", "vehicle.type"); vehiclename = MyConfig.GetParamValue("vehicle", "name"); timezone = MyConfig.GetParamValue("vehicle", "timezone"); timezone_region = MyConfig.GetParamValue("vehicle", "timezone_region"); for ( auto grpiter = unit_groups.begin(); grpiter != unit_groups.end(); ++grpiter) units_values[*grpiter] = OvmsMetricGetUserConfig(*grpiter); bat12v_factor = MyConfig.GetParamValue("system.adc", "factor12v"); bat12v_ref = MyConfig.GetParamValue("vehicle", "12v.ref"); bat12v_alert = MyConfig.GetParamValue("vehicle", "12v.alert"); c.head(200); } // generate form: c.panel_start("primary", "Vehicle configuration"); c.form_start(p.uri); c.print( "" "
    " "
    "); c.input_select_start("Vehicle type", "vehicletype"); c.input_select_option("—", "", vehicletype.empty()); for (OvmsVehicleFactory::map_vehicle_t::iterator k=MyVehicleFactory.m_vmap.begin(); k!=MyVehicleFactory.m_vmap.end(); ++k) c.input_select_option(k->second.name, k->first, (vehicletype == k->first)); c.input_select_end(); c.input_text("Vehicle ID", "vehicleid", vehicleid.c_str(), "Use ASCII letters, digits and '-'", "

    Note: this is also the vehicle account ID for server connections.

    "); c.input_text("Vehicle name", "vehiclename", vehiclename.c_str(), "optional, the name of your car"); c.printf( "
    " "" "
    " "" "" "
    " "
    " "
    " "" "
    " , _attr(timezone_region) , _attr(timezone)); for ( auto grpiter = unit_groups.begin(); grpiter != unit_groups.end(); ++grpiter) { std::string name = OvmsMetricGroupName(*grpiter); metric_unit_set_t group_units; if (OvmsMetricGroupUnits(*grpiter,group_units)) { bool use_select = group_units.size() > 3; std::string cfg = "units_"; cfg += name; std::string value = units_values[*grpiter]; if (use_select) c.input_select_start(OvmsMetricGroupLabel(*grpiter), cfg.c_str() ); else c.input_radiobtn_start(OvmsMetricGroupLabel(*grpiter), cfg.c_str() ); bool checked = value.empty(); if (use_select) c.input_select_option( "Default", "", checked); else c.input_radiobtn_option(cfg.c_str(), "Default", "", checked); for (auto unititer = group_units.begin(); unititer != group_units.end(); ++unititer) { const char* unit_name = OvmsMetricUnitName(*unititer); const char* unit_label = OvmsMetricUnitLabel(*unititer); checked = value == unit_name; if (use_select) c.input_select_option( unit_label, unit_name, checked); else c.input_radiobtn_option(cfg.c_str(), unit_label, unit_name, checked); } if (use_select) c.input_select_end(); else c.input_radiobtn_end(); } } c.input_password("PIN", "pin", "", "empty = no change", "

    Vehicle PIN code used for unlocking etc.

    ", "autocomplete=\"section-vehiclepin new-password\""); c.print( "
    " "
    "); c.input_info("12V reading", "
    " "
    " "?" "V" "
    " "
    "); c.input_slider("12V calibration", "bat12v_factor", 6, NULL, -1, bat12v_factor.empty() ? 195.7 : atof(bat12v_factor.c_str()), 195.7, 175.0, 225.0, 0.1, "

    Adjust the calibration so the voltage displayed above matches your real voltage.

    "); c.input("number", "12V reference", "bat12v_ref", bat12v_ref.c_str(), "Default: 12.6", "

    The nominal resting voltage level of your 12V battery when fully charged.

    ", "min=\"10\" max=\"15\" step=\"0.1\"", "V"); c.input("number", "12V alert threshold", "bat12v_alert", bat12v_alert.c_str(), "Default: 1.6", "

    If the actual voltage drops this far below the maximum of configured and measured reference" " level, an alert is sent.

    ", "min=\"0\" max=\"3\" step=\"0.1\"", "V"); c.print( "
    " "
    " "
    "); c.input_button("default", "Save"); c.form_end(); c.panel_end(); c.print( ""); c.done(); } #ifdef CONFIG_OVMS_COMP_CELLULAR /** * HandleCfgModem: configure APN & cellular modem features (URL /cfg/modem) */ void OvmsWebServer::HandleCfgModem(PageEntry_t& p, PageContext_t& c) { std::string apn, apn_user, apn_pass, network_dns, pincode; bool enable_gps, enable_gpstime, enable_net, enable_sms, wrongpincode; if (c.method == "POST") { // process form submission: apn = c.getvar("apn"); apn_user = c.getvar("apn_user"); apn_pass = c.getvar("apn_pass"); pincode = c.getvar("pincode"); network_dns = c.getvar("network_dns"); enable_net = (c.getvar("enable_net") == "yes"); enable_sms = (c.getvar("enable_sms") == "yes"); enable_gps = (c.getvar("enable_gps") == "yes"); enable_gpstime = (c.getvar("enable_gpstime") == "yes"); MyConfig.SetParamValue("modem", "apn", apn); MyConfig.SetParamValue("modem", "apn.user", apn_user); MyConfig.SetParamValue("modem", "apn.password", apn_pass); if ( MyConfig.GetParamValueBool("modem","wrongpincode") && (MyConfig.GetParamValue("modem","pincode") != pincode) ) { ESP_LOGI(TAG,"New SIM card PIN code entered. Cleared wrong_pin_code flag"); MyConfig.SetParamValueBool("modem", "wrongpincode", false); } MyConfig.SetParamValue("modem", "pincode", pincode); MyConfig.SetParamValue("network", "dns", network_dns); MyConfig.SetParamValueBool("modem", "enable.net", enable_net); MyConfig.SetParamValueBool("modem", "enable.sms", enable_sms); MyConfig.SetParamValueBool("modem", "enable.gps", enable_gps); MyConfig.SetParamValueBool("modem", "enable.gpstime", enable_gpstime); c.head(200); c.alert("success", "

    Modem configured.

    "); OutputHome(p, c); c.done(); return; } // read configuration: apn = MyConfig.GetParamValue("modem", "apn"); apn_user = MyConfig.GetParamValue("modem", "apn.user"); apn_pass = MyConfig.GetParamValue("modem", "apn.password"); pincode = MyConfig.GetParamValue("modem", "pincode"); wrongpincode = MyConfig.GetParamValueBool("modem", "wrongpincode",false); network_dns = MyConfig.GetParamValue("network", "dns"); enable_net = MyConfig.GetParamValueBool("modem", "enable.net", true); enable_sms = MyConfig.GetParamValueBool("modem", "enable.sms", true); enable_gps = MyConfig.GetParamValueBool("modem", "enable.gps", false); enable_gpstime = MyConfig.GetParamValueBool("modem", "enable.gpstime", false); // generate form: c.head(200); c.panel_start("primary", "Cellular modem configuration"); c.form_start(p.uri); std::string info; std::string iccid = StdMetrics.ms_m_net_mdm_iccid->AsString(); if (!iccid.empty()) { info = "" + iccid + ""; } else { info = "
    " "(power cellular modem on to read)" " " "" " " "" "
    " ""; } c.input_info("SIM ICCID", info.c_str()); c.input_text("SIM card PIN code", "pincode", pincode.c_str(), "", wrongpincode ? "

    Wrong PIN code entered previously!

    " : "

    Not needed for Hologram SIM cards

    "); c.fieldset_start("Internet"); c.input_checkbox("Enable IP networking", "enable_net", enable_net); c.input_text("APN", "apn", apn.c_str(), NULL, "

    For Hologram, use APN hologram with empty username & password

    "); c.input_text("…username", "apn_user", apn_user.c_str()); c.input_text("…password", "apn_pass", apn_pass.c_str()); c.input_text("DNS", "network_dns", network_dns.c_str(), "optional fixed DNS servers (space separated)", "

    Set this to i.e. 8.8.8.8 8.8.4.4 (Google public DNS) if you encounter problems with your network provider DNS

    "); c.fieldset_end(); c.fieldset_start("Features"); c.input_checkbox("Enable SMS", "enable_sms", enable_sms); c.input_checkbox("Enable GPS", "enable_gps", enable_gps); c.input_checkbox("Use GPS time", "enable_gpstime", enable_gpstime); c.fieldset_end(); c.hr(); c.input_button("default", "Save"); c.form_end(); c.panel_end(); c.done(); } #endif /** * HandleCfgPushover: Configure pushover notifications (URL /cfg/pushover) */ #ifdef CONFIG_OVMS_COMP_PUSHOVER void OvmsWebServer::HandleCfgPushover(PageEntry_t& p, PageContext_t& c) { std::string error; OvmsConfigParam* param = MyConfig.CachedParam("pushover"); ConfigParamMap pmap; int i, max; char buf[100]; std::string name, msg, pri; if (c.method == "POST") { // process form submission: pmap["enable"] = (c.getvar("enable") == "yes") ? "yes" : "no"; pmap["user_key"] = c.getvar("user_key"); pmap["token"] = c.getvar("token"); // validate: //if (server.length() == 0) // error += "
  • Server must not be empty
  • "; if (pmap["enable"]=="yes") { if (pmap["user_key"].length() == 0) error += "
  • User key must not be empty
  • "; if (pmap["user_key"].find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") != std::string::npos) error += "
  • User key may only contain ASCII letters and digits
  • "; if (pmap["token"].length() == 0) error += "
  • Token must not be empty
  • "; if (pmap["token"].find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") != std::string::npos) error += "
  • Token may only contain ASCII letters and digits
  • "; } pmap["sound.normal"] = c.getvar("sound.normal"); pmap["sound.high"] = c.getvar("sound.high"); pmap["sound.emergency"] = c.getvar("sound.emergency"); pmap["expire"] = c.getvar("expire"); pmap["retry"] = c.getvar("retry"); // read notification type/subtypes and their priorities max = atoi(c.getvar("npmax").c_str()); for (i = 1; i <= max; i++) { sprintf(buf, "nfy_%d", i); name = c.getvar(buf); if (name == "") continue; sprintf(buf, "np_%d", i); pri = c.getvar(buf); if (pri == "") continue; snprintf(buf, sizeof(buf), "np.%s", name.c_str()); pmap[buf] = pri; } // read events, their messages and priorities max = atoi(c.getvar("epmax").c_str()); for (i = 1; i <= max; i++) { sprintf(buf, "en_%d", i); name = c.getvar(buf); if (name == "") continue; sprintf(buf, "em_%d", i); msg = c.getvar(buf); sprintf(buf, "ep_%d", i); pri = c.getvar(buf); if (pri == "") continue; snprintf(buf, sizeof(buf), "ep.%s", name.c_str()); pri.append("/"); pri.append(msg); pmap[buf] = pri; } if (error == "") { if (c.getvar("action") == "save") { // save: param->m_map.clear(); param->m_map = std::move(pmap); param->Save(); c.head(200); c.alert("success", "

    Pushover connection configured.

    "); OutputHome(p, c); c.done(); return; } else if (c.getvar("action") == "test") { std::string reply; std::string popup; c.head(200); c.alert("success", "

    Sending message

    "); if (!MyPushoverClient.SendMessageOpt( c.getvar("user_key"), c.getvar("token"), c.getvar("test_message"), atoi(c.getvar("test_priority").c_str()), c.getvar("test_sound"), atoi(c.getvar("retry").c_str()), atoi(c.getvar("expire").c_str()), true /* receive server reply as reply/pushover-type notification */ )) { c.alert("danger", "

    Could not send test message!

    "); } } } else { // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } } else { // read configuration: pmap = param->m_map; // generate form: c.head(200); } c.panel_start("primary", "Pushover server configuration"); c.form_start(p.uri); c.printf("

    Please visit Pushover web site to create an account (identified by a user key) " " and then register OVMS as an application in order to receive an application token).
    " "Install Pushover iOS/Android application and specify your user key.
    Finally enter both the user key and the application token here and test connectivity.
    " "To receive specific notifications and events, configure them below.

    " ); c.input_checkbox("Enable Pushover connectivity", "enable", pmap["enable"] == "yes"); c.input_text("User key", "user_key", pmap["user_key"].c_str(), "Enter user key (alphanumerical key consisting of around 30 characters"); c.input_text("Token", "token", pmap["token"].c_str(), "Enter token (alphanumerical key consisting of around 30 characters"); auto gen_options_priority = [&c](std::string priority) { c.printf( "" "" "" "" "" , (priority=="-2") ? "selected" : "" , (priority=="-1") ? "selected" : "" , (priority=="0"||priority=="") ? "selected" : "" , (priority=="1") ? "selected" : "" , (priority=="2") ? "selected" : ""); }; auto gen_options_sound = [&c](std::string sound) { c.printf( "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" , (sound=="pushover") || (sound=="") ? "selected" : "" , (sound=="bike") ? "selected" : "" , (sound=="bugle") ? "selected" : "" , (sound=="cashregister") ? "selected" : "" , (sound=="classical") ? "selected" : "" , (sound=="cosmic") ? "selected" : "" , (sound=="falling") ? "selected" : "" , (sound=="gamelan") ? "selected" : "" , (sound=="incoming") ? "selected" : "" , (sound=="intermission") ? "selected" : "" , (sound=="magic") ? "selected" : "" , (sound=="mechanical") ? "selected" : "" , (sound=="pianobar") ? "selected" : "" , (sound=="siren") ? "selected" : "" , (sound=="spacealarm") ? "selected" : "" , (sound=="tugboat") ? "selected" : "" , (sound=="alien") ? "selected" : "" , (sound=="climb") ? "selected" : "" , (sound=="persistent") ? "selected" : "" , (sound=="echo") ? "selected" : "" , (sound=="updown") ? "selected" : "" , (sound=="none") ? "selected" : ""); }; c.input_select_start("Normal priority sound", "sound.normal"); gen_options_sound(pmap["sound.normal"]); c.input_select_end(); c.input_select_start("High priority sound", "sound.high"); gen_options_sound(pmap["sound.high"]); c.input_select_end(); c.input_select_start("Emergency priority sound", "sound.emergency"); gen_options_sound(pmap["sound.emergency"]); c.input_select_end(); c.input("number", "Retry", "retry", pmap["retry"].c_str(), "Default: 30", "

    Time period after which new notification is sent if emergency priority message is not acknowledged.

    ", "min=\"30\" step=\"1\"", "secs"); c.input("number", "Expiration", "expire", pmap["expire"].c_str(), "Default: 1800", "

    Time period after an emergency priority message will expire (and will not cause a new notification) if the message is not acknowledged.

    ", "min=\"0\" step=\"1\"", "secs"); // Test message area c.print( "
    " "" "
    " "
    " "" "" "" "" "" "
    "); c.input_text("Message", "test_message", c.getvar("test_message").c_str(), "Enter test message"); c.print(""); c.input_select_start("Priority", "test_priority"); gen_options_priority(c.getvar("test_priority") != "" ? c.getvar("test_priority") : "0"); c.input_select_end(); c.print(""); c.input_select_start("Sound", "test_sound"); gen_options_sound( c.getvar("test_sound") != "" ? c.getvar("test_sound").c_str() : pmap["sound.normal"]); c.input_select_end(); c.print(""); c.input_button("default", "Send", "action", "test"); c.printf( "
    " "
    " "
    " "
    "); // Input area for Notifications c.print( "
    " "" "
    " "
    " "" "" "" "" "" "" "" "" ""); max = 0; for (auto &kv: pmap) { if (!startsWith(kv.first, "np.")) continue; max++; name = kv.first.substr(3); c.printf( "" "" "" "" ""); } c.printf( "" "" "" "" "" "" "
    Type/SubtypePriority
    " "" "
    " "

    Enter the type of notification (for example \"alert\" or \"info\") or more specifically the type/subtype tuple (for example \"alert/alarm.sounding\"). " " If a notification matches multiple filters, only the more specific will be used. " "For more complete listing, see OVMS User Guide

    " "
    " "
    " , max); // Input area for Events c.print( "
    " "" "
    " "
    " "" "" "" "" "" "" "" "" "" ""); max = 0; for (auto &kv: pmap) { if (!startsWith(kv.first, "ep.")) continue; max++; // Priority and message is saved as "priority/message" tuple (eg. "-1/this is a message") name = kv.first.substr(3); if (kv.second[1]=='/') { pri = kv.second.substr(0,1); msg = kv.second.substr(2); } else if (kv.second[2]=='/') { pri = kv.second.substr(0,2); msg = kv.second.substr(3); } else continue; c.printf( "" "" "" "" "" ""); } c.printf( "" "" "" "" "" "" "
    EventMessagePriority
    " "" "
    " "

    Enter the event name (for example \"vehicle.locked\" or \"vehicle.alert.12v.on\"). " "For more complete listing, see OVMS User Guide

    " "
    " "
    " , max); c.input_button("default", "Save","action","save"); c.form_end(); c.print( ""); c.panel_end(); c.done(); } #endif #ifdef CONFIG_OVMS_COMP_SERVER #ifdef CONFIG_OVMS_COMP_SERVER_V2 /** * HandleCfgServerV2: configure server v2 connection (URL /cfg/server/v2) */ void OvmsWebServer::HandleCfgServerV2(PageEntry_t& p, PageContext_t& c) { std::string error; std::string server, vehicleid, password, port; std::string updatetime_connected, updatetime_idle; bool tls; if (c.method == "POST") { // process form submission: server = c.getvar("server"); tls = (c.getvar("tls") == "yes"); vehicleid = c.getvar("vehicleid"); password = c.getvar("password"); port = c.getvar("port"); updatetime_connected = c.getvar("updatetime_connected"); updatetime_idle = c.getvar("updatetime_idle"); // validate: if (port != "") { if (port.find_first_not_of("0123456789") != std::string::npos || atoi(port.c_str()) < 0 || atoi(port.c_str()) > 65535) { error += "
  • Port must be an integer value in the range 0…65535
  • "; } } if (vehicleid.length() == 0) error += "
  • Vehicle ID must not be empty
  • "; if (vehicleid.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-") != std::string::npos) error += "
  • Vehicle ID may only contain ASCII letters, digits and '-'
  • "; if (updatetime_connected != "") { if (atoi(updatetime_connected.c_str()) < 1) { error += "
  • Update interval (connected) must be at least 1 second
  • "; } } if (updatetime_idle != "") { if (atoi(updatetime_idle.c_str()) < 1) { error += "
  • Update interval (idle) must be at least 1 second
  • "; } } if (error == "") { // success: MyConfig.SetParamValue("server.v2", "server", server); MyConfig.SetParamValueBool("server.v2", "tls", tls); MyConfig.SetParamValue("server.v2", "port", port); MyConfig.SetParamValue("vehicle", "id", vehicleid); if (password != "") MyConfig.SetParamValue("password","server.v2", password); MyConfig.SetParamValue("server.v2", "updatetime.connected", updatetime_connected); MyConfig.SetParamValue("server.v2", "updatetime.idle", updatetime_idle); std::string info = "

    Server V2 (MP) connection configured.

    " ""; c.head(200); c.alert("success", info.c_str()); OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { // read configuration: server = MyConfig.GetParamValue("server.v2", "server"); tls = MyConfig.GetParamValueBool("server.v2", "tls", false); vehicleid = MyConfig.GetParamValue("vehicle", "id"); password = MyConfig.GetParamValue("password", "server.v2"); port = MyConfig.GetParamValue("server.v2", "port"); updatetime_connected = MyConfig.GetParamValue("server.v2", "updatetime.connected"); updatetime_idle = MyConfig.GetParamValue("server.v2", "updatetime.idle"); // generate form: c.head(200); } c.panel_start("primary", "Server V2 (MP) configuration"); c.form_start(p.uri); c.input_text("Server", "server", server.c_str(), "Enter host name or IP address", "

    Public OVMS V2 servers:

    " ""); c.input_checkbox("Enable TLS", "tls", tls, "

    Note: enable transport layer security (encryption) if your server supports it (all public OVMS servers do).

    "); c.input_text("Port", "port", port.c_str(), "optional, default: 6867 (no TLS) / 6870 (TLS)"); c.input_text("Vehicle ID", "vehicleid", vehicleid.c_str(), "Use ASCII letters, digits and '-'", NULL, "autocomplete=\"section-serverv2 username\""); c.input_password("Server password", "password", "", "empty = no change", "

    Note: enter the password for the vehicle ID, not your user account password

    ", "autocomplete=\"section-serverv2 current-password\""); c.fieldset_start("Update intervals"); c.input_text("…connected", "updatetime_connected", updatetime_connected.c_str(), "optional, in seconds, default: 60"); c.input_text("…idle", "updatetime_idle", updatetime_idle.c_str(), "optional, in seconds, default: 600"); c.fieldset_end(); c.hr(); c.input_button("default", "Save"); c.form_end(); c.panel_end(); c.done(); } #endif #ifdef CONFIG_OVMS_COMP_SERVER_V3 /** * HandleCfgServerV3: configure server v3 connection (URL /cfg/server/v3) */ void OvmsWebServer::HandleCfgServerV3(PageEntry_t& p, PageContext_t& c) { std::string error; std::string server, user, password, port, topic_prefix; std::string updatetime_connected, updatetime_idle, updatetime_on, updatetime_charging, updatetime_awake, updatetime_sendall; bool tls; if (c.method == "POST") { // process form submission: server = c.getvar("server"); tls = (c.getvar("tls") == "yes"); user = c.getvar("user"); password = c.getvar("password"); port = c.getvar("port"); topic_prefix = c.getvar("topic_prefix"); updatetime_connected = c.getvar("updatetime_connected"); updatetime_idle = c.getvar("updatetime_idle"); updatetime_on = c.getvar("updatetime_on"); updatetime_charging = c.getvar("updatetime_charging"); updatetime_awake = c.getvar("updatetime_awake"); updatetime_sendall = c.getvar("updatetime_sendall"); // validate: if (port != "") { if (port.find_first_not_of("0123456789") != std::string::npos || atoi(port.c_str()) < 0 || atoi(port.c_str()) > 65535) { error += "
  • Port must be an integer value in the range 0…65535
  • "; } } if (updatetime_connected != "") { if (atoi(updatetime_connected.c_str()) < 1) { error += "
  • Update interval (connected) must be at least 1 second
  • "; } } if (updatetime_idle != "") { if (atoi(updatetime_idle.c_str()) < 1) { error += "
  • Update interval (idle) must be at least 1 second
  • "; } } if (updatetime_on != "") { if (atoi(updatetime_on.c_str()) < 1) { error += "
  • Update interval (on) must be at least 1 second
  • "; } } if (updatetime_charging != "") { if (atoi(updatetime_charging.c_str()) < 1) { error += "
  • Update interval (charging) must be at least 1 second
  • "; } } if (updatetime_awake != "") { if (atoi(updatetime_awake.c_str()) < 1) { error += "
  • Update interval (awake) must be at least 1 second
  • "; } } if (updatetime_sendall != "") { if (atoi(updatetime_sendall.c_str()) < 60) { error += "
  • Update interval (sendall) must be at least 60 seconds
  • "; } } if (error == "") { // success: MyConfig.SetParamValue("server.v3", "server", server); MyConfig.SetParamValueBool("server.v3", "tls", tls); MyConfig.SetParamValue("server.v3", "user", user); if (password != "") MyConfig.SetParamValue("password", "server.v3", password); MyConfig.SetParamValue("server.v3", "port", port); MyConfig.SetParamValue("server.v3", "topic.prefix", topic_prefix); MyConfig.SetParamValue("server.v3", "updatetime.connected", updatetime_connected); MyConfig.SetParamValue("server.v3", "updatetime.idle", updatetime_idle); if (updatetime_on == "") MyConfig.DeleteInstance("server.v3", "updatetime.on"); else MyConfig.SetParamValue("server.v3", "updatetime.on", updatetime_on); if (updatetime_charging == "") MyConfig.DeleteInstance("server.v3", "updatetime.charging"); else MyConfig.SetParamValue("server.v3", "updatetime.charging", updatetime_charging); if (updatetime_awake == "") MyConfig.DeleteInstance("server.v3", "updatetime.awake"); else MyConfig.SetParamValue("server.v3", "updatetime.awake", updatetime_awake); if (updatetime_sendall == "") MyConfig.DeleteInstance("server.v3", "updatetime.sendall"); else MyConfig.SetParamValue("server.v3", "updatetime.sendall", updatetime_sendall); c.head(200); c.alert("success", "

    Server V3 (MQTT) connection configured.

    "); OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { // read configuration: server = MyConfig.GetParamValue("server.v3", "server"); tls = MyConfig.GetParamValueBool("server.v3", "tls", false); user = MyConfig.GetParamValue("server.v3", "user"); password = MyConfig.GetParamValue("password", "server.v3"); port = MyConfig.GetParamValue("server.v3", "port"); topic_prefix = MyConfig.GetParamValue("server.v3", "topic.prefix"); updatetime_connected = MyConfig.GetParamValue("server.v3", "updatetime.connected"); updatetime_idle = MyConfig.GetParamValue("server.v3", "updatetime.idle"); updatetime_on = MyConfig.GetParamValue("server.v3", "updatetime.on"); updatetime_charging = MyConfig.GetParamValue("server.v3", "updatetime.charging"); updatetime_awake = MyConfig.GetParamValue("server.v3", "updatetime.awake"); updatetime_sendall = MyConfig.GetParamValue("server.v3", "updatetime.sendall"); // generate form: c.head(200); } c.panel_start("primary", "Server V3 (MQTT) configuration"); c.form_start(p.uri); c.input_text("Server", "server", server.c_str(), "Enter host name or IP address", "

    Public OVMS V3 servers (MQTT brokers):

    " ""); c.input_checkbox("Enable TLS", "tls", tls, "

    Note: enable transport layer security (encryption) if your server supports it.

    "); c.input_text("Port", "port", port.c_str(), "optional, default: 1883 (no TLS) / 8883 (TLS)"); c.input_text("Username", "user", user.c_str(), "Enter user login name", NULL, "autocomplete=\"section-serverv3 username\""); c.input_password("Password", "password", "", "Enter user password, empty = no change", NULL, "autocomplete=\"section-serverv3 current-password\""); c.input_text("Topic Prefix", "topic_prefix", topic_prefix.c_str(), "optional, default: ovms///"); c.fieldset_start("Update intervals"); c.input_text("…connected", "updatetime_connected", updatetime_connected.c_str(), "optional, in seconds, default: 60"); c.input_text("…idle", "updatetime_idle", updatetime_idle.c_str(), "optional, in seconds, default: 600"); c.input_text("…on", "updatetime_on", updatetime_on.c_str(), "optional, in seconds, only used if set"); c.input_text("…charging", "updatetime_charging", updatetime_charging.c_str(), "optional, in seconds, only used if set"); c.input_text("…awake", "updatetime_awake", updatetime_awake.c_str(), "optional, in seconds, only used if set"); c.input_text("…sendall", "updatetime_sendall", updatetime_sendall.c_str(), "optional, in seconds, only used if set"); c.fieldset_end(); c.hr(); c.input_button("default", "Save"); c.form_end(); c.panel_end(); c.done(); } #endif #endif /** * HandleCfgNotifications: configure notifications & data logging (URL /cfg/notifications) */ void OvmsWebServer::HandleCfgNotifications(PageEntry_t& p, PageContext_t& c) { std::string error; std::string vehicle_minsoc, vehicle_stream; std::string log_trip_storetime, log_trip_minlength, log_grid_storetime; bool report_trip_enable; std::string report_trip_minlength; if (c.method == "POST") { // process form submission: vehicle_minsoc = c.getvar("vehicle_minsoc"); vehicle_stream = c.getvar("vehicle_stream"); log_trip_storetime = c.getvar("log_trip_storetime"); log_trip_minlength = c.getvar("log_trip_minlength"); log_grid_storetime = c.getvar("log_grid_storetime"); report_trip_enable = (c.getvar("report_trip_enable") == "yes"); report_trip_minlength = c.getvar("report_trip_minlength"); if (vehicle_minsoc != "") { if (atoi(vehicle_minsoc.c_str()) < 0 || atoi(vehicle_minsoc.c_str()) > 100) { error += "
  • Min SOC must be in the range 0…100 %
  • "; } } if (vehicle_stream != "") { if (atoi(vehicle_stream.c_str()) < 0 || atoi(vehicle_stream.c_str()) > 60) { error += "
  • GPS log interval must be in the range 0…60 seconds
  • "; } } if (log_trip_storetime != "") { if (atoi(log_trip_storetime.c_str()) < 0 || atoi(log_trip_storetime.c_str()) > 365) { error += "
  • Trip history log storage time must be in the range 0…365 days
  • "; } } if (log_trip_minlength != "") { if (atoi(log_trip_minlength.c_str()) < 0) { error += "
  • Trip min length must not be negative
  • "; } } if (log_grid_storetime != "") { if (atoi(log_grid_storetime.c_str()) < 0 || atoi(log_grid_storetime.c_str()) > 365) { error += "
  • Grid history log storage time must be in the range 0…365 days
  • "; } } if (report_trip_minlength != "") { if (atoi(report_trip_minlength.c_str()) < 0) { error += "
  • Trip report min length must not be negative
  • "; } } if (error == "") { // success: if (vehicle_minsoc == "") MyConfig.DeleteInstance("vehicle", "minsoc"); else MyConfig.SetParamValue("vehicle", "minsoc", vehicle_minsoc); if (vehicle_stream == "") MyConfig.DeleteInstance("vehicle", "stream"); else MyConfig.SetParamValue("vehicle", "stream", vehicle_stream); if (log_trip_storetime == "") MyConfig.DeleteInstance("notify", "log.trip.storetime"); else MyConfig.SetParamValue("notify", "log.trip.storetime", log_trip_storetime); if (log_trip_minlength == "") MyConfig.DeleteInstance("notify", "log.trip.minlength"); else MyConfig.SetParamValue("notify", "log.trip.minlength", log_trip_minlength); if (log_grid_storetime == "") MyConfig.DeleteInstance("notify", "log.grid.storetime"); else MyConfig.SetParamValue("notify", "log.grid.storetime", log_grid_storetime); MyConfig.SetParamValueBool("notify", "report.trip.enable", report_trip_enable); if (report_trip_minlength == "") MyConfig.DeleteInstance("notify", "report.trip.minlength"); else MyConfig.SetParamValue("notify", "report.trip.minlength", report_trip_minlength); c.head(200); c.alert("success", "

    Notifications configured.

    "); OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { // read configuration: vehicle_minsoc = MyConfig.GetParamValue("vehicle", "minsoc"); vehicle_stream = MyConfig.GetParamValue("vehicle", "stream"); log_trip_storetime = MyConfig.GetParamValue("notify", "log.trip.storetime"); log_trip_storetime = MyConfig.GetParamValue("notify", "log.trip.storetime"); log_trip_minlength = MyConfig.GetParamValue("notify", "log.trip.minlength"); log_grid_storetime = MyConfig.GetParamValue("notify", "log.grid.storetime"); report_trip_enable = MyConfig.GetParamValueBool("notify", "report.trip.enable"); report_trip_minlength = MyConfig.GetParamValue("notify", "report.trip.minlength"); // generate form: c.head(200); } c.panel_start("primary", "Notifications & Data Logging"); c.form_start(p.uri); c.fieldset_start("Vehicle Monitoring"); c.input_slider("Location streaming", "vehicle_stream", 3, "sec", -1, atoi(vehicle_stream.c_str()), 0, 0, 60, 1, "

    While driving send location updates to server every n seconds, 0 = use default update interval " "from server configuration. Same as App feature #8.

    "); c.input_slider("Minimum SOC", "vehicle_minsoc", 3, "%", -1, atoi(vehicle_minsoc.c_str()), 0, 0, 100, 1, "

    Send an alert when SOC drops below this level, 0 = off. Same as App feature #9.

    "); c.fieldset_end(); c.fieldset_start("Vehicle Reports"); c.input_checkbox("Enable trip report", "report_trip_enable", report_trip_enable, "

    This will send a textual report on driving statistics after each trip.

    "); c.input("number", "Report min trip length", "report_trip_minlength", report_trip_minlength.c_str(), "Default: 0.2 km", "

    Only trips over at least this distance will produce a report. If your vehicle does not support the " "v.p.trip metric, set this to 0.

    ", "min=\"0\" step=\"0.1\"", "km"); c.fieldset_end(); c.fieldset_start("Data Log Storage Times"); c.input("number", "Trip history log", "log_trip_storetime", log_trip_storetime.c_str(), "Default: empty/0 = disabled", "

    Empty/0 = disabled. If enabled, the trip log receives one entry per trip, " "see user manual for details.

    ", "min=\"0\" max=\"365\" step=\"1\"", "days"); c.input("number", "Trip min length", "log_trip_minlength", log_trip_minlength.c_str(), "Default: 0.2 km", "

    Only trips over at least this distance will be logged. If your vehicle does not support the " "v.p.trip metric, set this to 0.

    ", "min=\"0\" step=\"0.1\"", "km"); c.input("number", "Grid history log", "log_grid_storetime", log_grid_storetime.c_str(), "Default: empty/0 = disabled", "

    Empty/0 = disabled. If enabled, the grid log receives one entry per charge/generator state change, " "see user manual for details.

    ", "min=\"0\" max=\"365\" step=\"1\"", "days"); c.fieldset_end(); c.hr(); c.input_button("default", "Save"); c.form_end(); c.panel_end( "

    Data logs are sent to the server (V2/V3). A V2 server may store the logs as requested by you " "for up to 365 days, depending on the server configuration. You can download the log tables " "in CSV format from the server using the standard server APIs or the browser UI provided " "by the server.

    " "

    An MQTT (V3) server normally won't store data records for longer than a few days, possibly " "hours. You need to fetch them as soon as possible. There are automated MQTT tools available " "for this purpose.

    " "

    See user manual for details on notifications.

    " ); c.done(); } /** * HandleCfgWebServer: configure web server (URL /cfg/webserver) */ void OvmsWebServer::HandleCfgWebServer(PageEntry_t& p, PageContext_t& c) { std::string error, warn; std::string docroot, auth_domain, auth_file; bool enable_files, enable_dirlist, auth_global; extram::string tls_cert, tls_key; if (c.method == "POST") { // process form submission: docroot = c.getvar("docroot"); auth_domain = c.getvar("auth_domain"); auth_file = c.getvar("auth_file"); enable_files = (c.getvar("enable_files") == "yes"); enable_dirlist = (c.getvar("enable_dirlist") == "yes"); auth_global = (c.getvar("auth_global") == "yes"); c.getvar("tls_cert", tls_cert); c.getvar("tls_key", tls_key); // validate: if (docroot != "" && docroot[0] != '/') { error += "
  • Document root must start with '/'
  • "; } if (docroot == "/" || docroot == "/store" || docroot == "/store/" || startsWith(docroot, "/store/ovms_config")) { warn += "
  • Document root " + docroot + " may open access to OVMS configuration files, consider using a sub directory
  • "; } if (!tls_cert.empty() && !startsWith(tls_cert, "-----BEGIN CERTIFICATE-----")) { error += "
  • TLS certificate must be in PEM CERTIFICATE format
  • "; } if (!tls_key.empty() && !startsWith(tls_key, "-----BEGIN PRIVATE KEY-----")) { error += "
  • TLS private key must be in PEM PRIVATE KEY format
  • "; } if (tls_cert.empty() != tls_key.empty()) { error += "
  • Both TLS certificate and private key must be given
  • "; } // save TLS files: if (error == "") { if (tls_cert.empty()) { unlink("/store/tls/webserver.crt"); unlink("/store/tls/webserver.key"); } else { if (save_file("/store/tls/webserver.crt", tls_cert) != 0) { error += "
  • Error saving TLS certificate: "; error += strerror(errno); error += "
  • "; } if (save_file("/store/tls/webserver.key", tls_key) != 0) { error += "
  • Error saving TLS private key: "; error += strerror(errno); error += "
  • "; } } } if (error == "") { // success: if (docroot == "") MyConfig.DeleteInstance("http.server", "docroot"); else MyConfig.SetParamValue("http.server", "docroot", docroot); if (auth_domain == "") MyConfig.DeleteInstance("http.server", "auth.domain"); else MyConfig.SetParamValue("http.server", "auth.domain", auth_domain); if (auth_file == "") MyConfig.DeleteInstance("http.server", "auth.file"); else MyConfig.SetParamValue("http.server", "auth.file", auth_file); MyConfig.SetParamValueBool("http.server", "enable.files", enable_files); MyConfig.SetParamValueBool("http.server", "enable.dirlist", enable_dirlist); MyConfig.SetParamValueBool("http.server", "auth.global", auth_global); c.head(200); c.alert("success", "

    Webserver configuration saved.

    " "

    Note: if you changed the TLS certificate or key, you need to reboot" " the module to activate the change.

    "); if (warn != "") { warn = "

    Warning:

      " + warn + "
    "; c.alert("warning", warn.c_str()); } OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { // read configuration: docroot = MyConfig.GetParamValue("http.server", "docroot"); auth_domain = MyConfig.GetParamValue("http.server", "auth.domain"); auth_file = MyConfig.GetParamValue("http.server", "auth.file"); enable_files = MyConfig.GetParamValueBool("http.server", "enable.files", true); enable_dirlist = MyConfig.GetParamValueBool("http.server", "enable.dirlist", true); auth_global = MyConfig.GetParamValueBool("http.server", "auth.global", true); load_file("/store/tls/webserver.crt", tls_cert); load_file("/store/tls/webserver.key", tls_key); // generate form: c.head(200); } c.panel_start("primary", "Webserver configuration"); c.form_start(p.uri); c.input_checkbox("Enable file access", "enable_files", enable_files, "

    If enabled, paths not handled by the webserver itself are mapped to files below the web root path.

    " "

    Example: <img src=\"/icons/smiley.png\"> → file /sd/icons/smiley.png" " (if root path is /sd)

    "); c.input_text("Root path", "docroot", docroot.c_str(), "Default: /sd"); c.input_checkbox("Enable directory listings", "enable_dirlist", enable_dirlist); c.input_checkbox("Enable global file auth", "auth_global", auth_global, "

    If enabled, file access is globally protected by the admin password (if set).

    " "

    Disabling allows public access to directories without an auth file.

    " "

    To protect a directory, you can e.g. copy the default auth file:" " vfs cp /store/.htpasswd /sd/…/.htpasswd

    "); c.input_text("Directory auth file", "auth_file", auth_file.c_str(), "Default: .htpasswd", "

    Note: sub directories do not inherit the parent auth file.

    "); c.input_text("Auth domain/realm", "auth_domain", auth_domain.c_str(), "Default: ovms"); c.printf( "
    \n" "\n" "
    \n" "\n" "
    \n" "
    \n" , c.encode_html(tls_cert).c_str()); c.printf( "
    \n" "\n" "
    \n" "\n" "
    \n" "
    \n" , c.encode_html(tls_key).c_str()); c.input_button("default", "Save"); c.form_end(); c.panel_end( "

    To enable encryption (https, wss) you need to install a TLS certificate + key. Public" " certification authorities (CAs) won't issue certificates for private hosts and IP addresses," " so we recommend to create a self-signed TLS certificate for your module. Use a maximum key" " size of 2048 bit for acceptable performance.

    " "

    Example/template using OpenSSL:

    " "openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \\\n" "  -keyout ovms.key -out ovms.crt -subj \"/CN=ovmsname.local\" \\\n" "  -addext \"subjectAltName=IP:192.168.4.1,IP:192.168.2.101\"" "

    Change the name and add more IPs as needed. The command produces two files in your current" " directory, ovms.crt and ovms.key. Copy their contents into the" " respective fields above.

    " "

    Note: as this is a self-signed certificate, you will need to explicitly allow the" " browser to access the module on the first https connect.

    "); c.done(); } /** * HandleCfgWifi: configure wifi networks (URL /cfg/wifi) */ void OvmsWebServer::HandleCfgWifi(PageEntry_t& p, PageContext_t& c) { bool cfg_bad_reconnect; float cfg_sq_good, cfg_sq_bad; if (c.method == "POST") { std::string warn, error; // process form submission: UpdateWifiTable(p, c, "ap", "wifi.ap", warn, error, 8); UpdateWifiTable(p, c, "client", "wifi.ssid", warn, error, 0); cfg_sq_good = atof(c.getvar("cfg_sq_good").c_str()); cfg_sq_bad = atof(c.getvar("cfg_sq_bad").c_str()); cfg_bad_reconnect = (c.getvar("cfg_bad_reconnect") == "yes"); if (cfg_sq_bad >= cfg_sq_good) { error += "
  • 'Bad' signal level must be lower than 'good' level.
  • "; } else { if (cfg_sq_good == -87) MyConfig.DeleteInstance("network", "wifi.sq.good"); else MyConfig.SetParamValueFloat("network", "wifi.sq.good", cfg_sq_good); if (cfg_sq_bad == -89) MyConfig.DeleteInstance("network", "wifi.sq.bad"); else MyConfig.SetParamValueFloat("network", "wifi.sq.bad", cfg_sq_bad); if (!cfg_bad_reconnect) MyConfig.DeleteInstance("network", "wifi.bad.reconnect"); else MyConfig.SetParamValueBool("network", "wifi.bad.reconnect", cfg_bad_reconnect); } if (error == "") { c.head(200); c.alert("success", "

    Wifi configuration saved.

    "); if (warn != "") { warn = "

    Warning:

      " + warn + "
    "; c.alert("warning", warn.c_str()); } OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { cfg_sq_good = MyConfig.GetParamValueFloat("network", "wifi.sq.good", -87); cfg_sq_bad = MyConfig.GetParamValueFloat("network", "wifi.sq.bad", -89); cfg_bad_reconnect = MyConfig.GetParamValueBool("network", "wifi.bad.reconnect", false); c.head(200); } // generate form: c.panel_start("primary", "Wifi configuration"); c.printf( "
    " , _attr(p.uri)); c.fieldset_start("Access point networks"); OutputWifiTable(p, c, "ap", "wifi.ap", MyConfig.GetParamValue("auto", "wifi.ssid.ap", "OVMS")); c.fieldset_end(); c.fieldset_start("Wifi client networks"); OutputWifiTable(p, c, "client", "wifi.ssid", MyConfig.GetParamValue("auto", "wifi.ssid.client")); c.fieldset_end(); c.fieldset_start("Wifi client options"); c.input_slider("Good signal level", "cfg_sq_good", 3, "dBm", -1, cfg_sq_good, -87.0, -128.0, 0.0, 0.1, "

    Threshold for usable wifi signal strength

    "); c.input_slider("Bad signal level", "cfg_sq_bad", 3, "dBm", -1, cfg_sq_bad, -89.0, -128.0, 0.0, 0.1, "

    Threshold for unusable wifi signal strength

    "); c.input_checkbox("Immediate disconnect/reconnect", "cfg_bad_reconnect", cfg_bad_reconnect, "

    Check to immediately look for better access points when signal level gets bad." " Default is to stay with the current AP as long as possible.

    "); c.fieldset_end(); c.print( "
    " "" "
    " "\n" ""); c.panel_end( "

    Note: set the Wifi mode and default networks on the" " Autostart configuration page.

    "); c.done(); } void OvmsWebServer::OutputWifiTable(PageEntry_t& p, PageContext_t& c, const std::string prefix, const std::string paramname, const std::string autostart_ssid) { OvmsConfigParam* param = MyConfig.CachedParam(paramname); int pos = 0, pos_autostart = 0, max; char buf[50]; std::string ssid, pass; if (c.method == "POST") { max = atoi(c.getvar(prefix.c_str()).c_str()); sprintf(buf, "%s_autostart", prefix.c_str()); pos_autostart = atoi(c.getvar(buf).c_str()); } else { max = param->m_map.size(); } c.printf( "
    " "" "" "" "" "" "" "" "" "" "" , _attr(prefix), max); auto gen_row = [&c,&pos,&pos_autostart,&prefix,&ssid]() { if (prefix == "client") { // client entry: add network scanner/selector c.printf( "" "" "" "" "" , (pos == pos_autostart) ? "disabled title=\"Current autostart network\"" : "" , _attr(prefix), pos, _attr(ssid) , _attr(prefix), pos, _attr(prefix) , _attr(prefix), pos , _attr(prefix), pos); } else { // ap entry: c.printf( "" "" "" "" "" , (pos == pos_autostart) ? "disabled title=\"Current autostart network\"" : "" , _attr(prefix), pos, _attr(ssid), _attr(prefix) , _attr(prefix), pos); } }; if (c.method == "POST") { for (pos = 1; pos <= max; pos++) { sprintf(buf, "%s_ssid_%d", prefix.c_str(), pos); ssid = c.getvar(buf); gen_row(); } } else { for (auto const& kv : param->m_map) { pos++; ssid = kv.first; if (endsWith(ssid, ".ovms.staticip")) continue; if (ssid == autostart_ssid) pos_autostart = pos; gen_row(); } } c.print( "" "" "" "" "" "" "
    SSIDPassphrase
    " "" "
    " "" "
    "); c.printf( "" "
    " , _attr(prefix), pos_autostart); } void OvmsWebServer::UpdateWifiTable(PageEntry_t& p, PageContext_t& c, const std::string prefix, const std::string paramname, std::string& warn, std::string& error, int pass_minlen) { OvmsConfigParam* param = MyConfig.CachedParam(paramname); int i, max, pos_autostart; std::string ssid, pass, ssid_autostart; char buf[50]; ConfigParamMap newmap; max = atoi(c.getvar(prefix.c_str()).c_str()); sprintf(buf, "%s_autostart", prefix.c_str()); pos_autostart = atoi(c.getvar(buf).c_str()); for (i = 1; i <= max; i++) { sprintf(buf, "%s_ssid_%d", prefix.c_str(), i); ssid = c.getvar(buf); if (ssid == "") { if (i == pos_autostart) error += "
  • Autostart SSID may not be empty
  • "; continue; } sprintf(buf, "%s_pass_%d", prefix.c_str(), i); pass = c.getvar(buf); if (pass == "") pass = param->GetValue(ssid); if (pass == "") { if (i == pos_autostart) error += "
  • Autostart SSID " + ssid + " has no password
  • "; else warn += "
  • SSID " + ssid + " has no password
  • "; } else if (pass.length() < pass_minlen) { sprintf(buf, "%d", pass_minlen); error += "
  • SSID " + ssid + ": password is too short (min " + buf + " chars)
  • "; } newmap[ssid] = pass; if (param->IsDefined(ssid + ".ovms.staticip")) newmap[ssid + ".ovms.staticip"] = param->GetValue(ssid + ".ovms.staticip"); if (i == pos_autostart) ssid_autostart = ssid; } if (error == "") { // save new map: param->m_map.clear(); param->m_map = std::move(newmap); param->Save(); // set new autostart ssid: if (ssid_autostart != "") MyConfig.SetParamValue("auto", "wifi.ssid." + prefix, ssid_autostart); } } /** * HandleCfgAutoInit: configure auto init (URL /cfg/autostart) */ void OvmsWebServer::HandleCfgAutoInit(PageEntry_t& p, PageContext_t& c) { std::string error, warn; bool init, ext12v, modem, server_v2, server_v3; #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE bool scripting; #endif bool dbc; #ifdef CONFIG_OVMS_COMP_MAX7317 bool egpio; std::string egpio_ports; #endif //CONFIG_OVMS_COMP_MAX7317 std::string vehicle_type, obd2ecu, wifi_mode, wifi_ssid_client, wifi_ssid_ap; if (c.method == "POST") { // process form submission: init = (c.getvar("init") == "yes"); dbc = (c.getvar("dbc") == "yes"); ext12v = (c.getvar("ext12v") == "yes"); #ifdef CONFIG_OVMS_COMP_MAX7317 egpio = (c.getvar("egpio") == "yes"); egpio_ports = c.getvar("egpio_ports"); #endif //CONFIG_OVMS_COMP_MAX7317 modem = (c.getvar("modem") == "yes"); server_v2 = (c.getvar("server_v2") == "yes"); server_v3 = (c.getvar("server_v3") == "yes"); #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE scripting = (c.getvar("scripting") == "yes"); #endif vehicle_type = c.getvar("vehicle_type"); obd2ecu = c.getvar("obd2ecu"); wifi_mode = c.getvar("wifi_mode"); wifi_ssid_ap = c.getvar("wifi_ssid_ap"); wifi_ssid_client = c.getvar("wifi_ssid_client"); // check: if (wifi_mode == "ap" || wifi_mode == "apclient") { if (wifi_ssid_ap.empty()) wifi_ssid_ap = "OVMS"; if (MyConfig.GetParamValue("wifi.ap", wifi_ssid_ap).empty()) { if (MyConfig.GetParamValue("password", "module").empty()) error += "
  • Wifi AP mode invalid: no password set for SSID!
  • "; else warn += "
  • Wifi AP network has no password → uses module password.
  • "; } } if (wifi_mode == "client" || wifi_mode == "apclient") { if (wifi_ssid_client.empty()) { // check for defined client SSIDs: OvmsConfigParam* param = MyConfig.CachedParam("wifi.ssid"); int cnt = 0; for (auto const& kv : param->m_map) { if (kv.second != "") cnt++; } if (cnt == 0) { error += "
  • Wifi client scan mode invalid: no SSIDs defined!
  • "; } } else if (MyConfig.GetParamValue("wifi.ssid", wifi_ssid_client).empty()) { error += "
  • Wifi client mode invalid: no password set for SSID!
  • "; } } if (error == "") { // success: MyConfig.SetParamValueBool("auto", "init", init); MyConfig.SetParamValueBool("auto", "dbc", dbc); MyConfig.SetParamValueBool("auto", "ext12v", ext12v); #ifdef CONFIG_OVMS_COMP_MAX7317 MyConfig.SetParamValueBool("auto", "egpio", egpio); MyConfig.SetParamValue("egpio", "monitor.ports", egpio_ports); #endif //CONFIG_OVMS_COMP_MAX7317 MyConfig.SetParamValueBool("auto", "modem", modem); MyConfig.SetParamValueBool("auto", "server.v2", server_v2); MyConfig.SetParamValueBool("auto", "server.v3", server_v3); #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE MyConfig.SetParamValueBool("auto", "scripting", scripting); #endif MyConfig.SetParamValue("auto", "vehicle.type", vehicle_type); MyConfig.SetParamValue("auto", "obd2ecu", obd2ecu); MyConfig.SetParamValue("auto", "wifi.mode", wifi_mode); MyConfig.SetParamValue("auto", "wifi.ssid.ap", wifi_ssid_ap); MyConfig.SetParamValue("auto", "wifi.ssid.client", wifi_ssid_client); c.head(200); c.alert("success", "

    Auto start configuration saved.

    "); if (warn != "") { warn = "

    Warning:

      " + warn + "
    "; c.alert("warning", warn.c_str()); } if (c.getvar("action") == "save-reboot") OutputReboot(p, c); else OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { // read configuration: init = MyConfig.GetParamValueBool("auto", "init", true); dbc = MyConfig.GetParamValueBool("auto", "dbc", false); ext12v = MyConfig.GetParamValueBool("auto", "ext12v", false); #ifdef CONFIG_OVMS_COMP_MAX7317 egpio = MyConfig.GetParamValueBool("auto", "egpio", false); egpio_ports = MyConfig.GetParamValue("egpio", "monitor.ports"); #endif //CONFIG_OVMS_COMP_MAX7317 modem = MyConfig.GetParamValueBool("auto", "modem", false); server_v2 = MyConfig.GetParamValueBool("auto", "server.v2", false); server_v3 = MyConfig.GetParamValueBool("auto", "server.v3", false); #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE scripting = MyConfig.GetParamValueBool("auto", "scripting", true); #endif vehicle_type = MyConfig.GetParamValue("auto", "vehicle.type"); obd2ecu = MyConfig.GetParamValue("auto", "obd2ecu"); wifi_mode = MyConfig.GetParamValue("auto", "wifi.mode", "ap"); wifi_ssid_ap = MyConfig.GetParamValue("auto", "wifi.ssid.ap"); if (wifi_ssid_ap.empty()) wifi_ssid_ap = "OVMS"; wifi_ssid_client = MyConfig.GetParamValue("auto", "wifi.ssid.client"); c.head(200); } // generate form: c.panel_start("primary", "Auto start configuration"); c.form_start(p.uri); c.input_checkbox("Enable auto start", "init", init, "

    Note: if a crash occurs within 10 seconds after powering the module, autostart will be temporarily" " disabled. You may need to use the USB shell to access the module and fix the config.

    "); #ifdef CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE c.input_checkbox("Enable Javascript engine (Duktape)", "scripting", scripting, "

    Enable execution of Javascript on the module (plugins, commands, event handlers).

    "); #endif c.input_checkbox("Autoload DBC files", "dbc", dbc, "

    Enable to autoload DBC files (for reverse engineering).

    "); #ifdef CONFIG_OVMS_COMP_MAX7317 c.input_checkbox("Start EGPIO monitor", "egpio", egpio, "

    Enable to monitor EGPIO input ports and generate metrics and events from changes.

    "); c.input_text("EGPIO ports", "egpio_ports", egpio_ports.c_str(), "Ports to monitor", "

    Enter list of port numbers (0…9) to monitor, separated by spaces.

    "); #endif //CONFIG_OVMS_COMP_MAX7317 c.input_checkbox("Power on external 12V", "ext12v", ext12v, "

    Enable to provide 12V to external devices connected to the module (i.e. ECU displays).

    "); c.input_select_start("Wifi mode", "wifi_mode"); c.input_select_option("Access point", "ap", (wifi_mode == "ap")); c.input_select_option("Client mode", "client", (wifi_mode == "client")); c.input_select_option("Access point + Client", "apclient", (wifi_mode == "apclient")); c.input_select_option("Off", "off", (wifi_mode.empty() || wifi_mode == "off")); c.input_select_end(); c.input_select_start("… access point SSID", "wifi_ssid_ap"); OvmsConfigParam* param = MyConfig.CachedParam("wifi.ap"); if (param->m_map.find(wifi_ssid_ap) == param->m_map.end()) c.input_select_option(wifi_ssid_ap.c_str(), wifi_ssid_ap.c_str(), true); for (auto const& kv : param->m_map) c.input_select_option(kv.first.c_str(), kv.first.c_str(), (kv.first == wifi_ssid_ap)); c.input_select_end(); c.input_select_start("… client mode SSID", "wifi_ssid_client"); param = MyConfig.CachedParam("wifi.ssid"); c.input_select_option("Any known SSID (scan mode)", "", wifi_ssid_client.empty()); for (auto const& kv : param->m_map) c.input_select_option(kv.first.c_str(), kv.first.c_str(), (kv.first == wifi_ssid_client)); c.input_select_end(); c.input_checkbox("Start modem", "modem", modem); c.input_select_start("Vehicle type", "vehicle_type"); c.input_select_option("—", "", vehicle_type.empty()); for (OvmsVehicleFactory::map_vehicle_t::iterator k=MyVehicleFactory.m_vmap.begin(); k!=MyVehicleFactory.m_vmap.end(); ++k) c.input_select_option(k->second.name, k->first, (vehicle_type == k->first)); c.input_select_end(); c.input_select_start("Start OBD2ECU", "obd2ecu"); c.input_select_option("—", "", obd2ecu.empty()); c.input_select_option("can1", "can1", obd2ecu == "can1"); c.input_select_option("can2", "can2", obd2ecu == "can2"); c.input_select_option("can3", "can3", obd2ecu == "can3"); c.input_select_option("can4", "can4", obd2ecu == "can4"); c.input_select_end( "

    OBD2ECU translates OVMS to OBD2 metrics, i.e. to drive standard ECU displays

    "); c.input_checkbox("Start server V2", "server_v2", server_v2); c.input_checkbox("Start server V3", "server_v3", server_v3); c.print( "
    " "
    " " " " " "
    " "
    "); c.form_end(); c.panel_end(); c.done(); } #ifdef CONFIG_OVMS_COMP_OTA /** * HandleCfgFirmware: OTA firmware update & boot setup (URL /cfg/firmware) */ void OvmsWebServer::HandleCfgFirmware(PageEntry_t& p, PageContext_t& c) { std::string cmdres, mru; std::string action; ota_info info; bool auto_enable, auto_allow_modem; std::string auto_hour, server, tag; std::string output; std::string version; const char *what; char buf[132]; if (c.method == "POST") { // process form submission: bool error = false, showform = true, reboot = false; action = c.getvar("action"); auto_enable = (c.getvar("auto_enable") == "yes"); auto_allow_modem = (c.getvar("auto_allow_modem") == "yes"); auto_hour = c.getvar("auto_hour"); server = c.getvar("server"); tag = c.getvar("tag"); if (action.substr(0,3) == "set") { info.partition_boot = c.getvar("boot_old"); std::string partition_boot = c.getvar("boot"); if (partition_boot != info.partition_boot) { cmdres = ExecuteCommand("ota boot " + partition_boot); if (cmdres.find("Error:") != std::string::npos) error = true; output += "

    " + cmdres + "

    "; } else { output += "

    Boot partition unchanged.

    "; } if (!error) { MyConfig.SetParamValueBool("auto", "ota", auto_enable); MyConfig.SetParamValueBool("ota", "auto.allow.modem", auto_allow_modem); MyConfig.SetParamValue("ota", "auto.hour", auto_hour); MyConfig.SetParamValue("ota", "server", server); MyConfig.SetParamValue("ota", "tag", tag); } if (!error && action == "set-reboot") reboot = true; } else if (action == "reboot") { reboot = true; } else { error = true; output = "

    Unknown action.

    "; } // output result: if (error) { output = "

    Error!

    " + output; c.head(400); c.alert("danger", output.c_str()); } else { c.head(200); output = "

    OK!

    " + output; c.alert("success", output.c_str()); if (reboot) OutputReboot(p, c); if (reboot || !showform) { c.done(); return; } } } else { // read config: auto_enable = MyConfig.GetParamValueBool("auto", "ota", true); auto_allow_modem = MyConfig.GetParamValueBool("ota", "auto.allow.modem", false); auto_hour = MyConfig.GetParamValue("ota", "auto.hour", "2"); server = MyConfig.GetParamValue("ota", "server"); tag = MyConfig.GetParamValue("ota", "tag"); // generate form: c.head(200); } // read status: MyOTA.GetStatus(info); c.panel_start("primary", "Firmware setup & update"); c.input_info("Firmware version", info.version_firmware.c_str()); output = info.version_server; output.append(" "); c.input_info("…available", output.c_str()); c.print( "" "
    " "
    "); c.form_start(p.uri); // Boot partition: c.input_info("Running partition", info.partition_running.c_str()); c.printf("", _attr(info.partition_boot)); c.input_select_start("Boot from", "boot"); what = "Factory image"; version = GetOVMSPartitionVersion(ESP_PARTITION_SUBTYPE_APP_FACTORY); if (version != "") { snprintf(buf, sizeof(buf), "%s (%s)", what, version.c_str()); what = buf; } c.input_select_option(what, "factory", (info.partition_boot == "factory")); what = "OTA_0 image"; version = GetOVMSPartitionVersion(ESP_PARTITION_SUBTYPE_APP_OTA_0); if (version != "") { snprintf(buf, sizeof(buf), "%s (%s)", what, version.c_str()); what = buf; } c.input_select_option(what, "ota_0", (info.partition_boot == "ota_0")); what = "OTA_1 image"; version = GetOVMSPartitionVersion(ESP_PARTITION_SUBTYPE_APP_OTA_1); if (version != "") { snprintf(buf, sizeof(buf), "%s (%s)", what, version.c_str()); what = buf; } c.input_select_option(what, "ota_1", (info.partition_boot == "ota_1")); c.input_select_end(); // Server & auto update: c.print("
    "); c.input_checkbox("Enable auto update", "auto_enable", auto_enable, "

    Strongly recommended: if enabled, the module will perform automatic firmware updates within the hour of day specified.

    "); c.input("number", "Auto update hour of day", "auto_hour", auto_hour.c_str(), "0-23, default: 2", NULL, "min=\"0\" max=\"23\" step=\"1\""); c.input_checkbox("…allow via modem", "auto_allow_modem", auto_allow_modem, "

    Automatic updates are normally only done if a wifi connection is available at the time. Before allowing updates via modem, be aware a single firmware image has a size of around 3 MB, which may lead to additional costs on your data plan.

    "); c.print( "" "" "" "" ); c.input_text("Update server", "server", server.c_str(), "Specify or select from list (clear to see all options)", "

    Default is https://ovms-ota.bit-cloud.de.

    ", "list=\"server-list\""); c.input_text("Version tag", "tag", tag.c_str(), "Specify or select from list (clear to see all options)", "

    Default is main for standard releases. Use eap (early access program) for stable or edge for bleeding edge developer builds.

    ", "list=\"tag-list\""); c.print( "
    " "
    " "
    " " " " " " " "
    " "
    "); c.form_end(); c.print( "
    " "
    "); // warn about modem / AP connection: if (netif_default) { if (netif_default->name[0] == 'a' && netif_default->name[1] == 'p') { c.alert("warning", "

    No internet access.

    " "

    The module is running in wifi AP mode without cellular modem, so flashing from a public server is currently not possible.

    " "

    You can still flash from an AP network local IP address (192.168.4.x).

    "); } else if (netif_default->name[0] == 'p' && netif_default->name[1] == 'p') { c.alert("warning", "

    Using cellular modem connection for internet.

    " "

    Downloads from public servers will currently be done via cellular network. Be aware update files are >2 MB, " "which may exceed your data plan and need some time depending on your link speed.

    " "

    You can also flash locally from a wifi network IP address.

    "); } } c.form_start(p.uri); // Flash HTTP: mru = MyConfig.GetParamValue("ota", "http.mru"); c.input_text("HTTP URL", "flash_http", mru.c_str(), "optional: URL of firmware file", "

    Leave empty to download the latest firmware from the update server. " "Note: currently only http is supported.

    ", "list=\"urls\""); c.print(""); if (mru != "") c.printf(""); c.input_button("default", "Flash now", "action", "flash-http"); c.form_end(); c.print( "
    " "
    "); c.form_start(p.uri); // Flash VFS: mru = MyConfig.GetParamValue("ota", "vfs.mru"); c.input_info("Auto flash", "
      " "
    1. Place the file ovms3.bin in the SD root directory.
    2. " "
    3. Insert the SD card, wait until the module reboots.
    4. " "
    5. Note: after processing the file will be renamed to ovms3.done.
    6. " "
    "); c.input_info("Upload", "Not yet implemented. Please copy your update file to an SD card and enter the path below."); c.printf( "
    \n" "\n" "
    \n" "
    \n" "\n" "
    \n" "\n" "
    \n" "
    \n" "\n" "

    SD card root: /sd/

    \n" "
    \n" "
    \n" "
    \n" , mru.c_str()); c.print(""); if (mru != "") c.printf(""); c.input_button("default", "Flash now", "action", "flash-vfs"); c.form_end(); c.print( "
    " "
    "); c.panel_end( "

    The module can store up to three firmware images in a factory and two OTA partitions.

    " "

    Flashing from web or file writes alternating to the OTA partitions, the factory partition remains unchanged.

    " "

    You can flash the factory partition via USB, see developer manual for details.

    "); c.printf( "
    " "
    " "
    " "
    " "" "

    Version info %s

    " "
    " "
    " "
    %s
    " "
    " "
    " "" "
    " "
    " "
    " "
    " , _html(info.version_server) , _html(info.changelog_server)); c.print( "
    " "
    " "
    " "
    " "" "

    Flashing…

    " "
    " "
    " "
    "
              "
    " "
    " "" "" "
    " "
    " "
    " "
    " "\n" "
    \n" "\n" ""); c.done(); } #endif /** * HandleCfgLogging: configure logging (URL /cfg/logging) */ void OvmsWebServer::HandleCfgLogging(PageEntry_t& p, PageContext_t& c) { std::string error; OvmsConfigParam* param = MyConfig.CachedParam("log"); ConfigParamMap pmap; int i, max; char buf[100]; std::string file_path, tag, level; if (c.method == "POST") { // process form submission: pmap["file.enable"] = (c.getvar("file_enable") == "yes") ? "yes" : "no"; if (c.getvar("file_maxsize") != "") pmap["file.maxsize"] = c.getvar("file_maxsize"); if (c.getvar("file_keepdays") != "") pmap["file.keepdays"] = c.getvar("file_keepdays"); if (c.getvar("file_syncperiod") != "") pmap["file.syncperiod"] = c.getvar("file_syncperiod"); file_path = c.getvar("file_path"); pmap["file.path"] = file_path; if (pmap["file.enable"] == "yes" && !startsWith(file_path, "/sd/") && !startsWith(file_path, "/store/")) error += "
  • File must be on '/sd' or '/store'
  • "; pmap["level"] = c.getvar("level"); max = atoi(c.getvar("levelmax").c_str()); for (i = 1; i <= max; i++) { sprintf(buf, "tag_%d", i); tag = c.getvar(buf); if (tag == "") continue; sprintf(buf, "level_%d", i); level = c.getvar(buf); if (level == "") continue; snprintf(buf, sizeof(buf), "level.%s", tag.c_str()); pmap[buf] = level; } if (error == "") { // save: param->m_map.clear(); param->m_map = std::move(pmap); param->Save(); c.head(200); c.alert("success", "

    Logging configuration saved.

    "); OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { // read configuration: pmap = param->m_map; // generate form: c.head(200); } c.panel_start("primary", "Logging configuration"); c.form_start(p.uri); c.input_checkbox("Enable file logging", "file_enable", pmap["file.enable"] == "yes"); c.input_text("Log file path", "file_path", pmap["file.path"].c_str(), "Enter path on /sd or /store", "

    Logging to SD card will start automatically on mount. Do not remove the SD card while logging is active.

    " "

    " "

    "); std::string docroot = MyConfig.GetParamValue("http.server", "docroot", "/sd"); std::string download; if (startsWith(pmap["file.path"], docroot)) { std::string webpath = pmap["file.path"].substr(docroot.length()); download = "Open log file"; auto p = webpath.find_last_of('/'); if (p != std::string::npos) { std::string webdir = webpath.substr(0, p); if (webdir != docroot) download += " Open directory"; } } else { download = "You can access your logs with the browser if the path is in your webserver root (" + docroot + ")."; } c.input_info("Download", download.c_str()); c.input("number", "Sync period", "file_syncperiod", pmap["file.syncperiod"].c_str(), "Default: 3", "

    How often to flush log buffer to SD: 0 = never/auto, <0 = every n messages, >0 = after n/2 seconds idle

    ", "min=\"-1\" step=\"1\""); c.input("number", "Max file size", "file_maxsize", pmap["file.maxsize"].c_str(), "Default: 1024", "

    When exceeding the size, the log will be archived suffixed with date & time and a new file will be started. 0 = disable

    ", "min=\"0\" step=\"1\"", "kB"); c.input("number", "Expire time", "file_keepdays", pmap["file.keepdays"].c_str(), "Default: 30", "

    Automatically delete archived log files. 0 = disable

    ", "min=\"0\" step=\"1\"", "days"); auto gen_options = [&c](std::string level) { c.printf( "" "" "" "" "" "" , (level=="none") ? "selected" : "" , (level=="error") ? "selected" : "" , (level=="warn") ? "selected" : "" , (level=="info"||level=="") ? "selected" : "" , (level=="debug") ? "selected" : "" , (level=="verbose") ? "selected" : ""); }; c.input_select_start("Default level", "level"); gen_options(pmap["level"]); c.input_select_end(); c.print( "
    " "" "
    " "
    " "" "" "" "" "" "" "" "" ""); max = 0; for (auto &kv: pmap) { if (!startsWith(kv.first, "level.")) continue; max++; tag = kv.first.substr(6); c.printf( "" "" "" "" ""); } c.printf( "" "" "" "" "" "" "
    ComponentLevel
    " "" "
    " "
    " "
    " , max); c.input_button("default", "Save"); c.form_end(); c.print( ""); c.panel_end(); c.done(); } /** * HandleCfgLocations: configure GPS locations (URL /cfg/locations) */ void OvmsWebServer::HandleCfgLocations(PageEntry_t& p, PageContext_t& c) { std::string error; OvmsConfigParam* param = MyConfig.CachedParam("locations"); ConfigParamMap pmap; int i, max; char buf[100]; std::string latlon, name; int radius; double lat, lon; if (c.method == "POST") { // process form submission: max = atoi(c.getvar("loc").c_str()); for (i = 1; i <= max; i++) { sprintf(buf, "latlon_%d", i); latlon = c.getvar(buf); if (latlon == "") continue; lat = lon = 0; sscanf(latlon.c_str(), "%lf,%lf", &lat, &lon); if (lat == 0 || lon == 0) error += "
  • Invalid coordinates (enter latitude,longitude)
  • "; sprintf(buf, "radius_%d", i); radius = atoi(c.getvar(buf).c_str()); if (radius == 0) radius = 100; sprintf(buf, "name_%d", i); name = c.getvar(buf); if (name == "") error += "
  • Name must not be empty
  • "; snprintf(buf, sizeof(buf), "%f,%f,%d", lat, lon, radius); pmap[name] = buf; } if (error == "") { // save: param->m_map.clear(); param->m_map = std::move(pmap); param->Save(); c.head(200); c.alert("success", "

    Locations saved.

    "); OutputHome(p, c); c.done(); return; } // output error, return to form: error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { // read configuration: pmap = param->m_map; // generate form: c.head(200); } c.print( "\n" "\n"); c.panel_start("primary panel-single", "Locations"); c.form_start(p.uri); c.print( "
    " "" "" "" "" "" "" "" "" "" "" "" "" "" "" "
    " "" "
    " "
    \n" "\n" "\n" "
    \n" ); c.form_end(); c.print( ""); c.panel_end(); c.done(); } /** * HandleCfgBackup: config backup/restore (URL /cfg/backup) */ void OvmsWebServer::HandleCfgBackup(PageEntry_t& p, PageContext_t& c) { c.head(200); c.print( "\n" "\n" "
    \n" "
    Configuration Backup & Restore
    \n" "
    \n" "
    \n" "
    \n" "\n" "\n" "\n" "
    \n" "
    \n"
          "
    \n" "
    \n" "

    Use this tool to create or restore backups of your system configuration & scripts.\n" "User files or directories in /store will not be included or restored.\n" "ZIP files are password protected (hint: use 7z to unzip/create on a PC).

    \n" "

    Note: the module will perform a reboot after successful restore.

    \n" "
    \n" "
    \n" "\n" "\n" ); c.done(); } /** * HandleCfgPlugins: configure/edit web plugins (URL /cfg/plugins) */ static void OutputPluginList(PageEntry_t& p, PageContext_t& c) { c.print( "\n" "\n" "
    \n" "
    Webserver Plugins
    \n" "
    \n" "
    \n" "
    \n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "
    \n" "\n" "
    \n" "
    \n" "\n" "
    \n" "
    \n" "
    \n" "
    \n" "

    You can extend your OVMS web interface by using plugins. A plugin can be attached as a new page or hook into an existing page at predefined places.

    \n" "

    Plugin content is loaded from /store/plugin (covered by backups). Plugins currently must contain valid HTML (other mime types will be supported in the future).

    \n" "
    \n" "
    \n" "\n" "\n" ); } static bool SavePluginList(PageEntry_t& p, PageContext_t& c, std::string& error) { OvmsConfigParam* cp = MyConfig.CachedParam("http.plugin"); if (!cp) { error += "
  • Internal error: http.plugin config not found
  • "; return false; } const ConfigParamMap& pmap = cp->GetMap(); ConfigParamMap nmap; std::string key, type, enable, mode; int cnt = atoi(c.getvar("cnt").c_str()); char buf[20]; for (int i = 1; i <= cnt; i++) { sprintf(buf, "_%d", i); key = c.getvar(std::string("key")+buf); mode = c.getvar(std::string("mode")+buf); if (key == "" || mode == "") continue; type = c.getvar(std::string("type")+buf); enable = c.getvar(std::string("enable")+buf); nmap[key+".enable"] = enable; nmap[key+".page"] = (mode=="add") ? "" : cp->GetValue(key+".page"); if (type == "page") { nmap[key+".label"] = (mode=="add") ? "" : cp->GetValue(key+".label"); nmap[key+".menu"] = (mode=="add") ? "" : cp->GetValue(key+".menu"); nmap[key+".auth"] = (mode=="add") ? "" : cp->GetValue(key+".auth"); } else { nmap[key+".hook"] = (mode=="add") ? "" : cp->GetValue(key+".hook"); } } // deleted: for (auto& kv : pmap) { if (!endsWith(kv.first, ".enable")) continue; if (nmap.count(kv.first) == 0) { key = "/store/plugin/" + kv.first.substr(0, kv.first.length() - 7); unlink(key.c_str()); } } cp->SetMap(nmap); return true; } static void OutputPluginEditor(PageEntry_t& p, PageContext_t& c) { std::string key = c.getvar("key"); std::string type = c.getvar("type"); std::string page, hook, label, menu, auth; extram::string content; page = MyConfig.GetParamValue("http.plugin", key+".page"); if (type == "page") { label = MyConfig.GetParamValue("http.plugin", key+".label"); menu = MyConfig.GetParamValue("http.plugin", key+".menu"); auth = MyConfig.GetParamValue("http.plugin", key+".auth"); } else { hook = MyConfig.GetParamValue("http.plugin", key+".hook"); } // read plugin content: std::string path = "/store/plugin/" + key; std::ifstream file(path, std::ios::in | std::ios::binary | std::ios::ate); if (file.is_open()) { auto size = file.tellg(); if (size > 0) { content.resize(size, '\0'); file.seekg(0); file.read(&content[0], size); } } c.printf( "
    \n" "
    Plugin Editor: %s
    \n" "
    \n" , _html(key)); c.printf( "
    \n" "\n" "\n" , _attr(key) , _attr(type)); if (type == "page") { c.printf( "
    \n" "\n" "\n" "\n" "

    Note: framework URIs have priority. Use prefix /usr/… to avoid conflicts.

    \n" "
    \n" "
    \n" , _attr(page)); c.printf( "
    \n" "\n" "\n" "
    \n" , _attr(label)); c.printf( "
    \n" "
    \n" "\n" "\n" "
    \n" , (menu == "None") ? "selected" : "" , (menu == "Main") ? "selected" : "" , (menu == "Tools") ? "selected" : "" , (menu == "Config") ? "selected" : "" , (menu == "Vehicle") ? "selected" : ""); c.printf( "
    \n" "\n" "\n" "
    \n" "
    \n" , (auth == "None") ? "selected" : "" , (auth == "Cookie") ? "selected" : "" , (auth == "File") ? "selected" : ""); } else // type == "hook" { c.printf( "
    \n" "\n" "\n" "
    \n" , _attr(page)); c.printf( "
    \n" "\n" "\n" "\n" "\n" "
    \n" , _attr(hook)); } c.printf( "
    \n" "\n" "
    \n" "\n" "\n" "\n" "
    \n" "\n" "
    \n" , c.encode_html(content).c_str()); c.print( "
    \n" "\n" "\n" "\n" "
    \n" "
    \n" "
    \n" "
    \n" "\n" ); } static bool SavePluginEditor(PageEntry_t& p, PageContext_t& c, std::string& error) { OvmsConfigParam* cp = MyConfig.CachedParam("http.plugin"); if (!cp) { error += "
  • Internal error: http.plugin config not found
  • "; return false; } ConfigParamMap nmap = cp->GetMap(); std::string key = c.getvar("key"); std::string type = c.getvar("type"); extram::string content; nmap[key+".page"] = c.getvar("page"); if (type == "page") { nmap[key+".label"] = c.getvar("label"); nmap[key+".menu"] = c.getvar("menu"); nmap[key+".auth"] = c.getvar("auth"); } else { nmap[key+".hook"] = c.getvar("hook"); } cp->SetMap(nmap); // write plugin content: c.getvar("content", content); content = stripcr(content); mkpath("/store/plugin"); std::string path = "/store/plugin/" + key; std::ofstream file(path, std::ios::out | std::ios::binary | std::ios::trunc); if (file.is_open()) { file.write(content.data(), content.size()); } if (file.fail()) { error += "
  • Error writing to " + c.encode_html(path) + ": " + strerror(errno) + "
  • "; return false; } return true; } void OvmsWebServer::HandleCfgPlugins(PageEntry_t& p, PageContext_t& c) { std::string cnt = c.getvar("cnt"); std::string key = c.getvar("key"); std::string error, info; if (c.method == "POST") { if (cnt != "") { if (SavePluginList(p, c, error)) { info = "

    Plugin registration saved.

    " ""; } } else if (key != "") { if (SavePluginEditor(p, c, error)) { info = "

    Plugin " + c.encode_html(key) + " saved.

    " ""; key = ""; } } } if (error != "") { error = "

    Error!

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { c.head(200); if (info != "") c.alert("success", info.c_str()); } if (key == "") OutputPluginList(p, c); else OutputPluginEditor(p, c); c.done(); } /** * HandleEditor: simple text file editor */ void OvmsWebServer::HandleEditor(PageEntry_t& p, PageContext_t& c) { std::string error, info; std::string path = c.getvar("path"); extram::string content; if (MyConfig.ProtectedPath(path)) { c.head(400); c.alert("danger", "

    Error: protected path

    "); c.done(); return; } if (c.method == "POST") { bool got_content = c.getvar("content", content); content = stripcr(content); if (path == "" || path.front() != '/' || path.back() == '/') { error += "
  • Missing or invalid path
  • "; } else if (!got_content) { error += "
  • Missing content
  • "; } else { // create path: size_t n = path.rfind('/'); if (n != 0 && n != std::string::npos) { std::string dir = path.substr(0, n); if (!path_exists(dir)) { if (mkpath(dir) != 0) error += "
  • Error creating path " + c.encode_html(dir) + ": " + strerror(errno) + "
  • "; else info += "

    Path " + c.encode_html(dir) + " created.

    "; } } // write file: if (error == "") { std::ofstream file(path, std::ios::out | std::ios::binary | std::ios::trunc); if (file.is_open()) file.write(content.data(), content.size()); if (file.fail()) { error += "
  • Error writing to " + c.encode_html(path) + ": " + strerror(errno) + "
  • "; } else { info += "

    File " + c.encode_html(path) + " saved.

    "; MyEvents.SignalEvent("system.vfs.file.changed", (void*)path.c_str(), path.size()+1); } } } } else { if (path == "") { path = "/store/"; } else if (path.back() != '/') { // read file: std::ifstream file(path, std::ios::in | std::ios::binary | std::ios::ate); if (file.is_open()) { auto size = file.tellg(); if (size > 0) { content.resize(size, '\0'); file.seekg(0); file.read(&content[0], size); } } } } // output: if (error != "") { error = "

    Error:

      " + error + "
    "; c.head(400); c.alert("danger", error.c_str()); } else { c.head(200); if (info != "") c.alert("success", info.c_str()); } c.printf( "\n" "
    \n" "
    Text Editor
    \n" "
    \n" "
    \n" "
    \n" "
    \n" "\n" "\n" "
    \n" "
    \n" , _attr(p.uri), _attr(path)); c.printf( "
    \n" "
    \n" "\n" "\n" "\n" "
    \n" "\n" "
    \n" "
    \n" "
    \n" "
    \n" "\n" "\n" "
    \n" "
    \n" "
    \n" "
    \n" "\n" "\n" "\n" "\n" "
    \n" "
    \n" "
    \n" "
    \n" "
    \n" "
    \n"
          "
    \n" "
    \n" "

    Hints: you don't need to save to evaluate Javascript code. See user guide on how to test a module lib plugin w/o saving and reloading.\n" "Use a second session to test a web plugin.

    \n" "
    \n" "
    \n" , c.encode_html(content).c_str()); c.print( "\n" ); c.done(); } /** * HandleFile: file load/save API * * URL: /api/file * * @param method * GET = load content from path * POST = save content to path * @param path * Full path to file * @param content * File content for POST * * @return * Status: 200 (OK) / 400 (Error) * Body: GET: file content or error message, POST: empty or error message */ void OvmsWebServer::HandleFile(PageEntry_t& p, PageContext_t& c) { std::string error; std::string path = c.getvar("path"); extram::string content; std::string headers = "Content-Type: application/octet-stream; charset=utf-8\r\n" "Cache-Control: no-cache"; if (MyConfig.ProtectedPath(path)) { c.head(400, headers.c_str()); c.print("ERROR: Protected path\n"); c.done(); return; } if (c.method == "POST") { bool got_content = c.getvar("content", content); if (path == "" || path.front() != '/' || path.back() == '/') { error += "; Missing or invalid path"; } else if (!got_content) { error += "; Missing content"; } else { // write file: if (save_file(path, content) != 0) { error += "; Error writing to path: "; error += strerror(errno); } } } else { if (path == "") { path = "/store/"; } else if (path.back() != '/') { // read file: if (load_file(path, content) != 0) { error += "; Error reading from path: "; error += strerror(errno); } } } // output: if (!error.empty()) { c.head(400, headers.c_str()); c.print("ERROR: "); c.print(error.substr(2)); // skip "; " intro c.print("\n"); } else { c.head(200); if (c.method == "GET") { c.print(content); } } c.done(); }