diff --git a/server.py b/server.py index be85c5d..7db8125 100755 --- a/server.py +++ b/server.py @@ -2,13 +2,16 @@ import atexit import sys from threading import Thread + from oauth2_client.credentials_manager import OAuthError + +import web.app from ChargeControl import ChargeControls from MyLogger import my_logger import argparse from MyLogger import logger from MyPSACC import MyPSACC -from web.app import app, save_config +from web.app import start_app, save_config parser = argparse.ArgumentParser() @@ -24,6 +27,7 @@ def parse_args(): parser.add_argument("-m", "--mail", help="change the email address") parser.add_argument("-P", "--password", help="change the password") parser.add_argument("--remote-disable", help="disable remote control") + parser.add_argument("-b", "--base-path", help="base path for app",default="/") parser.parse_args() return parser @@ -36,14 +40,14 @@ if __name__ == "__main__": my_logger(handler_level=args.debug) logger.info("server start") if args.config: - app.myp = MyPSACC.load_config(name=args.config.name) + web.app.myp = MyPSACC.load_config(name=args.config.name) else: - app.myp = MyPSACC.load_config() - atexit.register(app.myp.save_config) + web.app.myp = MyPSACC.load_config() + atexit.register(web.app.save_config) if args.record_position: - app.myp.set_record(True) + web.app.myp.set_record(True) try: - app.myp.manager._refresh_token() + web.app.myp.manager._refresh_token() except OAuthError: if args.mail and args.password: client_email = args.mail @@ -51,15 +55,15 @@ if __name__ == "__main__": else: client_email = input("mypeugeot email: ") client_password = input("mypeugeot password: ") - app.myp.connect(client_email, client_password) - logger.info(app.myp.get_vehicles()) - t1 = Thread(target=app.run, kwargs={"host": args.listen, "port": int(args.port)}) + web.app.myp.connect(client_email, client_password) + logger.info(web.app.myp.get_vehicles()) + t1 = Thread(target=start_app, args=["My car info", args.base_path, args.debug, args.listen, int(args.port)]) t1.start() if args.remote_disable: logger.info("mqtt disabled") else: - app.myp.start_mqtt() + web.app.myp.start_mqtt() if args.charge_control: - app.chc = ChargeControls.load_config(app.myp, name=args.charge_control) - app.chc.start() - save_config(app.myp) \ No newline at end of file + web.app.chc = ChargeControls.load_config(web.app.myp, name=args.charge_control) + web.app.chc.start() + save_config(web.app.myp) \ No newline at end of file diff --git a/web/app.py b/web/app.py index 9adad93..1b69fea 100644 --- a/web/app.py +++ b/web/app.py @@ -1,167 +1,47 @@ -import json import threading -import traceback -from datetime import datetime, timezone + import dash import dash_bootstrap_components as dbc -from dash.dependencies import Output, Input -import dash_core_components as dcc -import dash_html_components as html +from flask import Flask import locale + +from werkzeug import run_simple +from werkzeug.middleware.dispatcher import DispatcherMiddleware + from MyLogger import logger -from flask import jsonify, request, Response as FlaskResponse - -from web import figures - from MyPSACC import MyPSACC -try: - locale = locale.getlocale()[0].split("_")[0] - locale_url = [f"https://cdn.plot.ly/plotly-locale-{locale}-latest.js"] - dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP], external_scripts=locale_url, title="My car info") -except: - logger.warn("Can't get language") - dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP]) +app = None +dash_app = None +dispatcher = None + + +def start_app(title, base_path, debug: bool, host, port): + global app, dash_app, dispatcher + try: + lang = locale.getlocale()[0].split("_")[0] + locale_url = [f"https://cdn.plot.ly/plotly-locale-{lang}-latest.js"] + except: + locale_url = None + logger.warn("Can't get language") + app = Flask(__name__) + app.config["DEBUG"] = debug + if base_path == "/": + application = DispatcherMiddleware(app) + requests_pathname_prefix = None + else: + application = DispatcherMiddleware(Flask('dummy_app'), {base_path: app}) + requests_pathname_prefix = base_path + "/" + dash_app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP], external_scripts=locale_url, title=title, + server=app, requests_pathname_prefix=requests_pathname_prefix) + import web.callback + return run_simple(host, port, application, use_reloader=debug, use_debugger=debug) + -app = dash_app.server myp = None chc = None -@dash_app.callback(Output('trips_map', 'figure'), - Output('consumption_fig', 'figure'), - Output('consumption_fig_by_speed', 'figure'), - Output('consumption', 'children'), - Output('tab_trips', 'children'), - Input('date-slider', 'value')) -def display_value(value): - min = datetime.fromtimestamp(value[0], tz=timezone.utc) - max = datetime.fromtimestamp(value[1], tz=timezone.utc) - filtered_trips = [] - for trip in trips: - if min <= trip.start_at <= max: - filtered_trips.append(trip) - figures.get_figures(filtered_trips) - consumption = "Average consumption: {:.1f} kW/100km".format(float(figures.consumption_df.mean(numeric_only=True))) - return figures.trips_map, figures.consumption_fig, figures.consumption_fig_by_speed, consumption, figures.table_fig - - -@app.route('/getvehicles') -def getvehicules(): - return jsonify(myp.getVIN()) - - -@app.route('/get_vehicleinfo/') -def get_vehicle_Info(vin): - response = app.response_class( - response=json.dumps(myp.get_vehicle_info(vin).to_dict(), default=str), - status=200, - mimetype='application/json' - ) - return response - - -@app.route('/charge_now//') -def charge_now(vin, charge): - return jsonify(myp.charge_now(vin, charge != 0)) - - -@app.route('/charge_hour') -def change_charge_hour(): - return jsonify(myp.change_charge_hour(request.form['vin'], request.form['hour'], request.form['minute'])) - - -@app.route('/wakeup/') -def wakeup(vin): - return jsonify(myp.wakeup(vin)) - - -@app.route('/preconditioning//') -def preconditioning(vin, activate): - return jsonify(myp.preconditioning(vin, activate)) - - -@app.route('/position/') -def get_position(vin): - res = myp.get_vehicle_info(vin) - longitude, latitude = res.last_position.geometry.coordinates - return jsonify( - {"longitude": longitude, "latitude": latitude, "url": f"http://maps.google.com/maps?q={latitude},{longitude}"}) - - -# Set a battery threshold and schedule an hour to stop the charge -@app.route('/charge_control') -def charge_control(): - logger.info(request) - vin = request.args['vin'] - charge_control = chc.get(vin) - if charge_control is None: - return jsonify("error: VIN not in list") - if 'hour' in request.args or 'minute' in request.args: - charge_control.set_stop_hour([int(request.args["hour"]), int(request.args["minute"])]) - if 'percentage' in request.args: - charge_control.percentage_threshold = int(request.args['percentage']) - save_config() - return jsonify(charge_control.get_dict()) - - -@app.route('/positions') -def get_recorded_position(): - return FlaskResponse(myp.get_recorded_position(), mimetype='application/json') - - -@app.after_request -def after_request(response): - header = response.headers - header['Access-Control-Allow-Origin'] = '*' - return response - - def save_config(my_peugeot: MyPSACC): my_peugeot.save_config() - threading.Timer(30, save_config, args=[my_peugeot]).start() - - -try: - trips = MyPSACC.get_trips() - min_date = trips[0].start_at - max_date = trips[-1].start_at - min_millis = figures.unix_time_millis(min_date) - max_millis = figures.unix_time_millis(max_date) - step = (max_millis - min_millis) / 100 - figures.get_figures(trips) - data_div = html.Div([dcc.RangeSlider( - id='date-slider', - min=min_millis, - max=max_millis, - step=step, - marks=figures.get_marks_from_start_end(min_date, - max_date), - value=[min_millis, max_millis], - ), - html.Div([ - dbc.Tabs([ - dbc.Tab(label="Summary", tab_id="summary", children=[ - html.H2(id="consumption", - children=figures.info), - dcc.Graph(figure=figures.consumption_fig, id="consumption_fig"), - dcc.Graph(figure=figures.consumption_fig_by_speed, id="consumption_fig_by_speed") - ]), - dbc.Tab(label="Trips", tab_id="trips", id="tab_trips", children=[figures.table_fig]), - dbc.Tab(label="Map", tab_id="map", children=[ - dcc.Graph(figure=figures.trips_map, id="trips_map", style={"height": '90vh'})]), - ], - id="tabs", - active_tab="summary", - ), - html.Div(id="tab-content", className="p-4"), - ])]) -except: - logger.error("Failed to generate figure, there is probably not enough data yet") - logger.error(traceback.format_exc()) - data_div = dbc.Alert("No data to show", color="danger") - -dash_app.layout = dbc.Container(fluid=True, children=[ - html.H1('My car info'), - data_div -]) + threading.Timer(30, save_config, args=[my_peugeot]).start() \ No newline at end of file diff --git a/web/callback.py b/web/callback.py new file mode 100644 index 0000000..95c25a3 --- /dev/null +++ b/web/callback.py @@ -0,0 +1,148 @@ +import json +import traceback +from datetime import datetime, timezone +import dash_bootstrap_components as dbc +from dash.dependencies import Output, Input +import dash_core_components as dcc +import dash_html_components as html +from MyLogger import logger +from flask import jsonify, request, Response as FlaskResponse + +from web import figures + +from MyPSACC import MyPSACC +from web.app import app, dash_app, myp, chc, save_config + + +@dash_app.callback(Output('trips_map', 'figure'), + Output('consumption_fig', 'figure'), + Output('consumption_fig_by_speed', 'figure'), + Output('consumption', 'children'), + Output('tab_trips', 'children'), + Input('date-slider', 'value')) +def display_value(value): + min = datetime.fromtimestamp(value[0], tz=timezone.utc) + max = datetime.fromtimestamp(value[1], tz=timezone.utc) + filtered_trips = [] + for trip in trips: + if min <= trip.start_at <= max: + filtered_trips.append(trip) + figures.get_figures(filtered_trips) + consumption = "Average consumption: {:.1f} kW/100km".format(float(figures.consumption_df.mean(numeric_only=True))) + return figures.trips_map, figures.consumption_fig, figures.consumption_fig_by_speed, consumption, figures.table_fig + + +@app.route('/getvehicles') +def getvehicules(): + return jsonify(myp.getVIN()) + + +@app.route('/get_vehicleinfo/') +def get_vehicle_Info(vin): + response = app.response_class( + response=json.dumps(myp.get_vehicle_info(vin).to_dict(), default=str), + status=200, + mimetype='application/json' + ) + return response + + +@app.route('/charge_now//') +def charge_now(vin, charge): + return jsonify(myp.charge_now(vin, charge != 0)) + + +@app.route('/charge_hour') +def change_charge_hour(): + return jsonify(myp.change_charge_hour(request.form['vin'], request.form['hour'], request.form['minute'])) + + +@app.route('/wakeup/') +def wakeup(vin): + return jsonify(myp.wakeup(vin)) + + +@app.route('/preconditioning//') +def preconditioning(vin, activate): + return jsonify(myp.preconditioning(vin, activate)) + + +@app.route('/position/') +def get_position(vin): + res = myp.get_vehicle_info(vin) + longitude, latitude = res.last_position.geometry.coordinates + return jsonify( + {"longitude": longitude, "latitude": latitude, "url": f"http://maps.google.com/maps?q={latitude},{longitude}"}) + + +# Set a battery threshold and schedule an hour to stop the charge +@app.route('/charge_control') +def charge_control(): + logger.info(request) + vin = request.args['vin'] + charge_control = chc.get(vin) + if charge_control is None: + return jsonify("error: VIN not in list") + if 'hour' in request.args or 'minute' in request.args: + charge_control.set_stop_hour([int(request.args["hour"]), int(request.args["minute"])]) + if 'percentage' in request.args: + charge_control.percentage_threshold = int(request.args['percentage']) + save_config() + return jsonify(charge_control.get_dict()) + + +@app.route('/positions') +def get_recorded_position(): + return FlaskResponse(myp.get_recorded_position(), mimetype='application/json') + + +@app.after_request +def after_request(response): + header = response.headers + header['Access-Control-Allow-Origin'] = '*' + return response + + +try: + trips = MyPSACC.get_trips() + min_date = trips[0].start_at + max_date = trips[-1].start_at + min_millis = figures.unix_time_millis(min_date) + max_millis = figures.unix_time_millis(max_date) + step = (max_millis - min_millis) / 100 + figures.get_figures(trips) + data_div = html.Div([dcc.RangeSlider( + id='date-slider', + min=min_millis, + max=max_millis, + step=step, + marks=figures.get_marks_from_start_end(min_date, + max_date), + value=[min_millis, max_millis], + ), + html.Div([ + dbc.Tabs([ + dbc.Tab(label="Summary", tab_id="summary", children=[ + html.H2(id="consumption", + children=figures.info), + dcc.Graph(figure=figures.consumption_fig, id="consumption_fig"), + dcc.Graph(figure=figures.consumption_fig_by_speed, id="consumption_fig_by_speed") + ]), + dbc.Tab(label="Trips", tab_id="trips", id="tab_trips", children=[figures.table_fig]), + dbc.Tab(label="Map", tab_id="map", children=[ + dcc.Graph(figure=figures.trips_map, id="trips_map", style={"height": '90vh'})]), + ], + id="tabs", + active_tab="summary", + ), + html.Div(id="tab-content", className="p-4"), + ])]) +except: + logger.error("Failed to generate figure, there is probably not enough data yet") + logger.error(traceback.format_exc()) + data_div = dbc.Alert("No data to show", color="danger") + +dash_app.layout = dbc.Container(fluid=True, children=[ + html.H1('My car info'), + data_div +])