OVMS3/OVMS.V3/components/ovms_webserver/src/web_displays.cpp

1420 lines
56 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
; 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"
// static const char *TAG = "webserver";
#include <string.h>
#include <stdio.h>
#include <string>
#include <sstream>
#include <iomanip>
#include <dirent.h>
#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"
#define _attr(text) (c.encode_html(text).c_str())
#define _html(text) (c.encode_html(text).c_str())
/**
* HandleDashboard:
*/
void OvmsWebServer::HandleDashboard(PageEntry_t& p, PageContext_t& c)
{
OvmsVehicle *vehicle = MyVehicleFactory.ActiveVehicle();
if (!vehicle) {
c.head(400);
c.alert("danger", "<p class=\"lead\">Error: no active vehicle!</p>");
c.done();
return;
}
// get dashboard configuration:
DashboardConfig cfg;
vehicle->GetDashboardConfig(cfg);
// output dashboard:
const char* content =
"<style>"
""
"@media (max-width: 767px) {"
".panel-single .panel-body {"
"padding: 2px;"
"}"
"}"
""
".highcharts-plot-band, .highcharts-pane {"
"fill-opacity: 0;"
"}"
""
".speed-default-pane {"
"fill: #95a222;"
"}"
".night .speed-default-pane {"
"fill: #151515;"
"fill-opacity: 0.5;"
"}"
""
".speed-outer-pane {"
" fill: #EFEFEF;"
"}"
".speed-middle-pane {"
" stroke-width: 1px;"
" stroke: #AAA;"
"}"
".night .highcharts-pane.speed-middle-pane {"
"stroke-width: 2px;"
"}"
".speed-inner-pane {"
" fill: #DDDDDD;"
"}"
""
".highcharts-plot-band.border {"
"stroke: #666666;"
"stroke-width: 1px;"
"}"
""
".green-band {"
" fill: #55BF3B;"
" fill-opacity: 0.4;"
"}"
".yellow-band {"
" fill: #DDDF0D;"
" fill-opacity: 0.5;"
"}"
".red-band {"
" fill: #DF5353;"
" fill-opacity: 0.6;"
"}"
".violet-band {"
"fill: #9622ff;"
"fill-opacity: 0.6;"
"}"
".night .violet-band {"
"fill: #9622ff;"
"fill-opacity: 0.8;"
"}"
""
".highcharts-gauge-series .highcharts-pivot {"
"stroke-width: 1px;"
"stroke: #757575;"
"fill-opacity: 1;"
"fill: black;"
"}"
""
".highcharts-gauge-series.auxgauge .highcharts-pivot {"
"fill-opacity: 1;"
"fill: #fff;"
"stroke-width: 0;"
"}"
".night .highcharts-gauge-series.auxgauge .highcharts-pivot {"
"fill: #000;"
"}"
""
".highcharts-gauge-series .highcharts-dial {"
"fill: #d80000;"
"stroke: #000;"
"stroke-width: 0.5px;"
"}"
""
".highcharts-yaxis-grid .highcharts-grid-line,"
".highcharts-yaxis-grid .highcharts-minor-grid-line {"
" display: none;"
"}"
""
".highcharts-yaxis .highcharts-tick {"
" stroke-width: 2px;"
" stroke: #666666;"
"}"
".night .highcharts-yaxis .highcharts-tick {"
"stroke: #e0e0e0;"
"}"
""
".highcharts-yaxis .highcharts-minor-tick {"
"stroke-width: 1.8px;"
"stroke: #00000085;"
"stroke-dasharray: 6;"
"stroke-dashoffset: -4.8;"
"stroke-linecap: round;"
"}"
".night .highcharts-yaxis .highcharts-minor-tick {"
"stroke: #ffffff85;"
"}"
""
".highcharts-axis-labels {"
"fill: #000;"
"font-weight: bold;"
"font-size: 0.9em;"
"}"
".night .highcharts-axis-labels {"
"fill: #ddd;"
"}"
""
".highcharts-data-label text {"
"fill: #333333;"
"}"
".night .highcharts-data-label text {"
"fill: #fff;"
"}"
""
".highcharts-axis-labels.speed {"
"font-size: 1.2em;"
"}"
""
".dashboard .highcharts-data-label text {"
"font-size: 2em;"
"}"
""
".dashboard .overlay {"
"position: absolute;"
"z-index: 10;"
"top: 62%;"
"text-align: center;"
"left: 0%;"
"right: 0%;"
"}"
".dashboard .underlay {"
"position: absolute;"
"z-index: 0;"
"width: 100%;"
"height: 100%;"
"overflow: hidden;"
"}"
".dashboard .value {"
"color: #04282b;"
"background: #97b597;"
"padding: 2px 5px;"
"margin-right: 0.2em;"
"text-align: center;"
"border: 1px inset #c3c3c380;"
"font-family: \"Monaco\", \"Menlo\", \"Consolas\", \"QuickType Mono\", \"Lucida Console\", \"Roboto Mono\", \"Ubuntu Mono\", \"DejaVu Sans Mono\", \"Droid Sans Mono\", monospace;"
"display: inline-block;"
"}"
".night .dashboard .value {"
"color: #fff;"
"background: #252525;"
"}"
".dashboard .unit {"
"color: #666;"
"font-size: 12px;"
"}"
".dashboard .range-value .value {"
"margin-left: 10px;"
"width: 100px;"
"}"
".dashboard .energy-value {"
"margin-top: 4px;"
"}"
".dashboard .energy-value .value {"
"margin-left: 18px;"
"width: 100px;"
"font-size: 12px;"
"}"
".dashboard .underlay .value {"
"font-size: 12px;"
"width: 32px;"
"padding: 2px 1px 0 0;"
"margin: 0;"
"position: absolute;"
"}"
".dashboard .underlay > div {"
"position: absolute;"
"display: inline-block;"
"}"
".dashboard .voltage-value .value {"
"left: 80px;"
"top: 40px;"
"}"
".dashboard .soc-value .value {"
"left: 80px;"
"top: 40px;"
"}"
".dashboard .consumption-value .value {"
"left: -112px;"
"top: 40px;"
"}"
".dashboard .power-value .value {"
"left: -112px;"
"top: 40px;"
"}"
"@media (min-width: 0px) and (max-width: 404px) {"
".dashboard .voltage-value { left: -20%; top: 20%; }"
".dashboard .soc-value { left: -20%; top: 60%; }"
".dashboard .consumption-value { left: 120%; top: 20%; }"
".dashboard .power-value { left: 120%; top: 60%; }"
"}"
"@media (min-width: 405px) and (max-width: 454px) {"
".dashboard .voltage-value { left: -15%; top: 20%; }"
".dashboard .soc-value { left: -15%; top: 60%; }"
".dashboard .consumption-value { left: 115%; top: 20%; }"
".dashboard .power-value { left: 115%; top: 60%; }"
"}"
"@media (min-width: 455px) and (max-width: 604px) {"
".dashboard .voltage-value { left: -10%; top: 20%; }"
".dashboard .soc-value { left: -10%; top: 60%; }"
".dashboard .consumption-value { left: 110%; top: 20%; }"
".dashboard .power-value { left: 110%; top: 60%; }"
"}"
"@media (min-width: 605px) {"
".dashboard .voltage-value { left: 0%; top: 20%; }"
".dashboard .soc-value { left: 0%; top: 60%; }"
".dashboard .consumption-value { left: 100%; top: 20%; }"
".dashboard .power-value { left: 100%; top: 60%; }"
"}"
"</style>"
""
"<div class=\"panel panel-primary\" id=\"panel-dashboard\">"
"<div class=\"panel-heading\">Dashboard</div>"
"<div class=\"panel-body\">"
"<div class=\"receiver get-window-resize\" id=\"livestatus\">"
"<div class=\"dashboard\" style=\"position: relative; width: 100%; height: 300px; margin: 0 auto\">"
"<div class=\"overlay\">"
"<div class=\"range-value\"><span class=\"value\">▼0 ▲0</span><span class=\"unit\">km</span></div>"
"<div class=\"energy-value\"><span class=\"value\">▲0.0 ▼0.0</span><span class=\"unit\">kWh</span></div>"
"</div>"
"<div class=\"underlay\">"
"<div class=\"voltage-value\"><span class=\"value\">0</span></div>"
"<div class=\"soc-value\"><span class=\"value\">0</span><span class=\"unit\">%</span></div>"
"<div class=\"consumption-value\"><span class=\"value\">0</span></div>"
"<div class=\"power-value\"><span class=\"value\">0</span></div>"
"</div>"
"<div id=\"gaugeset1\" style=\"width: 100%; height: 100%;\"></div>"
"</div>"
"</div>"
"</div>"
"</div>"
""
"<script type=\"text/javascript\">"
""
"var gaugeset1;"
""
"function get_dashboard_data() {"
"var rmin = metrics_user[\"v.b.range.est\"]||0, rmax = metrics_user[\"v.b.range.ideal\"]||0;\n"
"var euse = metrics_user[\"v.b.energy.used\"]||0, erec = metrics_user[\"v.b.energy.recd\"]||0;\n"
"var voltage = metrics[\"v.b.voltage\"]||0, soc = metrics_user[\"v.b.soc\"]||0;\n"
"var consumption = metrics_user[\"v.b.consumption\"]||0, power = metrics_user[\"v.b.power\"]||0;\n"
"euse = Math.floor(euse*10)/10; erec = Math.floor(erec*10)/10;"
"if (rmin > rmax) { var x = rmin; rmin = rmax; rmax = x; }"
"var md = {"
"range: { value: \"\" + rmin.toFixed(0) + \"\" + rmax.toFixed(0), unit: metrics_label[\"v.b.range.est\"] },"
"energy: { value: \"\" + euse.toFixed(1) + \"\" + erec.toFixed(1), unit: metrics_label[\"v.b.energy.used\"] },"
"voltage: { value: voltage.toFixed(0) },"
"soc: { value: soc.toFixed(0), unit: metrics_label[\"v.b.soc\"] },"
"consumption: { value: consumption.toFixed(0), unit: metrics_label[\"v.b.consumption\"] },"
"power: { value: power.toFixed(0), unit: metrics_label[\"v.b.power\"] },"
"series: ["
"{ data: [metrics_user[\"v.p.speed\"]] },"
"{ data: [metrics[\"v.b.voltage\"]] },"
"{ data: [metrics_user[\"v.b.soc\"]] },"
"{ data: [metrics_user[\"v.b.consumption\"]] },"
"{ data: [metrics_user[\"v.b.power\"]] },"
"{ data: [metrics_user[\"v.c.temp\"]] },"
"{ data: [metrics_user[\"v.b.temp\"]] },"
"{ data: [metrics_user[\"v.i.temp\"]] },"
"{ data: [metrics_user[\"v.m.temp\"]] }],"
"};"
"return md;"
"}"
""
"function update_dashboard() {"
"var md = get_dashboard_data();"
"$('.range-value .value').text(md.range.value);"
"$('.range-value .unit').text(md.range.unit);"
"$('.energy-value .value').text(md.energy.value);"
"$('.energy-value .unit').text(md.energy.unit);"
"$('.voltage-value .value').text(md.voltage.value);"
"$('.soc-value .value').text(md.soc.value);"
"$('.soc-value .unit').text(md.soc.unit);"
"$('.consumption-value .value').text(md.consumption.value);"
"$('.power-value .value').text(md.power.value);"
"gaugeset1.update({ series: md.series });"
"}"
""
"function init_gaugeset1() {"
"var chart_config = {"
"chart: {"
"type: 'gauge',"
"spacing: [0, 0, 0, 0],"
"margin: [0, 0, 0, 0],"
"animation: { duration: 0, easing: 'swing' },"
"},"
"title: { text: null },"
"credits: { enabled: false },"
"tooltip: { enabled: false },"
""
"pane: ["
"{ startAngle: -125, endAngle: 125, center: ['50%', '45%'], size: '80%' }, /* Speed */"
"{ startAngle: 70, endAngle: 110, center: ['-20%', '20%'], size: '100%' }, /* Voltage */"
"{ startAngle: 70, endAngle: 110, center: ['-20%', '60%'], size: '100%' }, /* SOC */"
"{ startAngle: -110, endAngle: -70, center: ['120%', '20%'], size: '100%' }, /* Efficiency */"
"{ startAngle: -110, endAngle: -70, center: ['120%', '60%'], size: '100%' }, /* Power */"
"{ startAngle: -45, endAngle: 45, center: ['20%', '100%'], size: '30%' }, /* Charger temperature */"
"{ startAngle: -45, endAngle: 45, center: ['40%', '100%'], size: '30%' }, /* Battery temperature */"
"{ startAngle: -45, endAngle: 45, center: ['60%', '100%'], size: '30%' }, /* Inverter temperature */"
"{ startAngle: -45, endAngle: 45, center: ['80%', '100%'], size: '30%' }], /* Motor temperature */"
""
"responsive: {"
"rules: [{"
"condition: { minWidth: 0, maxWidth: 400 },"
"chartOptions: {"
"pane: ["
"{ size: '60%' }, /* Speed */"
"{ center: ['-20%', '20%'] }, /* Voltage */"
"{ center: ['-20%', '60%'] }, /* SOC */"
"{ center: ['120%', '20%'] }, /* Efficiency */"
"{ center: ['120%', '60%'] }, /* Power */"
"{ center: ['15%', '100%'] , size: '25%' }, /* Charger temperature */"
"{ center: ['38.33%', '100%'], size: '25%' }, /* Battery temperature */"
"{ center: ['61.66%', '100%'], size: '25%' }, /* Inverter temperature */"
"{ center: ['85%', '100%'] , size: '25%' }], /* Motor temperature */"
"yAxis: [{ labels: { step: 1 } }], /* Speed */"
"},"
"},{"
"condition: { minWidth: 401, maxWidth: 450 },"
"chartOptions: {"
"pane: ["
"{ size: '70%' }, /* Speed */"
"{ center: ['-15%', '20%'] }, /* Voltage */"
"{ center: ['-15%', '60%'] }, /* SOC */"
"{ center: ['115%', '20%'] }, /* Efficiency */"
"{ center: ['115%', '60%'] }, /* Power */"
"{ center: ['15%', '100%'] , size: '27.5%' }, /* Charger temperature */"
"{ center: ['38.33%', '100%'], size: '27.5%' }, /* Battery temperature */"
"{ center: ['61.66%', '100%'], size: '27.5%' }, /* Inverter temperature */"
"{ center: ['85%', '100%'] , size: '27.5%' }], /* Motor temperature */"
"},"
"},{"
"condition: { minWidth: 451, maxWidth: 600 },"
"chartOptions: {"
"pane: ["
"{ size: '80%' }, /* Speed */"
"{ center: ['-10%', '20%'] }, /* Voltage */"
"{ center: ['-10%', '60%'] }, /* SOC */"
"{ center: ['110%', '20%'] }, /* Efficiency */"
"{ center: ['110%', '60%'] }], /* Power */"
"},"
"},{"
"condition: { minWidth: 601 },"
"chartOptions: {"
"pane: ["
"{ size: '85%' }, /* Speed */"
"{ center: ['0%', '20%'] }, /* Voltage */"
"{ center: ['0%', '60%'] }, /* SOC */"
"{ center: ['100%', '20%'] }, /* Efficiency */"
"{ center: ['100%', '60%'] }], /* Power */"
"},"
"}]"
"},"
""
"yAxis: [{"
"/* Speed axis: */"
"pane: 0, className: 'speed', title: { text: 'km/h' },"
"reversed: false,"
"minorTickInterval: 'auto', minorTickLength: 5, minorTickPosition: 'inside',"
"tickPixelInterval: 30, tickPosition: 'inside', tickLength: 13,"
"labels: { step: 2, distance: -28, x: 0, y: 5, zIndex: 2 },"
"},{"
"/* Voltage axis: */"
"pane: 1, className: 'voltage', title: { text: 'Volt', align: 'low', x: 85, y: 35, },"
"reversed: true,"
"minorTickInterval: 'auto', minorTickLength: 5, minorTickPosition: 'inside',"
"tickPixelInterval: 30, tickPosition: 'inside', tickLength: 13,"
"labels: { step: 1, distance: -25, x: 0, y: 5, zIndex: 2 },"
"},{"
"/* SOC axis: */"
"pane: 2, className: 'soc', title: { text: 'SOC', align: 'low', x: 82.5, y: 35 },"
"reversed: true,"
"minorTickInterval: 'auto', minorTickLength: 5, minorTickPosition: 'inside',"
"tickPixelInterval: 30, tickPosition: 'inside', tickLength: 13,"
"labels: { step: 1, distance: -25, x: 0, y: 5, zIndex: 2 },"
"},{"
"/* Efficiency axis: */"
"pane: 3, className: 'efficiency', title: { text: 'Wh/km', align: 'low', x: -116, y: 35 },"
"reversed: false,"
"minorTickInterval: 'auto', minorTickLength: 5, minorTickPosition: 'inside',"
"tickPixelInterval: 30, tickPosition: 'inside', tickLength: 13,"
"labels: { step: 1, distance: -25, x: 0, y: 5, zIndex: 2 },"
"},{"
"/* Power axis: */"
"pane: 4, className: 'power', title: { text: 'kW', align: 'low', x: -105, y: 35 },"
"reversed: false,"
"minorTickInterval: 'auto', minorTickLength: 5, minorTickPosition: 'inside',"
"tickPixelInterval: 30, tickPosition: 'inside', tickLength: 13,"
"labels: { step: 1, distance: -25, x: 0, y: 5, zIndex: 2 },"
"},{"
"/* Charger temperature axis: */"
"pane: 5, className: 'temp-charger', title: { text: 'CHG °C', y: 10 },"
"tickPosition: 'inside', tickLength: 10, minorTickInterval: null,"
"labels: { step: 1, distance: 3, x: 0, y: 0, zIndex: 2 },"
"},{"
"/* Battery temperature axis: */"
"pane: 6, className: 'temp-battery', title: { text: 'BAT °C', y: 10 },"
"tickPosition: 'inside', tickLength: 10, minorTickInterval: null,"
"labels: { step: 1, distance: 3, x: 0, y: 0, zIndex: 2 },"
"},{"
"/* Inverter temperature axis: */"
"pane: 7, className: 'temp-inverter', title: { text: 'PEM °C', y: 10 },"
"tickPosition: 'inside', tickLength: 10, minorTickInterval: null,"
"labels: { step: 1, distance: 3, x: 0, y: 0, zIndex: 2 },"
"},{"
"/* Motor temperature axis: */"
"pane: 8, className: 'temp-motor', title: { text: 'MOT °C', y: 10 },"
"tickPosition: 'inside', tickLength: 10, minorTickInterval: null,"
"labels: { step: 1, distance: 3, x: 0, y: 0, zIndex: 2 },"
"}],"
""
"plotOptions: {"
"gauge: {"
"dataLabels: { enabled: false },"
"overshoot: 1"
"}"
"},"
"series: [{"
"yAxis: 0, name: 'Speed', className: 'speed fullgauge', data: [0],"
"pivot: { radius: '10' },"
"dial: { radius: '88%', topWidth: 1, baseLength: '20%', baseWidth: 10, rearLength: '20%' },"
"},{"
"yAxis: 1, name: 'Voltage', className: 'voltage auxgauge', data: [0],"
"pivot: { radius: '85' },"
"dial: { radius: '95%', baseWidth: 5, baseLength: '90%' },"
"},{"
"yAxis: 2, name: 'SOC', className: 'soc auxgauge', data: [0],"
"pivot: { radius: '85' },"
"dial: { radius: '95%', baseWidth: 5, baseLength: '90%' },"
"},{"
"yAxis: 3, name: 'Efficiency', className: 'efficiency auxgauge', data: [0],"
"pivot: { radius: '85' },"
"dial: { radius: '95%', baseWidth: 5, baseLength: '90%' },"
"},{"
"yAxis: 4, name: 'Power', className: 'power auxgauge', data: [0],"
"pivot: { radius: '85' },"
"dial: { radius: '95%', baseWidth: 5, baseLength: '90%' },"
"},{"
"yAxis: 5, name: 'Charger temperature', className: 'temp-charger tempgauge', data: [0],"
"dial: { radius: '90%', baseWidth: 3, baseLength: '90%' },"
"},{"
"yAxis: 6, name: 'Battery temperature', className: 'temp-battery tempgauge', data: [0],"
"dial: { radius: '90%', baseWidth: 3, baseLength: '90%' },"
"},{"
"yAxis: 7, name: 'Inverter temperature', className: 'temp-inverter tempgauge', data: [0],"
"dial: { radius: '90%', baseWidth: 3, baseLength: '90%' },"
"},{"
"yAxis: 8, name: 'Motor temperature', className: 'temp-motor tempgauge', data: [0],"
"dial: { radius: '90%', baseWidth: 3, baseLength: '90%' },"
"}]"
"};"
""
"/* Inject vehicle config: */"
"for (var i = 0; i < chart_config.yAxis.length; i++) {"
"$.extend(true, chart_config.yAxis[i], vehicle_config.yAxis[i]);"
"}"
""
"gaugeset1 = Highcharts.chart('gaugeset1', chart_config,"
"function (chart) {"
"chart.update({ chart: { animation: { duration: 1000, easing: 'swing' } } });"
"$('#livestatus').on(\"msg:metrics\", function(e, update){"
"update_dashboard();"
"}).on(\"window-resize\", function(e){"
"chart.reflow();"
"});"
"}"
");"
"$('#gaugeset1').data('chart', gaugeset1).addClass('has-chart');"
"}"
""
"function init_charts() {"
"init_gaugeset1();"
"}"
""
"if (window.Highcharts) {"
"init_charts();"
"} else {"
"$.ajax({"
"url: \"" URL_ASSETS_CHARTS_JS "\","
"dataType: \"script\","
"cache: true,"
"success: function(){ init_charts(); }"
"});"
"}"
""
"</script>";
c.head(200);
PAGE_HOOK("body.pre");
c.printf(
"<script type=\"text/javascript\">"
"var vehicle_config = {"
"%s"
"};"
"</script>"
, cfg.gaugeset1.c_str());
new HttpDataSender(c.nc, (const uint8_t*)content, strlen(content));
}
/**
* HandleBmsCellMonitor: display cell voltages & temperatures
*
* Note: this is not enabled by default, as some vehicles do not provide BMS data.
* To enable, include this in the vehicle init:
* MyWebServer.RegisterPage("/bms/cellmon", "BMS cell monitor", OvmsWebServer::HandleBmsCellMonitor, PageMenu_Vehicle, PageAuth_Cookie);
* You can change the URL path, title, menu association and authentication as you like.
* For a clean shutdown, add
* MyWebServer.DeregisterPage("/bms/cellmon");
* …in your vehicle cleanup.
*/
void OvmsWebServer::HandleBmsCellMonitor(PageEntry_t& p, PageContext_t& c)
{
float stemwidth_v = 0.5, stemwidth_t = 0.5;
float
volt_warn_def = BMS_DEFTHR_VWARN, volt_alert_def = BMS_DEFTHR_VALERT,
volt_maxgrad_def = BMS_DEFTHR_VMAXGRAD, volt_maxsddev_def = BMS_DEFTHR_VMAXSDDEV,
temp_warn_def = BMS_DEFTHR_TWARN, temp_alert_def = BMS_DEFTHR_TALERT;
float
volt_warn = 0, volt_alert = 0,
volt_maxgrad = 0, volt_maxsddev = 0,
temp_warn = 0, temp_alert = 0;
bool alerts_enabled = true;
// get vehicle BMS configuration:
OvmsVehicle* vehicle = MyVehicleFactory.ActiveVehicle();
if (vehicle) {
int readings_v = vehicle->BmsGetCellArangementVoltage();
if (readings_v) {
stemwidth_v = 0.1 + 20.0 / readings_v; // 14 → 1.5 … 96 → 0.3
}
int readings_t = vehicle->BmsGetCellArangementTemperature();
if (readings_t) {
stemwidth_t = 0.1 + 10.0 / readings_t; // 7 → 1.5 … 96 → 0.4
}
vehicle->BmsGetCellDefaultThresholdsVoltage(&volt_warn_def, &volt_alert_def, &volt_maxgrad_def, &volt_maxsddev_def);
vehicle->BmsGetCellDefaultThresholdsTemperature(&temp_warn_def, &temp_alert_def);
}
if (c.method == "POST") {
// process form submission:
volt_warn = atof(c.getvar("volt_warn").c_str()) / 1000;
if (volt_warn > 0)
MyConfig.SetParamValueFloat("vehicle", "bms.dev.voltage.warn", volt_warn);
else
MyConfig.SetParamValue("vehicle", "bms.dev.voltage.warn", "");
volt_alert = atof(c.getvar("volt_alert").c_str()) / 1000;
if (volt_alert > 0)
MyConfig.SetParamValueFloat("vehicle", "bms.dev.voltage.alert", volt_alert);
else
MyConfig.SetParamValue("vehicle", "bms.dev.voltage.alert", "");
volt_maxgrad = atof(c.getvar("volt_maxgrad").c_str()) / 1000;
if (volt_maxgrad > 0)
MyConfig.SetParamValueFloat("vehicle", "bms.dev.voltage.maxgrad", volt_maxgrad);
else
MyConfig.SetParamValue("vehicle", "bms.dev.voltage.maxgrad", "");
volt_maxsddev = atof(c.getvar("volt_maxsddev").c_str()) / 1000;
if (volt_maxsddev > 0)
MyConfig.SetParamValueFloat("vehicle", "bms.dev.voltage.maxsddev", volt_maxsddev);
else
MyConfig.SetParamValue("vehicle", "bms.dev.voltage.maxsddev", "");
temp_warn = atof(c.getvar("temp_warn").c_str());
if (temp_warn > 0)
MyConfig.SetParamValueFloat("vehicle", "bms.dev.temp.warn", temp_warn);
else
MyConfig.SetParamValue("vehicle", "bms.dev.temp.warn", "");
temp_alert = atof(c.getvar("temp_alert").c_str());
if (temp_alert > 0)
MyConfig.SetParamValueFloat("vehicle", "bms.dev.temp.alert", temp_alert);
else
MyConfig.SetParamValue("vehicle", "bms.dev.temp.alert", "");
MyConfig.SetParamValueBool("vehicle", "bms.alerts.enabled", c.getvar("alerts_enabled") == "yes");
if (c.getvar("action") == "save-reset" && vehicle)
vehicle->BmsResetCellStats();
c.head(200);
c.alert("success", "<p>Saved.</p>");
c.print("<script>after(2, function(){ $('.modal').modal('hide'); });</script>");
c.done();
return;
}
// read config:
volt_warn = MyConfig.GetParamValueFloat("vehicle", "bms.dev.voltage.warn");
volt_alert = MyConfig.GetParamValueFloat("vehicle", "bms.dev.voltage.alert");
volt_maxgrad = MyConfig.GetParamValueFloat("vehicle", "bms.dev.voltage.maxgrad");
volt_maxsddev = MyConfig.GetParamValueFloat("vehicle", "bms.dev.voltage.maxsddev");
temp_warn = MyConfig.GetParamValueFloat("vehicle", "bms.dev.temp.warn");
temp_alert = MyConfig.GetParamValueFloat("vehicle", "bms.dev.temp.alert");
alerts_enabled = MyConfig.GetParamValueBool("vehicle", "bms.alerts.enabled", true);
c.head(200);
PAGE_HOOK("body.pre");
c.print(
"<div class=\"panel panel-primary panel-single receiver\" id=\"livestatus\">\n"
"<div class=\"panel-heading\">BMS Cell Monitor</div>\n"
"<div class=\"panel-body\">\n"
"<div class=\"row\">\n"
"<div id=\"voltchart\" style=\"width: 100%; max-width: 100%; height: 45vh; min-height: 280px; margin: 0 auto\"></div>\n"
"<div id=\"tempchart\" style=\"width: 100%; max-width: 100%; height: 25vh; min-height: 160px; margin: 0 auto\"></div>\n"
"</div>\n"
"</div>\n"
"<div class=\"panel-footer\">\n"
"<table class=\"table table-bordered table-condensed\">\n"
"<tbody>\n"
"<tr>\n"
"<th>Voltage</th>\n"
"<td>\n"
"<div class=\"metric number\" data-prec=\"3\" data-metric=\"v.b.p.voltage.avg\"><span class=\"label\">Cell avg</span><span class=\"value\"></span><span class=\"unit\">V</span></div>\n"
"<div class=\"metric number\" data-prec=\"3\" data-metric=\"v.b.p.voltage.min\"><span class=\"label\">Cell min</span><span class=\"value\"></span><span class=\"unit\">V</span></div>\n"
"<div class=\"metric number\" data-prec=\"3\" data-metric=\"v.b.p.voltage.max\"><span class=\"label\">Cell max</span><span class=\"value\"></span><span class=\"unit\">V</span></div>\n"
"<div class=\"metric number\" data-prec=\"1\" data-scale=\"1000\" data-metric=\"v.b.p.voltage.stddev\"><span class=\"label\">StdDev</span><span class=\"value\"></span><span class=\"unit\">mV</span></div>\n"
"<div class=\"metric number\" data-prec=\"1\" data-scale=\"1000\" data-metric=\"v.b.p.voltage.stddev.max\"><span class=\"label\">StdDev max</span><span class=\"value\"></span><span class=\"unit\">mV</span></div>\n"
"<div class=\"metric number\" data-prec=\"1\" data-scale=\"1000\" data-metric=\"v.b.p.voltage.grad\"><span class=\"label\">Gradient</span><span class=\"value\"></span><span class=\"unit\">mV</span></div>\n"
"</td>\n"
"</tr>\n"
"<tr>\n"
"<th>Temperature</th>\n"
"<td>\n"
"<div class=\"metric number\" data-prec=\"1\" data-metric=\"v.b.p.temp.avg\"><span class=\"label\">Cell avg</span><span class=\"value\"></span><span class=\"unit\">°C</span></div>\n"
"<div class=\"metric number\" data-prec=\"1\" data-metric=\"v.b.p.temp.min\"><span class=\"label\">Cell min</span><span class=\"value\"></span><span class=\"unit\">°C</span></div>\n"
"<div class=\"metric number\" data-prec=\"1\" data-metric=\"v.b.p.temp.max\"><span class=\"label\">Cell max</span><span class=\"value\"></span><span class=\"unit\">°C</span></div>\n"
"<div class=\"metric number\" data-prec=\"1\" data-metric=\"v.b.p.temp.stddev\"><span class=\"label\">StdDev</span><span class=\"value\"></span><span class=\"unit\">°C</span></div>\n"
"<div class=\"metric number\" data-prec=\"1\" data-metric=\"v.b.p.temp.stddev.max\"><span class=\"label\">StdDev max</span><span class=\"value\"></span><span class=\"unit\">°C</span></div>\n"
"</td>\n"
"</tr>\n"
"</tbody>\n"
"</table>\n"
"<button class=\"btn btn-default\" data-toggle=\"modal\" data-target=\"#cfg-dialog\">Alert config</button>\n"
"<button class=\"btn btn-default\" data-cmd=\"bms reset\" data-target=\"#output\" data-watchcnt=\"0\">Reset min/max</button>\n"
"<samp id=\"output\" class=\"samp-inline\"></samp>\n"
"</div>\n"
"</div>\n"
"\n"
"<div class=\"modal fade\" id=\"cfg-dialog\" role=\"dialog\" data-backdrop=\"true\" data-keyboard=\"true\">\n"
"<div class=\"modal-dialog modal-lg\">\n"
"<div class=\"modal-content\">\n"
"<div class=\"modal-header\">\n"
"<button type=\"button\" class=\"close\" data-dismiss=\"modal\">&times;</button>\n"
"<h4 class=\"modal-title\">Alert configuration</h4>\n"
"</div>\n"
"<div class=\"modal-body\">\n");
// Alert configuration form:
std::ostringstream sval, sdef;
sval << std::fixed;
sdef << std::fixed;
c.form_start(p.uri, "#cfg-result");
c.input_info("Thresholds",
"<p>Deviation warnings and alerts are triggered by cell deviation from the current average.</p>"
"<p>You may need to change the thresholds to adapt to the quality and age of your battery.</p>");
sval << std::setprecision(0);
sdef << std::setprecision(0);
sval.str(""); if (volt_warn > 0) sval << volt_warn * 1000;
sdef.str(""); sdef << "Default: " << volt_warn_def * 1000;
c.input("number", "Voltage warning", "volt_warn", sval.str().c_str(), sdef.str().c_str(),
NULL, "min=\"1\" step=\"1\"", "mV");
sval.str(""); if (volt_alert > 0) sval << volt_alert * 1000;
sdef.str(""); sdef << "Default: " << volt_alert_def * 1000;
c.input("number", "Voltage alert", "volt_alert", sval.str().c_str(), sdef.str().c_str(),
NULL, "min=\"1\" step=\"1\"", "mV");
sval << std::setprecision(1);
sdef << std::setprecision(1);
sval.str(""); if (volt_maxgrad > 0) sval << volt_maxgrad * 1000;
sdef.str(""); sdef << "Default: " << volt_maxgrad_def * 1000;
c.input("number", "Max valid gradient", "volt_maxgrad", sval.str().c_str(), sdef.str().c_str(),
NULL, "min=\"0\" step=\"0.1\"", "mV");
sval.str(""); if (volt_maxsddev > 0) sval << volt_maxsddev * 1000;
sdef.str(""); sdef << "Default: " << volt_maxsddev_def * 1000;
c.input("number", "Max stddev deviation", "volt_maxsddev", sval.str().c_str(), sdef.str().c_str(),
NULL, "min=\"0\" step=\"0.1\"", "mV");
sval << std::setprecision(1);
sdef << std::setprecision(1);
sval.str(""); if (temp_warn > 0) sval << temp_warn;
sdef.str(""); sdef << "Default: " << temp_warn_def;
c.input("number", "Temperature warning", "temp_warn", sval.str().c_str(), sdef.str().c_str(),
NULL, "min=\"0.1\" step=\"0.1\"", "°C");
sval.str(""); if (temp_alert > 0) sval << temp_alert;
sdef.str(""); sdef << "Default: " << temp_alert_def;
c.input("number", "Temperature alert", "temp_alert", sval.str().c_str(), sdef.str().c_str(),
NULL, "min=\"0.1\" step=\"0.1\"", "°C");
c.input_checkbox("Enable alert notifications", "alerts_enabled", alerts_enabled);
c.print(
"<div class=\"form-group\">"
"<div class=\"col-sm-offset-3 col-sm-9\">"
"<button type=\"submit\" class=\"btn btn-default\" name=\"action\" value=\"save\">Save</button> "
"<button type=\"submit\" class=\"btn btn-default\" name=\"action\" value=\"save-reset\">Save &amp; reset alerts</button> "
"</div>"
"</div>"
"<div class=\"form-group\">"
"<div class=\"col-sm-offset-3 col-sm-9\">"
"<div id=\"cfg-result\" class=\"modal-autoclear\"></div>"
"</div>"
"</div>");
c.form_end();
c.print(
"</div>\n"
"<div class=\"modal-footer\">\n"
"<button type=\"button\" class=\"btn btn-default\" data-dismiss=\"modal\">Close</button>\n"
"</div>\n"
"</div>\n"
"</div>\n"
"</div>\n"
"\n"
"\n"
"<style>\n"
"\n"
".highcharts-boxplot-stem {\n"
"stroke-opacity: 1;\n"
"}\n"
".highcharts-point {\n"
"stroke-width: 1px;\n"
"}\n"
".night .highcharts-columnrange-series .highcharts-point {\n"
"fill-opacity: 0.6;\n"
"stroke-opacity: 0.6;\n"
"}\n"
"\n"
".night .highcharts-legend-item text {\n"
"fill: #dddddd;\n"
"}\n"
"\n"
".highcharts-plot-line {\n"
"fill: none;\n"
"stroke: #3625c3;\n"
"stroke-width: 2px;\n"
"}\n"
".night .highcharts-plot-line {\n"
"stroke: #728def;\n"
"}\n"
".highcharts-plot-band {\n"
"fill: #3625c3;\n"
"fill-opacity: 0.1;\n"
"}\n"
".night .highcharts-plot-band {\n"
"fill: #728def;\n"
"fill-opacity: 0.2;\n"
"}\n"
"\n"
".plot-line-sdmaxlo {\n"
"stroke: #3625c3;\n"
"stroke-width: 1px;\n"
"}\n"
".plot-line-sdmaxhi {\n"
"stroke: #3625c3;\n"
"stroke-width: 1px;\n"
"}\n"
".night .plot-line-sdmaxlo {\n"
"stroke: #728def;\n"
"}\n"
".night .plot-line-sdmaxhi {\n"
"stroke: #728def;\n"
"}\n"
"\n"
".highcharts-plot-line-label {\n"
"fill: #888;\n"
"font-size: 10px;\n"
"}\n"
"\n"
".highcharts-color-1 {\n"
"fill: #ffb500;\n"
"stroke: #cc950e;\n"
"}\n"
".highcharts-color-2 {\n"
"fill: #ff3400;\n"
"stroke: #ff3400;\n"
"}\n"
"\n"
"#voltchart .highcharts-point.highcharts-color-0 {\n"
"fill: #3625c3;\n"
"fill-opacity: 0.1;\n"
"stroke: #3625c3;\n"
"}\n"
".night #voltchart .highcharts-point.highcharts-color-0 {\n"
"fill: #728def;\n"
"stroke: #728def;\n"
"}\n"
"\n"
"#voltchart .highcharts-boxplot-stem {\n"
"fill: #63cc7a;\n"
"fill-opacity: 1;\n"
"stroke: #63cc7a;\n"
"stroke-opacity: 1;\n"
"}\n"
".night #voltchart .highcharts-boxplot-stem {\n"
"fill-opacity: 0.6;\n"
"stroke-opacity: 0.6;\n"
"}\n"
"\n"
"#voltchart .highcharts-boxplot-median {\n"
"stroke: #193e07;\n"
"stroke-width: 5px;\n"
"stroke-linecap: round;\n"
"}\n"
".night #voltchart .highcharts-boxplot-median {\n"
"stroke: #afff45;\n"
"}\n"
"\n"
"#tempchart .highcharts-point.highcharts-color-0 {\n"
"fill: #3625c3;\n"
"fill-opacity: 0.1;\n"
"stroke: #3625c3;\n"
"}\n"
".night #tempchart .highcharts-point.highcharts-color-0 {\n"
"fill: #728def;\n"
"stroke: #728def;\n"
"}\n"
"\n"
"#tempchart .highcharts-boxplot-stem {\n"
"fill: #da9073;\n"
"fill-opacity: 1;\n"
"stroke: #da9073;\n"
"stroke-opacity: 1;\n"
"}\n"
".night #tempchart .highcharts-boxplot-stem {\n"
"fill-opacity: 0.6;\n"
"stroke-opacity: 0.6;\n"
"}\n"
"\n"
"#tempchart .highcharts-boxplot-median {\n"
"stroke: #7d1313;\n"
"stroke-width: 5px;\n"
"stroke-linecap: round;\n"
"}\n"
".night #tempchart .highcharts-boxplot-median {\n"
"stroke: #fdd02e;\n"
"}\n");
c.printf(
"#voltchart .highcharts-boxplot-stem {\n"
"stroke-width: %.1f%%;\n"
"}\n"
"#tempchart .highcharts-boxplot-stem {\n"
"stroke-width: %.1f%%;\n"
"}\n"
"</style>\n"
, stemwidth_v, stemwidth_t);
c.print(
"<script>\n"
"\n"
"/**\n"
"* Cell voltage chart\n"
"*/\n"
"\n"
"var voltchart;\n"
"\n"
"// get_volt_data: build boxplot dataset from metrics\n"
"function get_volt_data() {\n"
"var data = { cells: [], volts: [], devmax: [], voltmean: 0, sdlo: 0, sdhi: 0, sdmaxlo: 0, sdmaxhi: 0 };\n"
"var cnt = metrics[\"v.b.c.voltage\"] ? metrics[\"v.b.c.voltage\"].length : 0;\n"
"if (cnt == 0)\n"
"return data;\n"
"var i, act, min, max, devmax, dalert, dlow, dhigh;\n"
"data.voltmean = metrics[\"v.b.p.voltage.avg\"] || 0;\n"
"data.sdlo = data.voltmean - (metrics[\"v.b.p.voltage.stddev\"] || 0);\n"
"data.sdhi = data.voltmean + (metrics[\"v.b.p.voltage.stddev\"] || 0);\n"
"data.sdmaxlo = data.voltmean - (metrics[\"v.b.p.voltage.stddev.max\"] || 0);\n"
"data.sdmaxhi = data.voltmean + (metrics[\"v.b.p.voltage.stddev.max\"] || 0);\n"
"for (i=0; i<cnt; i++) {\n"
"act = metrics[\"v.b.c.voltage\"][i];\n"
"min = metrics[\"v.b.c.voltage.min\"][i] || act;\n"
"max = metrics[\"v.b.c.voltage.max\"][i] || act;\n"
"devmax = metrics[\"v.b.c.voltage.dev.max\"][i] || 0;\n"
"dalert = metrics[\"v.b.c.voltage.alert\"][i] || 0;\n"
"if (devmax > 0) {\n"
"dlow = data.voltmean;\n"
"dhigh = data.voltmean + devmax;\n"
"} else {\n"
"dlow = data.voltmean + devmax;\n"
"dhigh = data.voltmean;\n"
"}\n"
"data.cells.push(i+1);\n"
"data.volts.push([min,act,act,act,max]);\n"
"data.devmax.push({ x:i, low:dlow, high:dhigh, colorIndex:dalert,\n"
"id: ((devmax < 0) ? '▼ ' : '▲ ') + Math.abs(devmax * 1000).toFixed(0) + ' mV' });\n"
"}\n"
"return data;\n"
"}\n"
"\n"
"function update_volt_chart() {\n"
"var data = get_volt_data();\n"
"voltchart.xAxis[0].setCategories(data.cells);\n"
"voltchart.yAxis[0].removePlotLine('plot-line-mean');\n"
"voltchart.yAxis[0].addPlotLine({ id: 'plot-line-mean', className: 'plot-line-mean', value: data.voltmean, zIndex: 3 });\n"
"voltchart.yAxis[0].removePlotLine('plot-line-sdmaxlo');\n"
"voltchart.yAxis[0].addPlotLine({ id: 'plot-line-sdmaxlo', className: 'plot-line-sdmaxlo', value: data.sdmaxlo, zIndex: 3 });\n"
"voltchart.yAxis[0].removePlotLine('plot-line-sdmaxhi');\n"
"voltchart.yAxis[0].addPlotLine({ id: 'plot-line-sdmaxhi', className: 'plot-line-sdmaxhi', value: data.sdmaxhi, zIndex: 3, label: { text: 'Max Std Dev' } });\n"
"voltchart.yAxis[0].removePlotBand('plot-band-sd');\n"
"voltchart.yAxis[0].addPlotBand({ id: 'plot-band-sd', className: 'plot-band-sd', from: data.sdlo, to: data.sdhi });\n"
"voltchart.series[0].setData(data.volts);\n"
"voltchart.series[1].setData(data.devmax);\n"
"}\n"
"\n"
"function init_volt_chart() {\n"
"var data = get_volt_data();\n"
"voltchart = Highcharts.chart('voltchart', {\n"
"chart: {\n"
"type: 'boxplot',\n"
"events: {\n"
"load: function () {\n"
"$('#livestatus').on(\"msg:metrics\", function(e, update){\n"
"if (update[\"v.b.c.voltage\"] != null\n"
"|| update[\"v.b.c.voltage.min\"] != null\n"
"|| update[\"v.b.c.voltage.max\"] != null\n"
"|| update[\"v.b.c.voltage.dev.max\"] != null\n"
"|| update[\"v.b.c.voltage.alert\"] != null)\n"
"update_volt_chart();\n"
"});\n"
"}\n"
"},\n"
"zoomType: 'xy',\n"
"panning: true,\n"
"panKey: 'ctrl',\n"
"},\n"
"title: { text: null },\n"
"credits: { enabled: false },\n"
"legend: {\n"
"enabled: true,\n"
"align: 'center',\n"
"verticalAlign: 'bottom',\n"
"margin: 2,\n"
"padding: 2,\n"
"},\n"
"xAxis: { categories: data.cells },\n"
"yAxis: {\n"
"title: { text: null },\n"
"labels: { format: \"{value:.2f}V\" },\n"
"minTickInterval: 0.01,\n"
"minorTickInterval: 'auto',\n"
"plotLines: [\n"
"{ id: 'plot-line-mean', className: 'plot-line-mean', value: data.voltmean, zIndex: 3 },\n"
"{ id: 'plot-line-sdmaxlo', className: 'plot-line-sdmaxlo', value: data.sdmaxlo, zIndex: 3 },\n"
"{ id: 'plot-line-sdmaxhi', className: 'plot-line-sdmaxhi', value: data.sdmaxhi, zIndex: 3, label: { text: 'Max Std Dev' } },\n"
"],\n"
"plotBands: [{ id: 'plot-band-sd', className: 'plot-band-sd', from: data.sdlo, to: data.sdhi }],\n"
"},\n"
"tooltip: {\n"
"shared: true,\n"
"padding: 4,\n"
"positioner: function (labelWidth, labelHeight, point) {\n"
"return {\n"
"x: voltchart.plotLeft + Math.min(voltchart.plotWidth - labelWidth, Math.max(0, point.plotX - labelWidth/2)),\n"
"y: 0 };\n"
"},\n"
"headerFormat: 'Voltage #{point.key}: ',\n"
"pointFormatter: function () {\n"
"if (this.series.index == 0) {\n"
"return '<b>' + this.median.toFixed(3) + ' V</b>'\n"
"+ ' [' + this.low.toFixed(3) + ' ' + this.high.toFixed(3) + ']<br/>';\n"
"} else {\n"
"return 'Max deviation: <b>' + this.id + '</b>';\n"
"}\n"
"},\n"
"},\n"
"series: [{\n"
"name: 'Voltage',\n"
"zIndex: 1,\n"
"data: data.volts,\n"
"whiskerLength: 0,\n"
"animation: {\n"
"duration: 100,\n"
"easing: 'easeOutExpo'\n"
"},\n"
"},{\n"
"name: 'Max deviation',\n"
"zIndex: 0,\n"
"type: 'columnrange',\n"
"data: data.devmax,\n"
"maxPointWidth: 35,\n"
"animation: {\n"
"duration: 100,\n"
"easing: 'easeOutExpo'\n"
"},\n"
"}]\n"
"});\n"
"$('#voltchart').data('chart', voltchart).addClass('has-chart');"
"}\n"
"\n"
"\n"
"/**\n"
"* Cell temperature chart\n"
"*/\n"
"\n"
"var tempchart;\n"
"\n"
"// get_temp_data: build boxplot dataset from metrics\n"
"function get_temp_data() {\n"
"var data = { cells: [], temps: [], devmax: [], tempmean: 0, sdlo: 0, sdhi: 0, sdmaxlo: 0, sdmaxhi: 0 };\n"
"var cnt = metrics_user[\"v.b.c.temp\"] ? metrics_user[\"v.b.c.temp\"].length : 0;\n"
"if (cnt == 0)\n"
"return data;\n"
"var i, act, min, max, devmax, dalert, dlow, dhigh;\n"
"data.tempmean = metrics_user[\"v.b.p.temp.avg\"] || 0;\n"
"data.sdlo = data.tempmean - (metrics_user[\"v.b.p.temp.stddev\"] || 0);\n"
"data.sdhi = data.tempmean + (metrics_user[\"v.b.p.temp.stddev\"] || 0);\n"
"data.sdmaxlo = data.tempmean - (metrics_user[\"v.b.p.temp.stddev.max\"] || 0);\n"
"data.sdmaxhi = data.tempmean + (metrics_user[\"v.b.p.temp.stddev.max\"] || 0);\n"
"for (i=0; i<cnt; i++) {\n"
"act = metrics_user[\"v.b.c.temp\"][i];\n"
"min = metrics_user[\"v.b.c.temp.min\"][i] || act;\n"
"max = metrics_user[\"v.b.c.temp.max\"][i] || act;\n"
"devmax = metrics_user[\"v.b.c.temp.dev.max\"][i] || 0;\n"
"dalert = metrics[\"v.b.c.temp.alert\"][i] || 0;\n"
"if (devmax > 0) {\n"
"dlow = data.tempmean;\n"
"dhigh = data.tempmean + devmax;\n"
"} else {\n"
"dlow = data.tempmean + devmax;\n"
"dhigh = data.tempmean;\n"
"}\n"
"data.cells.push(i+1);\n"
"data.temps.push([min,act,act,act,max]);\n"
"data.devmax.push({ x:i, low:dlow, high:dhigh, colorIndex:dalert,\n"
"id: ((devmax < 0) ? '▼ ' : '▲ ') + Math.abs(devmax).toFixed(1) + ' °C' });\n"
"}\n"
"return data;\n"
"}\n"
"\n"
"function update_temp_chart() {\n"
"var data = get_temp_data();\n"
"tempchart.xAxis[0].setCategories(data.cells);\n"
"tempchart.yAxis[0].removePlotLine('plot-line-mean');\n"
"tempchart.yAxis[0].addPlotLine({ id: 'plot-line-mean', className: 'plot-line-mean', value: data.tempmean, zIndex: 3 });\n"
"tempchart.yAxis[0].removePlotLine('plot-line-sdmaxlo');\n"
"tempchart.yAxis[0].addPlotLine({ id: 'plot-line-sdmaxlo', className: 'plot-line-sdmaxlo', value: data.sdmaxlo, zIndex: 3 });\n"
"tempchart.yAxis[0].removePlotLine('plot-line-sdmaxhi');\n"
"tempchart.yAxis[0].addPlotLine({ id: 'plot-line-sdmaxhi', className: 'plot-line-sdmaxhi', value: data.sdmaxhi, zIndex: 3, label: { text: 'Max Std Dev' } });\n"
"tempchart.yAxis[0].removePlotBand('plot-band-sd');\n"
"tempchart.yAxis[0].addPlotBand({ id: 'plot-band-sd', className: 'plot-band-sd', from: data.sdlo, to: data.sdhi });\n"
"tempchart.series[0].setData(data.temps);\n"
"tempchart.series[1].setData(data.devmax);\n"
"}\n"
"\n"
"function init_temp_chart() {\n"
"var data = get_temp_data();\n"
"tempchart = Highcharts.chart('tempchart', {\n"
"chart: {\n"
"type: 'boxplot',\n"
"events: {\n"
"load: function () {\n"
"$('#livestatus').on(\"msg:metrics\", function(e, update){\n"
"if (update[\"v.b.c.temp\"] != null\n"
"|| update[\"v.b.c.temp.min\"] != null\n"
"|| update[\"v.b.c.temp.max\"] != null\n"
"|| update[\"v.b.c.temp.dev.max\"] != null\n"
"|| update[\"v.b.c.temp.alert\"] != null)\n"
"update_temp_chart();\n"
"});\n"
"}\n"
"},\n"
"zoomType: 'xy',\n"
"panning: true,\n"
"panKey: 'ctrl',\n"
"},\n"
"title: { text: null },\n"
"credits: { enabled: false },\n"
"legend: {\n"
"enabled: true,\n"
"align: 'center',\n"
"verticalAlign: 'bottom',\n"
"margin: 2,\n"
"padding: 2,\n"
"},\n"
"xAxis: { categories: data.cells },\n"
"yAxis: {\n"
"title: { text: null },\n"
"labels: { format: \"{value:.0f} °C\" },\n"
"minTickInterval: 1,\n"
"minorTickInterval: 'auto',\n"
"plotLines: [\n"
"{ id: 'plot-line-mean', className: 'plot-line-mean', value: data.tempmean, zIndex: 3 },\n"
"{ id: 'plot-line-sdmaxlo', className: 'plot-line-sdmaxlo', value: data.sdmaxlo, zIndex: 3 },\n"
"{ id: 'plot-line-sdmaxhi', className: 'plot-line-sdmaxhi', value: data.sdmaxhi, zIndex: 3, label: { text: 'Max Std Dev' } },\n"
"],\n"
"plotBands: [{ id: 'plot-band-sd', className: 'plot-band-sd', from: data.sdlo, to: data.sdhi }],\n"
"},\n"
"tooltip: {\n"
"shared: true,\n"
"padding: 4,\n"
"positioner: function (labelWidth, labelHeight, point) {\n"
"return {\n"
"x: tempchart.plotLeft + Math.min(tempchart.plotWidth - labelWidth, Math.max(0, point.plotX - labelWidth/2)),\n"
"y: 0 };\n"
"},\n"
"headerFormat: 'Temperature #{point.key}: ',\n"
"pointFormatter: function () {\n"
"if (this.series.index == 0) {\n"
"return '<b>' + this.median.toFixed(1) + ' °C</b>'\n"
"+ ' [' + this.low.toFixed(1) + ' ' + this.high.toFixed(1) + ']<br/>';\n"
"} else {\n"
"return 'Max deviation: <b>' + this.id + '</b>';\n"
"}\n"
"},\n"
"},\n"
"series: [{\n"
"name: 'Temperature',\n"
"zIndex: 1,\n"
"data: data.temps,\n"
"whiskerLength: 0,\n"
"animation: {\n"
"duration: 100,\n"
"easing: 'easeOutExpo'\n"
"},\n"
"},{\n"
"name: 'Max deviation',\n"
"zIndex: 0,\n"
"type: 'columnrange',\n"
"data: data.devmax,\n"
"maxPointWidth: 35,\n"
"animation: {\n"
"duration: 100,\n"
"easing: 'easeOutExpo'\n"
"},\n"
"}]\n"
"});\n"
"$('#tempchart').data('chart', tempchart).addClass('has-chart');"
"}\n"
"\n"
"\n"
"/**\n"
"* Chart initialization\n"
"*/\n"
"\n"
"function init_charts() {\n"
"init_volt_chart();\n"
"init_temp_chart();\n"
"}\n"
"\n"
"if (window.Highcharts) {\n"
"init_charts();\n"
"} else {\n"
"$.ajax({\n"
"url: \"" URL_ASSETS_CHARTS_JS "\",\n"
"dataType: \"script\",\n"
"cache: true,\n"
"success: function(){ init_charts(); }\n"
"});\n"
"}\n"
"\n"
"</script>\n");
PAGE_HOOK("body.post");
c.done();
}
/**
* HandleCfgBrakelight: configure vehicle brake light control
*
* Note: this is not enabled by default, as most vehicles do not need this.
* To enable, include this in the vehicle init:
* MyWebServer.RegisterPage("/cfg/brakelight", "Brake light control", OvmsWebServer::HandleCfgBrakelight, PageMenu_Vehicle, PageAuth_Cookie);
* You can change the URL path, title, menu association and authentication as you like.
* For a clean shutdown, add
* MyWebServer.DeregisterPage("/cfg/brakelight");
* …in your vehicle cleanup.
*/
void OvmsWebServer::HandleCfgBrakelight(PageEntry_t& p, PageContext_t& c)
{
std::string error, info;
bool enable, ignftbrk;
std::string smooth_acc, smooth_bat, port, level_on, level_off, basepwr;
if (c.method == "POST") {
// process form submission:
enable = (c.getvar("enable") == "yes");
smooth_acc = c.getvar("smooth_acc");
smooth_bat = c.getvar("smooth_bat");
port = c.getvar("port");
level_on = c.getvar("level_on");
level_off = c.getvar("level_off");
ignftbrk = (c.getvar("ignftbrk") == "yes");
basepwr = c.getvar("basepwr");
// validate:
if (smooth_acc != "") {
int v = atof(smooth_acc.c_str());
if (v < 0) {
error += "<li data-input=\"smooth_acc\">Acceleration smoothing must be greater or equal 0.</li>";
}
}
if (smooth_bat != "") {
int v = atof(smooth_bat.c_str());
if (v < 0) {
error += "<li data-input=\"smooth_bat\">Battery power smoothing must be greater or equal 0.</li>";
}
}
if (port != "") {
int v = atoi(port.c_str());
if (v == 2 || v < 1 || v > 9) {
error += "<li data-input=\"port\">Port must be one of 1, 3…9</li>";
}
}
if (level_on != "") {
int v = atof(level_on.c_str());
if (v < 0) {
error += "<li data-input=\"level_on\">Activation level must be greater or equal 0.</li>";
}
}
if (level_off != "") {
int v = atof(level_off.c_str());
if (v < 0) {
error += "<li data-input=\"level_off\">Deactivation level must be greater or equal 0.</li>";
}
}
if (basepwr != "") {
int v = atof(basepwr.c_str());
if (v < 0) {
error += "<li data-input=\"basepwr\">Base power level must be greater or equal 0.</li>";
}
}
if (error == "") {
// success:
MyConfig.SetParamValue("vehicle", "accel.smoothing", smooth_acc);
MyConfig.SetParamValue("vehicle", "batpwr.smoothing", smooth_bat);
MyConfig.SetParamValueBool("vehicle", "brakelight.enable", enable);
MyConfig.SetParamValue("vehicle", "brakelight.port", port);
MyConfig.SetParamValue("vehicle", "brakelight.on", level_on);
MyConfig.SetParamValue("vehicle", "brakelight.off", level_off);
MyConfig.SetParamValueBool("vehicle", "brakelight.ignftbrk", ignftbrk);
MyConfig.SetParamValue("vehicle", "brakelight.basepwr", basepwr);
info = "<p class=\"lead\">Success!</p><ul class=\"infolist\">" + info + "</ul>";
c.head(200);
c.alert("success", info.c_str());
OutputHome(p, c);
c.done();
return;
}
// output error, return to form:
error = "<p class=\"lead\">Error!</p><ul class=\"errorlist\">" + error + "</ul>";
c.head(400);
c.alert("danger", error.c_str());
}
else {
// read configuration:
smooth_acc = MyConfig.GetParamValue("vehicle", "accel.smoothing");
smooth_bat = MyConfig.GetParamValue("vehicle", "batpwr.smoothing");
enable = MyConfig.GetParamValueBool("vehicle", "brakelight.enable", false);
port = MyConfig.GetParamValue("vehicle", "brakelight.port", "1");
level_on = MyConfig.GetParamValue("vehicle", "brakelight.on");
level_off = MyConfig.GetParamValue("vehicle", "brakelight.off");
ignftbrk = MyConfig.GetParamValueBool("vehicle", "brakelight.ignftbrk", false);
basepwr = MyConfig.GetParamValue("vehicle", "brakelight.basepwr");
c.head(200);
}
// generate form:
c.panel_start("primary", "Regen Brake Light");
c.form_start(p.uri);
c.input_slider("Acceleration smoothing", "smooth_acc", 3, NULL,
-1, smooth_acc.empty() ? 2.0 : atof(smooth_acc.c_str()), 2.0, 0.0, 10.0, 0.1,
"<p>Speed/acceleration smoothing eliminates road bump and gear box backlash noise.</p>"
"<p>Lower value = higher sensitivity. Set to zero if your vehicle data is already smoothed.</p>");
c.input_slider("Battery power smoothing", "smooth_bat", 3, NULL,
-1, smooth_bat.empty() ? 2.0 : atof(smooth_bat.c_str()), 2.0, 0.0, 10.0, 0.1,
"<p>Battery power smoothing eliminates road bump and gear box backlash noise.</p>"
"<p>Lower value = higher sensitivity. Set to zero if your vehicle data is already smoothed.</p>");
c.print("<hr>");
c.input_radiobtn_start("Regen brake signal", "enable");
c.input_radiobtn_option("enable", "OFF", "no", !enable);
c.input_radiobtn_option("enable", "ON", "yes", enable);
c.input_radiobtn_end();
c.input_select_start("… control port", "port");
c.input_select_option("SW_12V (DA26 pin 18)", "1", port == "1");
c.input_select_option("EGPIO_2", "3", port == "3");
c.input_select_option("EGPIO_3", "4", port == "4");
c.input_select_option("EGPIO_4", "5", port == "5");
c.input_select_option("EGPIO_5", "6", port == "6");
c.input_select_option("EGPIO_6", "7", port == "7");
c.input_select_option("EGPIO_7", "8", port == "8");
c.input_select_option("EGPIO_8", "9", port == "9");
c.input_select_end();
c.input_slider("… activation level", "level_on", 3, "m/s²",
-1, level_on.empty() ? 1.3 : atof(level_on.c_str()), 1.3, 0.0, 3.0, 0.1,
"<p>Deceleration threshold to activate regen brake light.</p>"
"<p>Under UN regulation 13H, brake light illumination is required for decelerations &gt;1.3 m/s².</p>");
c.input_slider("… deactivation level", "level_off", 3, "m/s²",
-1, level_off.empty() ? 0.7 : atof(level_off.c_str()), 0.7, 0.0, 3.0, 0.1,
"<p>Deceleration threshold to deactivate regen brake light.</p>"
"<p>Under UN regulation 13H, brake lights must not be illuminated for decelerations &le;0.7 m/s².</p>");
c.input_slider("… base power range", "basepwr", 3, "kW",
-1, basepwr.empty() ? 0 : atof(basepwr.c_str()), 0, 0.0, 5.0, 0.1,
"<p>Battery power range around zero (+/-) ignored for the regen signal. Raise to desensitize the signal"
" for minor power levels (if the vehicle supports the battery power metric).</p>");
c.input_checkbox("<strong>Ignore foot brake</strong>", "ignftbrk", ignftbrk,
"<p>Create regen signal independent of the foot brake status.</p>");
c.print("<hr>");
c.input_button("default", "Save");
c.form_end();
c.panel_end(
"<p>The OVMS generated regenerative braking signal needs a hardware modification to drive your"
" vehicle brake lights. See your OVMS vehicle manual section for details on how to do this.</p>"
"<p>The regen brake signal is activated when the deceleration level exceeds the configured threshold,"
" the speed is above 1 m/s (3.6 kph / 2.2 mph) and the battery power is negative (charging).</p>"
"<p>The signal is deactivated when deceleration drops below the deactivation level, speed drops"
" below 1 m/s or battery power indicates discharging."
" The signal will be held activated for at least 500 ms.</p>");
c.done();
}