mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-21 17:06:18 +00:00
Merge pull request #462 from stevoh6/feature/#386-mileage-in-charge-table
Feature/#386 mileage in charge table
This commit is contained in:
@@ -60,14 +60,14 @@ class Charging:
|
||||
@staticmethod
|
||||
def record_charging(car: Car, charging_status, charge_date: datetime, level, latitude,
|
||||
# pylint: disable=too-many-locals
|
||||
longitude, country_code, charging_mode, charging_rate, autonomy):
|
||||
longitude, country_code, charging_mode, charging_rate, autonomy, mileage):
|
||||
conn = Database.get_db()
|
||||
charge_date = charge_date.replace(microsecond=0)
|
||||
if charging_status == "InProgress":
|
||||
last_charge = Database.get_last_charge(car.vin)
|
||||
if Charging.is_charge_ended(last_charge):
|
||||
conn.execute("INSERT INTO battery(start_at,start_level,charging_mode,VIN) VALUES(?,?,?,?)",
|
||||
(charge_date, level, charging_mode, car.vin))
|
||||
conn.execute("INSERT INTO battery(start_at,start_level,charging_mode,VIN,mileage) VALUES(?,?,?,?,?)",
|
||||
(charge_date, level, charging_mode, car.vin, mileage))
|
||||
start_at = charge_date
|
||||
else:
|
||||
start_at = last_charge.start_at
|
||||
@@ -93,6 +93,7 @@ class Charging:
|
||||
last_charge.co2 = co2_per_kw
|
||||
last_charge.kw = consumption_kw
|
||||
last_charge.stop_at = charge_date
|
||||
last_charge.mileage = mileage
|
||||
Charging.update_chargings(conn, last_charge, car)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
@@ -200,7 +200,7 @@ class PSAClient:
|
||||
charging_rate = car.status.get_energy('Electric').charging.charging_rate
|
||||
autonomy = car.status.get_energy('Electric').autonomy
|
||||
Charging.record_charging(car, charging_status, charge_date, level, latitude, longitude, self.country_code,
|
||||
charging_mode, charging_rate, autonomy)
|
||||
charging_mode, charging_rate, autonomy, mileage)
|
||||
logger.debug("charging_status:%s ", charging_status)
|
||||
except AttributeError as ex:
|
||||
logger.error("charging status not available from api")
|
||||
|
||||
@@ -15,7 +15,7 @@ class ChargingMode(Enum):
|
||||
class Charge:
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, start_at: datetime, stop_at: datetime = None, vin=None, start_level=None, end_level=None,
|
||||
co2=None, kw=None, price=None, charging_mode=None):
|
||||
co2=None, kw=None, price=None, charging_mode=None, mileage=None):
|
||||
assert isinstance(start_at, datetime)
|
||||
self.charging_mode: ChargingMode = ChargingMode(charging_mode)
|
||||
self.start_at = start_at
|
||||
@@ -26,3 +26,4 @@ class Charge:
|
||||
self.co2 = co2
|
||||
self.kw = kw
|
||||
self.price = price
|
||||
self.mileage = mileage
|
||||
|
||||
@@ -14,6 +14,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_CONFIG = """[General]
|
||||
currency = €
|
||||
# define format for data export, can be csv or xlsx
|
||||
export_format = csv
|
||||
# minimum trip length in km so it's added to stats and map in website
|
||||
minimum trip length =
|
||||
# for future use
|
||||
@@ -83,6 +85,7 @@ class GeneralConfig(BaseModel):
|
||||
currency: str = "€"
|
||||
length_unit: str = "km"
|
||||
minimum_trip_length: float = 10
|
||||
export_format = "csv"
|
||||
|
||||
|
||||
class ElectricityPriceConfig(BaseModel):
|
||||
|
||||
@@ -18,7 +18,7 @@ from psa_car_controller.psacc.utils.utils import get_temp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
NEW_BATTERY_COLUMNS = [["price", "INTEGER"], ["charging_mode", "TEXT"]]
|
||||
NEW_BATTERY_COLUMNS = [["price", "INTEGER"], ["charging_mode", "TEXT"], ["mileage", "REAL"]]
|
||||
NEW_POSITION_COLUMNS = [["level_fuel", "INTEGER"], ["altitude", "INTEGER"]]
|
||||
NEW_BATTERY_CURVE_COLUMNS = [["rate", "INTEGER"], ["autonomy", "INTEGER"]]
|
||||
|
||||
@@ -270,8 +270,7 @@ class Database:
|
||||
@staticmethod
|
||||
def get_last_charge(vin) -> Charge:
|
||||
conn = Database.get_db()
|
||||
res = conn.execute("SELECT start_at, stop_at, vin, start_level, end_level, co2, kw, price, charging_mode "
|
||||
"FROM battery WHERE VIN=? ORDER BY start_at DESC limit 1", (vin,)).fetchone()
|
||||
res = conn.execute("SELECT * FROM battery WHERE VIN=? ORDER BY start_at DESC limit 1", (vin,)).fetchone()
|
||||
if res:
|
||||
return Charge(**dict_key_to_lower_case(**res))
|
||||
return None
|
||||
@@ -295,6 +294,8 @@ class Database:
|
||||
|
||||
@staticmethod
|
||||
def update_charge(charge: Charge):
|
||||
# we don't need to update mileage, since it should be inserted at beginning of charge,
|
||||
# maybe in future this will be supported
|
||||
conn = Database.get_db()
|
||||
res = conn.execute(
|
||||
"UPDATE battery set stop_at=?, end_level=?, co2=?, kw=?, price=? WHERE start_at=? and VIN=?",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#battery-table button.export,
|
||||
#trips-table button.export {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.export-load-anim div.dash-sk-circle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dash-table-container .dash-spreadsheet-container .dash-spreadsheet-inner tr:hover > td:not(.focused) {
|
||||
background-image: linear-gradient(#ffeb3b75 0 0);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ from dash import html
|
||||
from dash.dash_table import DataTable
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
from dash.dash_table.Format import Scheme, Symbol, Format
|
||||
from dash.dash_table.Format import Scheme, Symbol, Group, Format
|
||||
from dash.dcc import Graph
|
||||
|
||||
from psa_car_controller.psacc.application.charging import Charging
|
||||
@@ -35,6 +35,7 @@ ELEC_CONSUM_PRICE = "elec_consum_price"
|
||||
AVG_CONSUM_KW = "avg_consum_kw"
|
||||
AVG_CONSUM_PRICE = "avg_consum_price"
|
||||
CURRENCY = "€"
|
||||
EXPORT_FORMAT = "csv"
|
||||
|
||||
SUMMARY_CARDS = {"Average consumption": {"text": [card_value_div(AVG_CONSUM_KW, "kWh/100km"),
|
||||
card_value_div(AVG_CONSUM_PRICE, f"{CURRENCY}/100km")],
|
||||
@@ -62,7 +63,7 @@ def get_figures(car: Car):
|
||||
lon=[lons[0]], lat=[lats[0]],
|
||||
showlegend=False, name="Last Position"))
|
||||
# table
|
||||
nb_format = Format(precision=2, scheme=Scheme.fixed, symbol=Symbol.yes) # pylint: disable=no-member
|
||||
nb_format = Format(precision=2, scheme=Scheme.fixed, symbol=Symbol.yes, group=Group.yes)
|
||||
style_cell_conditional = []
|
||||
if car.is_electric():
|
||||
style_cell_conditional.append({'if': {'column_id': 'consumption_fuel_km', }, 'display': 'None', })
|
||||
@@ -70,15 +71,22 @@ def get_figures(car: Car):
|
||||
style_cell_conditional.append({'if': {'column_id': 'consumption_km', }, 'display': 'None', })
|
||||
table_fig = DataTable(
|
||||
id='trips-table',
|
||||
export_format=EXPORT_FORMAT,
|
||||
sort_action='custom',
|
||||
sort_by=[{'column_id': 'id', 'direction': 'desc'}],
|
||||
style_data={
|
||||
'width': '10%',
|
||||
'maxWidth': '10%',
|
||||
'minWidth': '10%',
|
||||
'color': 'gray'
|
||||
},
|
||||
columns=[{'id': 'id', 'name': '#', 'type': 'numeric'},
|
||||
{'id': 'start_at_str', 'name': 'start at', 'type': 'datetime'},
|
||||
{'id': 'duration', 'name': 'duration', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" min").precision(0)},
|
||||
{'id': 'speed_average', 'name': 'average speed', 'type': 'numeric',
|
||||
{'id': 'speed_average', 'name': 'avg. speed', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" km/h").precision(0)},
|
||||
{'id': 'consumption_km', 'name': 'average consumption', 'type': 'numeric',
|
||||
{'id': 'consumption_km', 'name': 'avg. consumption', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" kWh/100km")},
|
||||
{'id': 'consumption_fuel_km', 'name': 'average consumption fuel', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" L/100km")},
|
||||
@@ -86,14 +94,17 @@ def get_figures(car: Car):
|
||||
'format': nb_format.symbol_suffix(" km").precision(1)},
|
||||
{'id': 'mileage', 'name': 'mileage', 'type': 'numeric',
|
||||
'format': nb_format},
|
||||
{'id': 'altitude_diff', 'name': 'Altitude diff', 'type': 'numeric',
|
||||
{'id': 'altitude_diff', 'name': 'altitude diff', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" m").precision(0)}
|
||||
],
|
||||
style_data_conditional=[
|
||||
{
|
||||
'if': {'column_id': ['id']},
|
||||
'width': '5%'
|
||||
},
|
||||
{
|
||||
'if': {'column_id': ['altitude_diff']},
|
||||
'color': 'dodgerblue',
|
||||
"text-decoration": "underline"
|
||||
'color': 'black'
|
||||
}
|
||||
],
|
||||
style_cell_conditional=style_cell_conditional,
|
||||
@@ -113,11 +124,17 @@ def get_figures(car: Car):
|
||||
consumption_fig_by_speed.add_trace(go.Scatter(mode="markers", x=[0],
|
||||
y=[0], name="Trips"))
|
||||
consumption_fig_by_speed.update_layout(xaxis_title="average Speed km/h", yaxis_title="Consumption kWh/100Km")
|
||||
|
||||
# battery_table
|
||||
battery_table = DataTable(
|
||||
id='battery-table',
|
||||
export_format=EXPORT_FORMAT,
|
||||
sort_action='custom',
|
||||
style_data={
|
||||
'width': '10%',
|
||||
'maxWidth': '10%',
|
||||
'minWidth': '10%',
|
||||
'color': 'gray'
|
||||
},
|
||||
sort_by=[{'column_id': 'start_at_str', 'direction': 'desc'}],
|
||||
columns=[{'id': 'start_at_str', 'name': 'start at', 'type': 'datetime'},
|
||||
{'id': 'stop_at_str', 'name': 'stop at', 'type': 'datetime'},
|
||||
@@ -128,18 +145,41 @@ def get_figures(car: Car):
|
||||
{'id': 'kw', 'name': 'consumption', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" kWh").precision(2)},
|
||||
{'id': 'price', 'name': 'price', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" " + CURRENCY).precision(2), 'editable': True}
|
||||
'format': deepcopy(nb_format).symbol_suffix(" " + CURRENCY).precision(2), 'editable': True},
|
||||
{'id': 'charging_mode', 'name': 'charging mode', 'type': 'string'},
|
||||
{'id': 'mileage', 'name': 'mileage', 'type': 'numeric', 'format': nb_format},
|
||||
],
|
||||
data=[],
|
||||
page_size=50,
|
||||
style_data_conditional=[
|
||||
{
|
||||
'if': {'column_id': ['start_level', "end_level"]},
|
||||
'color': 'dodgerblue',
|
||||
"text-decoration": "underline"
|
||||
'color': 'black'
|
||||
},
|
||||
{
|
||||
'if': {
|
||||
'filter_query': '{start_level} < 15',
|
||||
'column_id': 'start_level'
|
||||
},
|
||||
'color': 'red'
|
||||
},
|
||||
{
|
||||
'if': {
|
||||
'filter_query': '{end_level} > 85',
|
||||
'column_id': 'end_level'
|
||||
},
|
||||
'color': 'green'
|
||||
},
|
||||
{
|
||||
'if': {
|
||||
'filter_query': '{charging_mode} = "Quick"'
|
||||
},
|
||||
'backgroundColor': 'ivory'
|
||||
},
|
||||
{
|
||||
'if': {'column_id': 'price'},
|
||||
'backgroundColor': '#ABE2FB'
|
||||
'color': 'dodgerblue',
|
||||
'font-weihgt': 'bold'
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from dash import dcc, html
|
||||
from dash.dependencies import Output, Input, State
|
||||
from dash.exceptions import PreventUpdate
|
||||
from flask import jsonify, request, Response as FlaskResponse
|
||||
import time
|
||||
|
||||
from psa_car_controller.common.mylogger import CustomLogger
|
||||
from psa_car_controller.psacc.application.car_controller import PSACarController
|
||||
@@ -125,7 +126,39 @@ def create_callback(): # noqa: MC0001
|
||||
return figures.get_altitude_fig(trips[active_cell["row_id"] - 1]), True
|
||||
return "", False
|
||||
|
||||
@dash_app.callback(Output("loading-output-trips", "children"), Input("export-trips-table", "n_clicks"))
|
||||
def export_trips_loading_animation(n_clicks): # pylint: disable=unused-argument
|
||||
time.sleep(3)
|
||||
|
||||
@dash_app.callback(Output("loading-output-battery", "children"), Input("export-battery-table", "n_clicks"))
|
||||
def export_batt_loading_animation(n_clicks): # pylint: disable=unused-argument
|
||||
time.sleep(3)
|
||||
# Emulate click on original Export datatables button, since original button is hard to modify
|
||||
dash_app.clientside_callback(
|
||||
"""
|
||||
function(n_clicks) {
|
||||
if (n_clicks > 0)
|
||||
document.querySelector("#trips-table button.export").click()
|
||||
return ""
|
||||
}
|
||||
""",
|
||||
Output("trips-table", "data-dummy"),
|
||||
[Input("export-trips-table", "n_clicks")]
|
||||
)
|
||||
dash_app.clientside_callback(
|
||||
"""
|
||||
function(n_clicks) {
|
||||
if (n_clicks > 0)
|
||||
document.querySelector("#battery-table button.export").click()
|
||||
return ""
|
||||
}
|
||||
""",
|
||||
Output("battery-table", "data-dummy"),
|
||||
[Input("export-battery-table", "n_clicks")]
|
||||
)
|
||||
|
||||
figures.CURRENCY = APP.config.General.currency
|
||||
figures.EXPORT_FORMAT = APP.config.General.export_format
|
||||
CALLBACK_CREATED = True
|
||||
|
||||
|
||||
@@ -343,7 +376,25 @@ def serve_layout():
|
||||
dbc.Tabs([
|
||||
dbc.Tab(label="Summary", tab_id="summary", children=summary_tab),
|
||||
dbc.Tab(label="Trips", tab_id="trips", id="tab_trips",
|
||||
children=[html.Div(id="tab_trips_fig", children=figures.table_fig),
|
||||
children=[dbc.Row(
|
||||
dbc.Col([
|
||||
dcc.Loading(
|
||||
id="loading-div-trips",
|
||||
children=[html.Div([html.Div(id="loading-output-trips")])],
|
||||
type="circle",
|
||||
className="export-load-anim"
|
||||
),
|
||||
dbc.Button("Export trips data",
|
||||
id="export-trips-table",
|
||||
n_clicks=0,
|
||||
size="sm",
|
||||
color="light",
|
||||
className="m-1 w-200"
|
||||
)],
|
||||
className="d-grid gap-2 d-md-flex justify-content-md-end"
|
||||
)
|
||||
),
|
||||
html.Div(id="tab_trips_fig", children=figures.table_fig),
|
||||
dbc.Modal(
|
||||
[
|
||||
dbc.ModalHeader("Altitude"),
|
||||
@@ -360,7 +411,25 @@ def serve_layout():
|
||||
)
|
||||
]),
|
||||
dbc.Tab(label="Charge", tab_id="charge", id="tab_charge",
|
||||
children=[figures.battery_table,
|
||||
children=[dbc.Row(
|
||||
dbc.Col([
|
||||
dcc.Loading(
|
||||
id="loading-div-battery",
|
||||
children=[html.Div([html.Div(id="loading-output-battery")])],
|
||||
type="circle",
|
||||
className="export-load-anim"
|
||||
),
|
||||
dbc.Button("Export charging data",
|
||||
id="export-battery-table",
|
||||
n_clicks=0,
|
||||
size="sm",
|
||||
color="light",
|
||||
className="m-1 w-200"
|
||||
)],
|
||||
className="d-grid gap-2 d-md-flex justify-content-md-end"
|
||||
)
|
||||
),
|
||||
figures.battery_table,
|
||||
dbc.Modal(
|
||||
[
|
||||
dbc.ModalHeader(
|
||||
@@ -399,6 +468,7 @@ try:
|
||||
Charging.set_default_price(APP.myp.vehicles_list)
|
||||
Database.set_db_callback(update_trips)
|
||||
figures.CURRENCY = APP.config.General.currency
|
||||
figures.EXPORT_FORMAT = APP.config.General.export_format
|
||||
update_trips()
|
||||
except (IndexError, TypeError):
|
||||
logger.debug("Failed to get trips, there is probably not enough data yet:", exc_info=True)
|
||||
|
||||
+8
-6
@@ -188,11 +188,12 @@ class TestUnit(unittest.TestCase):
|
||||
Charging.elec_price = ConfigRepository.read_config(DATA_DIR + "config.ini").Electricity_config
|
||||
start_level = 40
|
||||
end_level = 85
|
||||
Charging.record_charging(car, "InProgress", date0, start_level, latitude, longitude, None, "slow", 20, 60)
|
||||
Charging.record_charging(car, "InProgress", date1, 70, latitude, longitude, "FR", "slow", 20, 60)
|
||||
Charging.record_charging(car, "InProgress", date1, 70, latitude, longitude, "FR", "slow", 20, 60)
|
||||
Charging.record_charging(car, "InProgress", date2, 80, latitude, longitude, "FR", "slow", 20, 60)
|
||||
Charging.record_charging(car, "Stopped", date3, end_level, latitude, longitude, "FR", "slow", 20, 60)
|
||||
mileage = 123456789.1
|
||||
Charging.record_charging(car, "InProgress", date0, start_level, latitude, longitude, None, "slow", 20, 60, mileage)
|
||||
Charging.record_charging(car, "InProgress", date1, 70, latitude, longitude, "FR", "slow", 20, 60, mileage)
|
||||
Charging.record_charging(car, "InProgress", date1, 70, latitude, longitude, "FR", "slow", 20, 60, mileage)
|
||||
Charging.record_charging(car, "InProgress", date2, 80, latitude, longitude, "FR", "slow", 20, 60, mileage)
|
||||
Charging.record_charging(car, "Stopped", date3, end_level, latitude, longitude, "FR", "slow", 20, 60, mileage)
|
||||
chargings = Charging.get_chargings()
|
||||
co2 = chargings[0]["co2"]
|
||||
assert isinstance(co2, float)
|
||||
@@ -204,7 +205,8 @@ class TestUnit(unittest.TestCase):
|
||||
'co2': co2,
|
||||
'kw': 20.7,
|
||||
'price': 4.29,
|
||||
'charging_mode': 'slow'}])
|
||||
'charging_mode': 'slow',
|
||||
'mileage': 123456789.1}])
|
||||
assert get_figures(car)
|
||||
row = {"start_at": date0.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
|
||||
"stop_at": date3.strftime('%Y-%m-%dT%H:%M:%S.000Z'), "start_level": start_level, "end_level": end_level}
|
||||
|
||||
+5
-5
@@ -47,11 +47,11 @@ def record_position():
|
||||
|
||||
|
||||
def record_charging():
|
||||
Charging.record_charging(car, "InProgress", date0, 50, latitude, longitude, "FR", "slow", 20, 60)
|
||||
Charging.record_charging(car, "InProgress", date1, 75, latitude, longitude, "FR", "slow", 20, 60)
|
||||
Charging.record_charging(car, "InProgress", date2, 85, latitude, longitude, "FR", "slow", 20, 60)
|
||||
Charging.record_charging(car, "InProgress", date3, 90, latitude, longitude, "FR", "slow", 20, 60)
|
||||
Charging.record_charging(car, "Stopped", date4, 91, latitude, longitude, "FR", "slow", 20, 60)
|
||||
Charging.record_charging(car, "InProgress", date0, 50, latitude, longitude, "FR", "slow", 20, 60, 123456789.1)
|
||||
Charging.record_charging(car, "InProgress", date1, 75, latitude, longitude, "FR", "slow", 20, 60, 123456789.1)
|
||||
Charging.record_charging(car, "InProgress", date2, 85, latitude, longitude, "FR", "slow", 20, 60, 123456789.1)
|
||||
Charging.record_charging(car, "InProgress", date3, 90, latitude, longitude, "FR", "slow", 20, 60, 123456789.1)
|
||||
Charging.record_charging(car, "Stopped", date4, 91, latitude, longitude, "FR", "slow", 20, 60, 123456789.1)
|
||||
|
||||
|
||||
def get_date(offset):
|
||||
|
||||
Reference in New Issue
Block a user