add altitude to db & altitude graph

This commit is contained in:
Florian Bezannier
2021-04-28 20:14:32 +02:00
parent 6d210a7b4a
commit 91f72884e6
5 changed files with 197 additions and 82 deletions
+4 -50
View File
@@ -10,8 +10,6 @@ from time import sleep
from oauth2_client.credentials_manager import ServiceInformation
import paho.mqtt.client as mqtt
from geojson import Feature, Point, FeatureCollection
from geojson import dumps as geo_dumps
import psa_connectedcar as psac
from libs.car import Cars, Car
@@ -22,7 +20,7 @@ from otp.otp import load_otp, new_otp_session, save_otp, ConfigException, Otp
from psa_connectedcar.rest import ApiException
from mylogger import logger
from utils import get_temp, rate_limit
from utils import rate_limit
from web.abrp import Abrp
from web.db import Database
@@ -451,13 +449,15 @@ class MyPSACC:
longitude = car.status.last_position.geometry.coordinates[0]
latitude = car.status.last_position.geometry.coordinates[1]
altitude = car.status.last_position.geometry.coordinates[3]
date = car.status.last_position.properties.updated_at
if date is None:
date = charge_date
logger.debug("vin:%s longitude:%s latitude:%s date:%s mileage:%s level:%s charge_date:%s level_fuel:"
"%s moving:%s", car.vin, longitude, latitude, date, mileage, level, charge_date, level_fuel,
moving)
self.__record_position(car.vin, mileage, latitude, longitude, date, level, level_fuel, moving)
Database.record_position(self.weather_api, car.vin, mileage, latitude, longitude, altitude, date, level,
level_fuel, moving)
self.abrp.call(car, Database.get_last_temp(car.vin))
try:
charging_status = car.status.get_energy('Electric').charging.status
@@ -467,52 +467,6 @@ class MyPSACC:
except AttributeError:
logger.error("charging status not available from api")
def __record_position(self, vin, mileage, latitude, longitude, date, level, level_fuel, moving):
conn = Database.get_db()
if mileage == 0: # fix a bug of the api
logger.error("The api return a wrong mileage for %s : %f", vin, mileage)
else:
if conn.execute("SELECT Timestamp from position where Timestamp=?", (date,)).fetchone() is None:
temp = get_temp(latitude, longitude, self.weather_api)
if level_fuel == 0: # fix fuel level not provided when car is off
try:
level_fuel = conn.execute(
"SELECT level_fuel FROM position WHERE level_fuel>0 AND VIN=? ORDER BY Timestamp DESC "
"LIMIT 1",
(vin,)).fetchone()[0]
logger.info("level_fuel fixed with last real value %f for %s", level_fuel, vin)
except TypeError:
level_fuel = None
logger.info("level_fuel unfixed for %s", vin)
conn.execute("INSERT INTO position(Timestamp,VIN,longitude,latitude,mileage,level,level_fuel,moving,"
"temperature) VALUES(?,?,?,?,?,?,?,?,?)",
(date, vin, longitude, latitude, mileage, level, level_fuel, moving, temp))
conn.commit()
logger.info("new position recorded for %s", vin)
Database.clean_position(conn)
return True
logger.debug("position already saved")
return False
@staticmethod
def get_recorded_position():
conn = Database.get_db()
res = conn.execute('SELECT * FROM position ORDER BY Timestamp')
features_list = []
for row in res:
if row["longitude"] is None or row["latitude"] is None:
continue
feature = Feature(geometry=Point((row["longitude"], row["latitude"])),
properties={"vin": row["vin"], "date": row["Timestamp"].strftime("%x %X"),
"mileage": row["mileage"],
"level": row["level"], "level_fuel": row["level_fuel"]})
features_list.append(feature)
feature_collection = FeatureCollection(features_list)
conn.close()
return geo_dumps(feature_collection, sort_keys=True)
def __iter__(self):
for key, value in self.__dict__.items():
yield key, value
+22 -4
View File
@@ -1,3 +1,4 @@
import traceback
from statistics import mean
from typing import List, Dict
@@ -6,7 +7,6 @@ from geojson import Feature, FeatureCollection, MultiLineString
from libs.car import Cars, Car
from mylogger import logger
from psa_connectedcar import Trips
from trip_parser import TripParser
from web.db import Database
@@ -36,6 +36,7 @@ class Trip:
self.duration = None
self.mileage = None
self.car: Car = None
self.altitude_diff = None
self.temperatures = []
def add_points(self, latitude, longitude):
@@ -87,14 +88,22 @@ class Trip:
"average consumption": self.consumption_km,
"average consumption fuel": self.consumption_fuel_km})
def get_info(self):
def get_info(self, row_id=None):
res = {"start_at": self.start_at.astimezone(tz.tzlocal()).replace(tzinfo=None).strftime("%x %X"),
# convert to naive tz,
"duration": self.duration * 60, "speed_average": self.speed_average,
"consumption_km": self.consumption_km, "consumption_fuel_km": self.consumption_fuel_km,
"distance": self.distance, "mileage": self.mileage}
"distance": self.distance, "mileage": self.mileage, "altitude_diff": self.altitude_diff}
if row_id is not None:
res["id"] = row_id
return res
def set_altitude_diff(self, start, end):
try:
self.altitude_diff = end - start
except (NameError, TypeError):
pass
class Trips(list):
def __init__(self, *args):
@@ -125,7 +134,7 @@ class Trips(list):
# flake8: noqa: C901
@staticmethod
def get_trips(vehicles_list: Cars) -> Dict[str, Trips]:
def get_trips(vehicles_list: Cars) -> Dict[str, "Trips"]:
# pylint: disable=too-many-locals,too-many-statements,too-many-nested-blocks
conn = Database.get_db()
vehicles = conn.execute(
@@ -200,6 +209,7 @@ class Trips(list):
trip.duration = (end["Timestamp"] - start["Timestamp"]).total_seconds() / 3600
trip.speed_average = trip.distance / trip.duration
diff_level, diff_level_fuel = trip_parser.get_level_consumption(start, end)
trip.set_altitude_diff(start["altitude"], end["altitude"])
trip.car = car
if diff_level != 0:
trip.set_consumption(diff_level) # kw
@@ -220,3 +230,11 @@ class Trips(list):
end = next_el
trips_by_vin[vin] = trips
return trips_by_vin
def get_info(self):
res = []
id = 1
for trip in self:
res.append(trip.get_info(id))
id += 1
return res
+109 -17
View File
@@ -1,13 +1,21 @@
import sys
import sqlite3
import traceback
from datetime import datetime
from time import sleep
from typing import Callable
import pytz
import requests
from geojson import Feature, Point, FeatureCollection
from geojson import dumps as geo_dumps
from mylogger import logger
from utils import get_temp
NEW_BATTERY_COLUMNS = [["battery", "INTEGER"], ["charging_mode", "TEXT"]]
NEW_POSITION_COLUMNS = [["level_fuel", "INTEGER"], ["altitude", "INTEGER"]]
DATE_FORMAT = "%Y-%m-%d %H:%M:%S+00:00"
@@ -26,7 +34,6 @@ class Database:
def convert_datetime_from_bytes(bytes_string):
return datetime.strptime(bytes_string.decode("utf-8"), DATE_FORMAT).replace(tzinfo=pytz.UTC)
@staticmethod
def convert_datetime_from_string(st):
return datetime.strptime(st, DATE_FORMAT).replace(tzinfo=pytz.UTC)
@@ -54,42 +61,46 @@ class Database:
@staticmethod
def init_db(conn):
conn.execute("CREATE TABLE IF NOT EXISTS position (Timestamp DATETIME PRIMARY KEY, VIN TEXT, longitude REAL, "
"latitude REAL, mileage REAL, level INTEGER, level_fuel INTEGER, moving BOOLEAN,"
" temperature INTEGER);")
conn.execute("""CREATE TABLE IF NOT EXISTS position (Timestamp DATETIME PRIMARY KEY,
VIN TEXT, longitude REAL,
latitude REAL,
mileage REAL,
level INTEGER,
level_fuel INTEGER,
moving BOOLEAN,
temperature INTEGER,
altitude INTEGER);""")
make_backup = False
try:
conn.execute("ALTER TABLE position ADD level_fuel INTEGER;")
make_backup = True
except sqlite3.OperationalError:
pass
conn.execute("CREATE TABLE IF NOT EXISTS battery (start_at DATETIME PRIMARY KEY,stop_at DATETIME,VIN TEXT, "
"start_level INTEGER, end_level INTEGER, co2 INTEGER, kw INTEGER);")
conn.create_function("update_trips", 0, Database.update_callback)
conn.execute("CREATE TEMP TRIGGER IF NOT EXISTS update_trigger AFTER INSERT ON position BEGIN "
"SELECT update_trips(); END;")
conn.execute("""CREATE TABLE IF NOT EXISTS battery_curve (start_at DATETIME, VIN TEXT, date DATETIME,
level INTEGER, UNIQUE(start_at, VIN, level));""")
for column, column_type in NEW_BATTERY_COLUMNS:
try:
conn.execute(f"ALTER TABLE battery ADD {column} {column_type};")
make_backup = True
except sqlite3.OperationalError:
pass
for table, columns in [["position", NEW_POSITION_COLUMNS], ["battery", NEW_BATTERY_COLUMNS]]:
for column, column_type in columns:
try:
conn.execute(f"ALTER TABLE {table} ADD {column} {column_type};")
make_backup = True
except sqlite3.OperationalError:
pass
if make_backup:
Database.backup(conn)
Database.clean_battery(conn)
Database.add_altitude_to_db(conn)
conn.commit()
Database.db_initialized = True
@staticmethod
def get_db(db_file=None):
def get_db(db_file=None, update_callback=True):
if db_file is None:
db_file = Database.DEFAULT_DB_FILE
sqlite3.register_converter("DATETIME", Database.convert_datetime_from_bytes)
sqlite3.register_adapter(datetime, Database.convert_datetime_to_string)
conn = sqlite3.connect(db_file, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
conn.row_factory = sqlite3.Row
if update_callback:
conn.create_function("update_trips", 0, Database.update_callback)
if not Database.db_initialized:
Database.init_db(conn)
return conn
@@ -135,3 +146,84 @@ class Database:
def get_battery_curve(conn, start_at, vin):
return convert_sql_res(conn.execute("""SELECT date, level FROM battery_curve
WHERE start_at=? and VIN=?;""", (start_at, vin)).fetchall())
@staticmethod
def add_altitude_to_db(conn):
max_pos_by_req = 100
nb_null = conn.execute(
"SELECT COUNT(1) FROM position WHERE altitude IS NULL;").fetchone()[0]
if nb_null > max_pos_by_req:
logger.warning("There is %s to fetch from API, it can take some time")
try:
while True:
res = conn.execute(f"SELECT DISTINCT latitude,longitude "
f"FROM position WHERE altitude IS NULL LIMIT {max_pos_by_req};").fetchall()
nb_res = len(res)
if nb_res > 0:
logger.info("add altitude for %s", len(res))
locations_str = ""
for line in res:
locations_str += str(line[0]) + "," + str(line[1]) + "|"
locations_str = locations_str[:-1]
res = requests.get("https://api.opentopodata.org/v1/srtm30m",
params={"locations": locations_str})
data = res.json()["results"]
for line in data:
conn.execute("UPDATE position SET altitude=? WHERE latitude=? and longitude=?",
(line["elevation"], line["location"]["lat"], line["location"]["lng"]))
conn.commit()
if nb_res == 100:
sleep(1) # API is limited to 1 call by sec
else:
break
except (ValueError, KeyError):
logger.error("Can't get altitude from API")
logger.debug(traceback.format_exc())
@staticmethod
def get_recorded_position():
conn = Database.get_db()
res = conn.execute('SELECT * FROM position ORDER BY Timestamp')
features_list = []
for row in res:
if row["longitude"] is None or row["latitude"] is None:
continue
feature = Feature(geometry=Point((row["longitude"], row["latitude"])),
properties={"vin": row["vin"], "date": row["Timestamp"].strftime("%x %X"),
"mileage": row["mileage"],
"level": row["level"], "level_fuel": row["level_fuel"]})
features_list.append(feature)
feature_collection = FeatureCollection(features_list)
conn.close()
return geo_dumps(feature_collection, sort_keys=True)
# pylint: disable=too-many-arguments
@staticmethod
def record_position(weather_api, vin, mileage, latitude, longitude, altitude, date, level, level_fuel, moving):
conn = Database.get_db()
if mileage == 0: # fix a bug of the api
logger.error("The api return a wrong mileage for %s : %f", vin, mileage)
else:
if conn.execute("SELECT Timestamp from position where Timestamp=?", (date,)).fetchone() is None:
temp = get_temp(latitude, longitude, weather_api)
if level_fuel == 0: # fix fuel level not provided when car is off
try:
level_fuel = conn.execute(
"SELECT level_fuel FROM position WHERE level_fuel>0 AND VIN=? ORDER BY Timestamp DESC "
"LIMIT 1",
(vin,)).fetchone()[0]
logger.info("level_fuel fixed with last real value %f for %s", level_fuel, vin)
except TypeError:
level_fuel = None
logger.info("level_fuel unfixed for %s", vin)
conn.execute("INSERT INTO position(Timestamp,VIN,longitude,latitude,altitude,mileage,level,level_fuel,"
"moving,temperature) VALUES(?,?,?,?,?,?,?,?,?,?)",
(date, vin, longitude, latitude, altitude, mileage, level, level_fuel, moving, temp))
conn.commit()
logger.info("new position recorded for %s", vin)
Database.clean_position(conn)
return True
logger.debug("position already saved")
return False
+28 -5
View File
@@ -18,7 +18,7 @@ import pytz
from libs.car import Car
from libs.elec_price import ElecPrice
from trip import Trips
from trip import Trips, Trip
from web.db import Database
@@ -87,8 +87,9 @@ def get_figures(trips: Trips, charging: List[dict]):
table_fig = dash_table.DataTable(
id='trips-table',
sort_action='native',
# sort_by=[{'column_id': 'start_at', 'direction': 'desc'}],
columns=[{'id': 'start_at', 'name': 'start at', 'type': 'datetime'},
sort_by=[{'column_id': 'id', 'direction': 'desc'}],
columns=[{'id': 'id', 'name': '#', 'type': 'numeric'},
{'id': 'start_at', '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',
@@ -100,8 +101,18 @@ def get_figures(trips: Trips, charging: List[dict]):
{'id': 'distance', 'name': 'distance', 'type': 'numeric',
'format': nb_format.symbol_suffix(" km").precision(1)},
{'id': 'mileage', 'name': 'mileage', 'type': 'numeric',
'format': nb_format.symbol_suffix(" km").precision(1)}],
data=[tr.get_info() for tr in trips[::-1]],
'format': nb_format},
{'id': 'altitude_diff', 'name': 'Altitude diff', 'type': 'numeric',
'format': deepcopy(nb_format).symbol_suffix(" m").precision(0)}
],
style_data_conditional=[
{
'if': {'column_id': ['altitude_diff']},
'color': 'dodgerblue',
"text-decoration": "underline"
}
],
data=trips.get_info(),
page_size=50
)
# consumption_fig
@@ -243,3 +254,15 @@ def get_battery_curve_fig(row: dict, car: Car):
fig = px.line(battery_curves, x="level", y="speed")
fig.update_layout(xaxis_title="Battery %", yaxis_title="Charging speed in kW")
return html.Div(Graph(figure=fig))
def get_altitude_fig(trip:Trip):
conn = Database.get_db()
res = list(map(list, conn.execute("SELECT mileage, altitude FROM position WHERE Timestamp>=? and Timestamp<=?;",
(trip.start_at, trip.end_at)).fetchall()))
start_mileage = res[0][0]
for line in res:
line[0] = line[0] - start_mileage
fig = px.line(res, x=0, y=1)
fig.update_layout(xaxis_title="Distance km", yaxis_title="Altitude m")
return html.Div(Graph(figure=fig))
+34 -6
View File
@@ -63,7 +63,7 @@ def create_callback():
Output('consumption_fig_by_speed', 'figure'),
Output('consumption_graph_by_temp', 'children'),
Output('consumption', 'children'),
Output('tab_trips', 'children'),
Output('tab_trips_fig', 'children'),
Output('tab_battery_fig', 'children'),
Output('tab_charge', 'children'),
Output('date-slider', 'max'),
@@ -108,18 +108,29 @@ def create_callback():
[Input("battery-table", "active_cell"),
Input("tab_battery_popup-close", "n_clicks")],
[State('battery-table', 'data'),
State("tab_battery_popup", "is_open")]
)
State("tab_battery_popup", "is_open")])
def get_battery_curve(active_cell, close, data, is_open): # pylint: disable=unused-argument
if is_open is None:
is_open = False
if active_cell is not None and active_cell["column_id"] in ["start_level", "end_level"] and not is_open:
row = data[active_cell["row"]]
print("ok")
return figures.get_battery_curve_fig(row, myp.vehicles_list[0]), True
return "", False
@dash_app.callback([Output("tab_trips_popup_graph", "children"), Output("tab_trips_popup", "is_open"), ],
[Input("trips-table", "active_cell"),
Input("tab_trips_popup-close", "n_clicks")],
State("tab_trips_popup", "is_open"))
def get_altitude(active_cell, close, is_open): # pylint: disable=unused-argument
print("altitude")
if is_open is None:
is_open = False
if active_cell is not None and active_cell["column_id"] in ["altitude_diff"] and not is_open:
return figures.get_altitude_fig(trips[active_cell["row_id"]-1]), True
return "", False
@dash_app.callback(Output({'role': ABRP_SWITCH + RESPONSE, 'vin': MATCH}, 'children'),
Input({'role': ABRP_SWITCH, 'vin': MATCH}, 'id'),
Input({'role': ABRP_SWITCH, 'vin': MATCH}, 'value'))
@@ -208,7 +219,7 @@ def get_charge_control():
@app.route('/positions')
def get_recorded_position():
return FlaskResponse(myp.get_recorded_position(), mimetype='application/json')
return FlaskResponse(Database.get_recorded_position(), mimetype='application/json')
@app.route('/abrp')
@@ -236,6 +247,7 @@ def after_request(response):
def update_trips():
global trips, chargings, cached_layout
logger.info("update_data")
Database.add_altitude_to_db(Database.get_db(update_callback=False))
try:
trips_by_vin = Trips.get_trips(myp.vehicles_list)
trips = next(iter(trips_by_vin.values())) # todo handle multiple car
@@ -310,7 +322,23 @@ def serve_layout():
html.Div([
dbc.Tabs([
dbc.Tab(label="Summary", tab_id="summary", children=summary_tab),
dbc.Tab(label="Trips", tab_id="trips", id="tab_trips", children=[figures.table_fig]),
dbc.Tab(label="Trips", tab_id="trips", id="tab_trips",
children=[html.Div(id="tab_trips_fig", children=figures.table_fig),
dbc.Modal(
[
dbc.ModalHeader("Altitude"),
dbc.ModalBody(html.Div(
id="tab_trips_popup_graph")),
dbc.ModalFooter(
dbc.Button("Close",
id="tab_trips_popup-close",
className="ml-auto")
),
],
id="tab_trips_popup",
size="xl",
)
]),
dbc.Tab(label="Battery", tab_id="battery", id="tab_battery",
children=[html.Div(id="tab_battery_fig", children=[figures.battery_info]),
dbc.Modal(