package com.openvehicles.OVMS.ui;
import android.app.AlertDialog;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.os.Handler;
import android.text.Html;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.SeekBar;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuInflater;
import com.actionbarsherlock.view.MenuItem;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.charts.CandleStickChart;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.CandleData;
import com.github.mikephil.charting.data.CandleDataSet;
import com.github.mikephil.charting.data.CandleEntry;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.listener.OnChartValueSelectedListener;
import com.github.mikephil.charting.utils.ViewPortHandler;
import com.github.mikephil.charting.utils.Highlight;
import com.github.mikephil.charting.utils.ValueFormatter;
import com.luttu.AppPrefes;
import com.openvehicles.OVMS.R;
import com.openvehicles.OVMS.entities.BatteryData;
import com.openvehicles.OVMS.entities.CarData;
import com.openvehicles.OVMS.entities.CmdSeries;
import com.openvehicles.OVMS.ui.utils.ProgressOverlay;
import com.openvehicles.OVMS.utils.CarsStorage;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
/**
* Battery pack and cell status history charts.
*
* Currently only usable for Renault Twizy, but can be extended to support
* other battery layouts easily as soon as other vehicles deliver this info.
*
* Data model: BatteryData
*
* Created by balzer on 27.03.15.
*
*/
public class BatteryFragment
extends BaseFragment
implements CmdSeries.Listener, ProgressOverlay.OnCancelListener
{
private static final String TAG = "BatteryFragment";
// data set colors:
private static final int COLOR_SOC_LINE = Color.parseColor("#A04455FF");
private static final int COLOR_SOC_TEXT = Color.parseColor("#AAAAFF");
private static final int COLOR_SOC_GRID = Color.parseColor("#7777AA");
private static final int COLOR_VOLT = Color.parseColor("#CCFF33");
private static final int COLOR_VOLT_MIN = Color.parseColor("#77AA00");
private static final int COLOR_VOLT_GRID = Color.parseColor("#77AA77");
private static final int COLOR_TEMP = Color.parseColor("#FFEE33");
private static final int COLOR_TEMP_GRID = Color.parseColor("#AAAA77");
// data storage:
private BatteryData batteryData;
// user interface:
private Menu optionsMenu;
private LineChart packChart;
private LineData packData;
private LineDataSet packVoltSet, packVoltMinSet, packTempSet;
private CandleStickChart cellChart;
private SeekBar seekPack;
private int highlightSetNr = -1;
private String highlightSetLabel = "";
private boolean mShowVolt = true;
private boolean mShowTemp = false;
// system services:
private final static Handler mHandler = new Handler();
private CarData mCarData;
private CmdSeries cmdSeries;
private AppPrefes appPrefes;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// Init data storage:
batteryData = new BatteryData();
// Load prefs:
appPrefes = new AppPrefes(getActivity(), "ovms");
mShowVolt = appPrefes.getData("battery_show_volt").equals("on");
mShowTemp = appPrefes.getData("battery_show_temp").equals("on");
if (!mShowVolt && !mShowTemp)
mShowVolt = true;
// Setup UI:
ProgressOverlay progressOverlay = createProgressOverlay(inflater, container, false);
progressOverlay.setOnCancelListener(this);
View rootView = inflater.inflate(R.layout.fragment_battery, null);
//
// Setup Cell status chart:
//
XAxis xAxis;
YAxis yAxis;
cellChart = (CandleStickChart) rootView.findViewById(R.id.chart_cells);
cellChart.setDescription(getString(R.string.battery_cell_description));
cellChart.getPaint(LineChart.PAINT_DESCRIPTION).setColor(Color.LTGRAY);
cellChart.setDrawGridBackground(false);
cellChart.setDrawBorders(true);
xAxis = cellChart.getXAxis();
xAxis.setTextColor(Color.WHITE);
yAxis = cellChart.getAxisLeft();
yAxis.setTextColor(COLOR_VOLT);
yAxis.setGridColor(COLOR_VOLT_GRID);
yAxis.setStartAtZero(false);
yAxis.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float v) {
return String.format("%.2f", v);
}
});
yAxis = cellChart.getAxisRight();
yAxis.setTextColor(COLOR_TEMP);
yAxis.setGridColor(COLOR_TEMP_GRID);
yAxis.setStartAtZero(false);
yAxis.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float v) {
return String.format("%.0f", v);
}
});
//
// Setup Pack history chart:
//
packChart = (LineChart) rootView.findViewById(R.id.chart_pack);
packChart.setDescription(getString(R.string.battery_pack_description));
packChart.getPaint(LineChart.PAINT_DESCRIPTION).setColor(Color.LTGRAY);
packChart.setDrawGridBackground(false);
packChart.setDrawBorders(true);
packChart.setOnChartValueSelectedListener(new OnChartValueSelectedListener() {
@Override
public void onValueSelected(Entry entry, int dataSet, Highlight highlight) {
// remember user data set selection:
highlightSetNr = dataSet;
highlightSetLabel = packChart.getData().getDataSetByIndex(dataSet).getLabel();
// update seek bar:
seekPack.setProgress(entry.getXIndex()); // fires listener event (fromUser=false)
// update cell chart:
showCellStatus(entry.getXIndex());
}
@Override
public void onNothingSelected() {
// nop
}
});
xAxis = packChart.getXAxis();
xAxis.setTextColor(Color.WHITE);
yAxis = packChart.getAxisLeft();
yAxis.setStartAtZero(false);
yAxis.setSpaceTop(5f);
yAxis.setSpaceBottom(5f);
yAxis.setTextColor(COLOR_SOC_TEXT);
yAxis.setGridColor(COLOR_SOC_GRID);
yAxis.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float v) {
return String.format("%.0f%%", v);
}
});
yAxis = packChart.getAxisRight();
yAxis.setStartAtZero(false);
yAxis.setSpaceTop(15f);
yAxis.setSpaceBottom(15f);
yAxis.setTextColor(COLOR_VOLT);
yAxis.setGridColor(COLOR_VOLT_GRID);
yAxis.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float v) {
return String.format("%.0f", v);
}
});
//
// Setup Pack history seek bar:
//
seekPack = (SeekBar) rootView.findViewById(R.id.seek_pack);
seekPack.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int val, boolean fromUser) {
if (fromUser) {
// highlight entry:
highlightPackEntry(val);
// update cell chart:
showCellStatus(val);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// nop
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// nop
}
});
// default data set to highlight:
highlightSetLabel = getString(R.string.battery_data_soc);
// attach menu:
setHasOptionsMenu(true);
return rootView;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.battery_chart_options, menu);
optionsMenu = menu;
optionsMenu.findItem(R.id.mi_chk_volt).setChecked(mShowVolt);
optionsMenu.findItem(R.id.mi_chk_temp).setChecked(mShowTemp);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getSherlockActivity().setTitle(R.string.battery_title);
getSherlockActivity().getSupportActionBar().setIcon(R.drawable.ic_action_chart);
// get data of current car:
mCarData = CarsStorage.get().getSelectedCarData();
// schedule data loader:
showProgressOverlay(getString(R.string.battery_msg_loading_data));
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// load and display saved vehicle data:
BatteryData saved = BatteryData.loadFile(mCarData.sel_vehicleid);
if (saved != null) {
Log.v(TAG, "BatteryData loaded for " + mCarData.sel_vehicleid);
batteryData = saved;
dataSetChanged();
}
hideProgressOverlay();
}
}, 200);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int menuId = item.getItemId();
boolean newState = !item.isChecked();
switch(menuId) {
case R.id.mi_get_data:
cmdSeries = new CmdSeries(getService(), BatteryFragment.this)
.add(R.string.battery_msg_get_status, "206") // ensure non-empty history
.add(R.string.battery_msg_get_battpack, "32,RT-BAT-P")
.add(R.string.battery_msg_get_battcell, "32,RT-BAT-C")
.start();
return true;
case R.id.mi_reset_view:
packChart.fitScreen();
cellChart.fitScreen();
return true;
case R.id.mi_help:
new AlertDialog.Builder(getActivity())
.setTitle(R.string.battery_btn_help)
.setMessage(Html.fromHtml(getString(R.string.battery_help)))
.setPositiveButton(android.R.string.ok, null)
.show();
return true;
case R.id.mi_chk_volt:
mShowVolt = newState;
if (!mShowVolt && !mShowTemp) {
mShowTemp = true;
optionsMenu.findItem(R.id.mi_chk_temp).setChecked(true);
}
item.setChecked(newState);
dataFilterChanged();
return true;
case R.id.mi_chk_temp:
mShowTemp = newState;
if (!mShowVolt && !mShowTemp) {
mShowVolt = true;
optionsMenu.findItem(R.id.mi_chk_volt).setChecked(true);
}
item.setChecked(newState);
dataFilterChanged();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onStop() {
if (cmdSeries != null)
cmdSeries.cancel();
super.onStop();
}
@Override
public void onCmdSeriesProgress(String message, int pos, int posCnt, int step, int stepCnt) {
stepProgressOverlay(message, pos, posCnt, step, stepCnt);
}
@Override
public void onProgressCancel() {
if (cmdSeries != null)
cmdSeries.cancel();
hideProgressOverlay();
}
@Override
public void onCmdSeriesFinish(CmdSeries cmdSeries, int returnCode) {
String errorMsg = "";
String errorDetail = "";
hideProgressOverlay();
switch (returnCode) {
case -1: // abort
return;
case 0: // ok: process & display results:
showProgressOverlay(getString(R.string.msg_processing_data));
batteryData.processCmdResults(cmdSeries);
batteryData.saveFile(mCarData.sel_vehicleid);
dataSetChanged();
hideProgressOverlay();
return;
case 1: // failed
errorDetail = cmdSeries.getErrorDetail();
if (errorDetail.contains("B "))
errorDetail += getString(R.string.hint_sevcon_offline);
errorMsg = getString(R.string.err_failed, errorDetail);
break;
case 2: // unsupported
errorMsg = getString(R.string.err_unsupported_operation);
break;
case 3: // unimplemented
errorMsg = getString(R.string.err_unimplemented_operation);
break;
}
// getting here means error:
new AlertDialog.Builder(getActivity())
.setTitle(R.string.Error)
.setMessage(cmdSeries.getMessage() + " => " + errorMsg)
.setPositiveButton(android.R.string.ok, null)
.show();
}
/**
* Check data model validity
*/
private boolean isPackValid() {
return (batteryData != null
&& batteryData.packHistory != null
&& batteryData.packHistory.size() > 0);
}
/**
* Check data model and pack index validity
*/
private boolean isPackIndexValid(int index) {
return (isPackValid()
&& index < batteryData.packHistory.size());
}
/**
* Call when underlying batteryData object ready/changed
*/
public void dataSetChanged() {
if (!isPackValid())
return;
// update pack chart:
showPackHistory();
int lastEntry = batteryData.packHistory.size() - 1;
// update seek bar:
seekPack.setMax(lastEntry);
seekPack.setProgress(lastEntry);
// highlight latest entry:
highlightPackEntry(lastEntry);
// show latest cell status:
showCellStatus(lastEntry);
}
/**
* Call when user changed data filter (i.e. show Volt / Temp sets)
*/
private void dataFilterChanged() {
// save prefs:
appPrefes.SaveData("battery_show_volt", mShowVolt ? "on" : "off");
appPrefes.SaveData("battery_show_temp", mShowTemp ? "on" : "off");
// check data status:
if (!isPackValid())
return;
// update pack chart:
showPackHistory();
int seekEntry = seekPack.getProgress();
// highlight last selected entry:
highlightSetNr = -1; // ...
highlightPackEntry(seekEntry);
// show last selected entry cell status:
showCellStatus(seekEntry);
}
/**
* Update the pack chart
*/
public void showPackHistory() {
if (!isPackValid())
return;
ArrayList<BatteryData.PackStatus> packHistory = batteryData.packHistory;
BatteryData.PackStatus packStatus, lastStatus;
SimpleDateFormat timeFmt = new SimpleDateFormat("HH:mm");
//
// Pack chart:
//
// create value arrays:
ArrayList<String> xValues = new ArrayList<String>();
ArrayList<LimitLine> xSections = new ArrayList<LimitLine>();
ArrayList<Entry> socValues = new ArrayList<Entry>();
ArrayList<Entry> voltValues = new ArrayList<Entry>();
ArrayList<Entry> voltMinValues = new ArrayList<Entry>();
ArrayList<Entry> tempValues = new ArrayList<Entry>();
lastStatus = null;
for (int i = 0; i < packHistory.size(); i++) {
packStatus = packHistory.get(i);
xValues.add(timeFmt.format(packStatus.timeStamp));
socValues.add(new Entry(packStatus.soc, i));
voltValues.add(new Entry(packStatus.volt, i));
voltMinValues.add(new Entry(packStatus.voltMin, i));
tempValues.add(new Entry(packStatus.temp, i));
// add section markers:
if (packStatus.isNewSection(lastStatus)) {
LimitLine l = new LimitLine(i);
l.setLabel(timeFmt.format(packStatus.timeStamp));
l.setLabelPosition(LimitLine.LimitLabelPosition.POS_RIGHT);
l.setTextColor(Color.WHITE);
l.setTextStyle(Paint.Style.FILL);
l.enableDashedLine(3f, 2f, 0f);
xSections.add(l);
}
lastStatus = packStatus;
}
// create data sets:
ArrayList<LineDataSet> dataSets = new ArrayList<LineDataSet>();
LineDataSet dataSet;
packTempSet = null;
packVoltSet = null;
if (mShowTemp) {
packTempSet = dataSet = new LineDataSet(tempValues, getString(R.string.battery_data_temp));
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
dataSet.setColor(COLOR_TEMP);
dataSet.setLineWidth(3f);
dataSet.setDrawCircles(false);
dataSet.setDrawValues(false);
dataSets.add(dataSet);
}
if (mShowVolt) {
packVoltMinSet = dataSet = new LineDataSet(voltMinValues, getString(R.string.battery_data_volt_min));
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
dataSet.setColor(COLOR_VOLT_MIN);
dataSet.setLineWidth(2f);
dataSet.setDrawCircles(false);
dataSet.setDrawValues(false);
dataSets.add(dataSet);
packVoltSet = dataSet = new LineDataSet(voltValues, getString(R.string.battery_data_volt));
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
dataSet.setColor(COLOR_VOLT);
dataSet.setLineWidth(3f);
dataSet.setDrawCircles(false);
dataSet.setDrawValues(false);
dataSets.add(dataSet);
}
dataSet = new LineDataSet(socValues, getString(R.string.battery_data_soc));
dataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
dataSet.setColor(COLOR_SOC_LINE);
dataSet.setLineWidth(4f);
dataSet.setDrawCircles(false);
dataSet.setDrawValues(false);
dataSets.add(dataSet);
// display data sets:
LineData data;
packData = data = new LineData(xValues, dataSets);
data.setValueTextColor(Color.WHITE);
data.setValueTextSize(9f);
packChart.setData(data);
XAxis xAxis = packChart.getXAxis();
xAxis.removeAllLimitLines();
for (int i=0; i < xSections.size(); i++) {
xAxis.addLimitLine(xSections.get(i));
}
packChart.getLegend().setTextColor(Color.WHITE);
packChart.invalidate();
}
/**
* Set pack chart highlight to a specific pack history index.
*
* This will try to keep the last selected data set. If highlightSetNr == -1
* it will try to determine the data set by the last selected label.
*
* @param index - pack history index to highlight
*/
private void highlightPackEntry(int index) {
if (!isPackIndexValid(index))
return;
// check model status:
if (batteryData == null || batteryData.packHistory == null
|| index >= batteryData.packHistory.size()) {
Log.e(TAG, "highlighPackEntry: #" + index + " out of bounds");
return;
}
// determine data set to highlight:
if (highlightSetNr == -1) {
// get user selection by set label:
LineDataSet dataSet = packData.getDataSetByLabel(highlightSetLabel, false);
highlightSetNr = packData.getIndexOfDataSet(dataSet);
}
if (highlightSetNr == -1) {
// fallback to foreground set:
highlightSetNr = packData.getDataSetCount() - 1;
}
// highlight entry:
packChart.highlightValue(index, highlightSetNr); // does not fire listener event
// center highlight in chart viewport:
LineDataSet dataSet = packChart.getData().getDataSetByIndex(highlightSetNr);
packChart.centerViewTo(index, dataSet.getYValForXIndex(index),
dataSet.getAxisDependency());
}
/**
* Update the cell chart
*
* @param index - pack history index to display
*/
private void showCellStatus(int index) {
if (!isPackIndexValid(index))
return;
ArrayList<BatteryData.PackStatus> packHistory = batteryData.packHistory;
BatteryData.PackStatus pack;
ArrayList<BatteryData.CellStatus> cells;
BatteryData.CellStatus cell;
pack = packHistory.get(index);
cells = pack.cells;
if (cells == null) {
Log.w(TAG, "showCellStatus x=" + index + ": cells=null");
return;
} else if (cells.size() != 14) {
Log.w(TAG, "showCellStatus x=" + index + ": cells.size=" + cells.size());
}
// create value arrays:
ArrayList<String> xValues = new ArrayList<String>();
ArrayList<CandleEntry> voltValues = new ArrayList<CandleEntry>();
ArrayList<CandleEntry> tempValues = new ArrayList<CandleEntry>();
float low, high, open, close;
for (int i = 0; i < cells.size(); i++) {
cell = cells.get(i);
xValues.add("#" + (i+1));
// Volt: high=current, low=min
high = cell.volt;
low = cell.voltMin;
if (cell.voltDevMax < 0) {
// bad: cell voltage breaks more down than normal
// => filled candle (close < open)
open = high;
close = high + cell.voltDevMax;
if (close < low)
low = close;
} else {
// ok:
open = high - cell.voltDevMax;
close = high;
if (open < low)
low = open;
}
voltValues.add(new CandleEntry(i, high, low, open, close));
// Temp: high=max, low=min
open = cell.temp + (cell.tempDevMax / 2.1f);
close = cell.temp - (cell.tempDevMax / 2.1f);
high = Math.max(cell.tempMax, Math.max(open, close));
low = Math.min(cell.tempMin, Math.min(open, close));
tempValues.add(new CandleEntry(i, high, low, open, close));
}
// create data sets:
ArrayList<CandleDataSet> dataSets = new ArrayList<CandleDataSet>();
CandleDataSet dataSet;
if (mShowTemp) {
dataSet = new CandleDataSet(tempValues, getString(R.string.battery_data_temp));
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
dataSet.setColor(COLOR_TEMP);
dataSet.setDrawValues(true);
dataSet.setShadowWidth(4f);
dataSet.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float value) {
return String.format("%.0f", value);
}
});
dataSets.add(dataSet);
}
if (mShowVolt) {
dataSet = new CandleDataSet(voltValues, getString(R.string.battery_data_volt));
dataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
dataSet.setColor(COLOR_VOLT);
dataSet.setDrawValues(true);
dataSet.setShadowWidth(4f);
dataSet.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float value) {
return String.format("%.3f", value);
}
});
dataSets.add(dataSet);
}
// configure y axes:
YAxis yAxis = cellChart.getAxisLeft();
yAxis.setEnabled(mShowVolt);
if (mShowVolt && (packVoltSet != null)) {
float yMax = packVoltSet.getYMax() / 14f + 0.1f;
float yMin = packVoltMinSet.getYMin() / 14f - 0.1f;
yAxis.setAxisMaxValue(yMax);
if (mShowTemp)
yAxis.setAxisMinValue(yMin-(yMax-yMin)); // half height
else
yAxis.setAxisMinValue(yMin); // full height
}
yAxis = cellChart.getAxisRight();
yAxis.setEnabled(mShowTemp);
if (mShowTemp && (packTempSet != null)) {
float yMax = packTempSet.getYMax() + 3f;
float yMin = packTempSet.getYMin() - 3f;
if (mShowVolt)
yAxis.setAxisMaxValue(yMax + (yMax - yMin)); // half height
else
yAxis.setAxisMaxValue(yMax); // full height
yAxis.setAxisMinValue(yMin);
}
// display data sets:
CandleData data = new CandleData(xValues, dataSets);
data.setValueTextColor(Color.WHITE);
data.setValueTextSize(9f);
cellChart.setData(data);
cellChart.getLegend().setTextColor(Color.WHITE);
cellChart.invalidate();
}
}