mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-26 10:17:18 +00:00
webui to connect to PSA API
This commit is contained in:
+6
-17
@@ -1,4 +1,3 @@
|
||||
import threading
|
||||
import locale
|
||||
|
||||
import dash
|
||||
@@ -13,18 +12,13 @@ try:
|
||||
except ImportError:
|
||||
from werkzeug import DispatcherMiddleware
|
||||
|
||||
from charge_control import ChargeControls
|
||||
from mylogger import logger
|
||||
from my_psacc import MyPSACC
|
||||
import importlib
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
app = None
|
||||
dash_app = None
|
||||
dispatcher = None
|
||||
# noinspection PyTypeChecker
|
||||
myp: MyPSACC = None
|
||||
# noinspection PyTypeChecker
|
||||
chc: ChargeControls = None
|
||||
|
||||
|
||||
def start_app(*args, **kwargs):
|
||||
@@ -32,7 +26,7 @@ def start_app(*args, **kwargs):
|
||||
|
||||
|
||||
def config_flask(title, base_path, debug: bool, host, port, reloader=False, # pylint: disable=too-many-arguments
|
||||
unminified=False):
|
||||
unminified=False, view="web.views"):
|
||||
global app, dash_app, dispatcher
|
||||
reload_view = app is not None
|
||||
app = Flask(__name__)
|
||||
@@ -54,20 +48,15 @@ def config_flask(title, base_path, debug: bool, host, port, reloader=False, # p
|
||||
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)
|
||||
server=app, requests_pathname_prefix=requests_pathname_prefix,
|
||||
suppress_callback_exceptions=True)
|
||||
dash_app.enable_dev_tools(reloader)
|
||||
# keep this line
|
||||
import web.views # pylint: disable=import-outside-toplevel
|
||||
importlib.import_module(view)
|
||||
if reload_view:
|
||||
import importlib # pylint: disable=import-outside-toplevel
|
||||
importlib.reload(web.views)
|
||||
importlib.reload(view)
|
||||
return {"hostname": host, "port": port, "application": application, "use_reloader": reloader, "use_debugger": debug}
|
||||
|
||||
|
||||
def run(config):
|
||||
return run_simple(**config)
|
||||
|
||||
|
||||
def save_config(my_peugeot: MyPSACC, name):
|
||||
my_peugeot.save_config(name)
|
||||
threading.Timer(30, save_config, args=[my_peugeot, name]).start()
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
from dash import callback_context
|
||||
from dash.exceptions import PreventUpdate
|
||||
from flask import request
|
||||
|
||||
from app_decoder import firstLaunchConfig
|
||||
from libs.config import Config
|
||||
from mylogger import LOG_FILE
|
||||
from otp.otp import new_otp_session
|
||||
from web.app import dash_app
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash.dependencies import Output, Input, State
|
||||
import dash_core_components as dcc
|
||||
import dash_html_components as html
|
||||
|
||||
config = Config()
|
||||
config_layout = dbc.Row(dbc.Col(className="col-md-12 col-lg-2 ml-2", children=[
|
||||
html.H2('Config'),
|
||||
dbc.Form([
|
||||
dbc.FormGroup([
|
||||
dbc.Label("Car Brand", html_for="psa-app"),
|
||||
dcc.Dropdown(
|
||||
id="psa-app",
|
||||
options=[
|
||||
{"label": "Peugeot", "value": "com.psa.mym.mypeugeot"},
|
||||
{"label": "Opel", "value": "com.psa.mym.myopel"},
|
||||
{"label": "Cirtroën", "value": "com.psa.mym.citroen"},
|
||||
{"label": "DS", "value": "com.psa.mym.myds"},
|
||||
{"label": "Vauxhall", "value": "com.psa.mym.myvauxhll"}
|
||||
],
|
||||
)]),
|
||||
dbc.FormGroup(
|
||||
[
|
||||
dbc.Label("Email", html_for="psa-email"),
|
||||
dbc.Input(type="email", id="psa-email", placeholder="Enter email"),
|
||||
dbc.FormText(
|
||||
"PSA account email",
|
||||
color="secondary",
|
||||
),
|
||||
]
|
||||
),
|
||||
dbc.FormGroup(
|
||||
[
|
||||
dbc.Label("Password", html_for="psa-password"),
|
||||
dbc.Input(
|
||||
type="password",
|
||||
id="psa-password",
|
||||
placeholder="Enter password",
|
||||
),
|
||||
dbc.FormText(
|
||||
"PSA account password",
|
||||
color="secondary",
|
||||
),
|
||||
]
|
||||
),
|
||||
dbc.FormGroup(
|
||||
[
|
||||
dbc.Label("Country code", html_for="countrycode"),
|
||||
dbc.Input(
|
||||
type="text",
|
||||
id="psa-countrycode",
|
||||
placeholder="Enter your country code",
|
||||
),
|
||||
dbc.FormText(
|
||||
"Example: FR for FRANCE or EN for England",
|
||||
color="secondary",
|
||||
)
|
||||
]
|
||||
),
|
||||
dbc.FormGroup([
|
||||
dbc.Button("Submit", color="primary", id="submit-form"),
|
||||
dbc.FormText(
|
||||
"After submit be patient it can take some time...",
|
||||
color="secondary",
|
||||
),
|
||||
dcc.Loading(
|
||||
id="loading-2",
|
||||
children=[html.Div([html.Div(id="form_result")])],
|
||||
type="circle",
|
||||
)]
|
||||
),
|
||||
])]))
|
||||
|
||||
config_otp_layout = dbc.Row(dbc.Col(className="col-md-12 col-lg-2 ml-2", children=[
|
||||
html.H2('Config OTP'),
|
||||
dbc.Form([
|
||||
dbc.FormGroup([
|
||||
dbc.Label("Click to receive a code by SMS", html_for="ask-sms"),
|
||||
dbc.Button("Send SMS", color="info", id="ask-sms"),
|
||||
html.Div(id="sms-demand-result", className="mt-2")
|
||||
]),
|
||||
dbc.FormGroup(
|
||||
[
|
||||
dbc.Label("Write the code you just received by SMS", html_for="psa-email"),
|
||||
dbc.Input(type="text", id="psa-code", placeholder="Enter code"),
|
||||
]
|
||||
),
|
||||
dbc.FormGroup(
|
||||
[
|
||||
dbc.Label("Enter your code PIN", html_for="psa-pin"),
|
||||
dbc.Input(
|
||||
type="password",
|
||||
id="psa-pin",
|
||||
placeholder="Enter codepin",
|
||||
),
|
||||
dbc.FormText(
|
||||
"It's a digit password",
|
||||
color="secondary",
|
||||
),
|
||||
]
|
||||
),
|
||||
dbc.Button("Submit", color="primary", id="finish-otp"),
|
||||
html.Div(id="opt-result")
|
||||
])]))
|
||||
|
||||
|
||||
def log_layout():
|
||||
with open(LOG_FILE, "r") as f:
|
||||
log_text = f.read()
|
||||
return html.H3(children=["Log:", dbc.Textarea(
|
||||
valid=True,
|
||||
bs_size="sm",
|
||||
className="mt-3",
|
||||
style={"height": "80vh"},
|
||||
placeholder="Log",
|
||||
contentEditable=False,
|
||||
value=log_text
|
||||
)])
|
||||
|
||||
|
||||
@dash_app.callback(
|
||||
Output("form_result", "children"),
|
||||
Input("submit-form", "n_clicks"),
|
||||
State("psa-app", "value"),
|
||||
State("psa-email", "value"),
|
||||
State("psa-password", "value"),
|
||||
State("psa-countrycode", "value"))
|
||||
def connectPSA(n_clicks, app_name, email, password, countrycode): # pylint: disable=unused-argument
|
||||
ctx = callback_context
|
||||
if ctx.triggered:
|
||||
try:
|
||||
res = firstLaunchConfig(app_name, email, password, countrycode)
|
||||
config.load_app()
|
||||
return dbc.Alert([res, html.A(" Go to otp config", href=request.url_root + "config_otp")], color="success")
|
||||
except Exception as e:
|
||||
res = str(e)
|
||||
return dbc.Alert(res, color="danger")
|
||||
else:
|
||||
return ""
|
||||
raise PreventUpdate()
|
||||
|
||||
|
||||
@dash_app.callback(
|
||||
Output("sms-demand-result", "children"),
|
||||
Input("ask-sms", "n_clicks"))
|
||||
def askCode(n_clicks): # pylint: disable=unused-argument
|
||||
ctx = callback_context
|
||||
if ctx.triggered:
|
||||
try:
|
||||
config.myp.get_sms_otp_code()
|
||||
return dbc.Alert("Sms sent", color="success")
|
||||
except Exception as e:
|
||||
res = str(e)
|
||||
return dbc.Alert(res, color="danger")
|
||||
raise PreventUpdate()
|
||||
|
||||
@dash_app.callback(
|
||||
Output("opt-result", "children"),
|
||||
Input("finish-otp", "n_clicks"),
|
||||
State("psa-pin", "value"),
|
||||
State("psa-code", "value"))
|
||||
def finishOtp(n_clicks, code_pin, sms_code): # pylint: disable=unused-argument
|
||||
ctx = callback_context
|
||||
if ctx.triggered:
|
||||
try:
|
||||
otp_session = new_otp_session(smscode=sms_code, codepin=code_pin)
|
||||
config.myp.otp = otp_session
|
||||
Config().start_remote_control()
|
||||
return dbc.Alert(["OTP config finish !!! ", html.A("Go to home", href=request.url_root)],
|
||||
color="success")
|
||||
except Exception as e:
|
||||
res = str(e)
|
||||
return dbc.Alert(res, color="danger")
|
||||
raise PreventUpdate()
|
||||
+80
-61
@@ -18,13 +18,15 @@ from trip import Trips
|
||||
from libs.charging import Charging
|
||||
from web import figures
|
||||
|
||||
from web.app import app, dash_app, myp, chc
|
||||
from web.app import app, dash_app
|
||||
from web.db import Database
|
||||
from web.config_views import config_layout, config_otp_layout, log_layout
|
||||
from web.utils import diff_dashtable, dash_date_to_datetime
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
from web.figurefilter import FigureFilter
|
||||
from web.utils import create_card
|
||||
from libs.config import Config
|
||||
|
||||
RESPONSE = "-response"
|
||||
EMPTY_DIV = "empty-div"
|
||||
@@ -34,6 +36,21 @@ CALLBACK_CREATED = False
|
||||
trips: Trips = Trips()
|
||||
chargings: List[dict]
|
||||
min_date = max_date = min_millis = max_millis = step = marks = cached_layout = None
|
||||
CONFIG = Config()
|
||||
|
||||
|
||||
@dash_app.callback(Output('page-content', 'children'),
|
||||
[Input('url', 'pathname')])
|
||||
def display_page(pathname):
|
||||
if pathname == "/config":
|
||||
return config_layout
|
||||
if pathname == "/log":
|
||||
return log_layout()
|
||||
if not CONFIG.is_good:
|
||||
return dcc.Location(pathname="/config", id="config_redirect")
|
||||
if pathname == "/config_otp":
|
||||
return config_otp_layout
|
||||
return serve_layout()
|
||||
|
||||
|
||||
def create_callback(): # noqa: MC0001
|
||||
@@ -68,7 +85,7 @@ def create_callback(): # noqa: MC0001
|
||||
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"]]
|
||||
return figures.get_battery_curve_fig(row, myp.vehicles_list[0]), True
|
||||
return figures.get_battery_curve_fig(row, CONFIG.myp.vehicles_list[0]), True
|
||||
return "", False
|
||||
|
||||
@dash_app.callback([Output("tab_trips_popup_graph", "children"), Output("tab_trips_popup", "is_open"), ],
|
||||
@@ -91,17 +108,17 @@ def create_callback(): # noqa: MC0001
|
||||
def update_abrp(div_id, value):
|
||||
vin = div_id["vin"]
|
||||
if value:
|
||||
myp.abrp.abrp_enable_vin.add(vin)
|
||||
CONFIG.myp.abrp.abrp_enable_vin.add(vin)
|
||||
else:
|
||||
myp.abrp.abrp_enable_vin.discard(vin)
|
||||
myp.save_config()
|
||||
CONFIG.myp.abrp.abrp_enable_vin.discard(vin)
|
||||
CONFIG.myp.save_config()
|
||||
return " "
|
||||
|
||||
|
||||
@app.route('/get_vehicles')
|
||||
def get_vehicules():
|
||||
response = app.response_class(
|
||||
response=json.dumps(myp.get_vehicles(), default=lambda car: car.to_dict()),
|
||||
response=json.dumps(CONFIG.myp.get_vehicles(), default=lambda car: car.to_dict()),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
@@ -112,7 +129,7 @@ def get_vehicules():
|
||||
def get_vehicle_info(vin):
|
||||
from_cache = int(request.args.get('from_cache', 0)) == 1
|
||||
response = app.response_class(
|
||||
response=json.dumps(myp.get_vehicle_info(vin, from_cache).to_dict(), default=str),
|
||||
response=json.dumps(CONFIG.myp.get_vehicle_info(vin, from_cache).to_dict(), default=str),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
@@ -136,27 +153,27 @@ def get_style():
|
||||
|
||||
@app.route('/charge_now/<string:vin>/<int:charge>')
|
||||
def charge_now(vin, charge):
|
||||
return jsonify(myp.charge_now(vin, charge != 0))
|
||||
return jsonify(CONFIG.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']))
|
||||
return jsonify(CONFIG.myp.change_charge_hour(request.form['vin'], request.form['hour'], request.form['minute']))
|
||||
|
||||
|
||||
@app.route('/wakeup/<string:vin>')
|
||||
def wakeup(vin):
|
||||
return jsonify(myp.wakeup(vin))
|
||||
return jsonify(CONFIG.myp.wakeup(vin))
|
||||
|
||||
|
||||
@app.route('/preconditioning/<string:vin>/<int:activate>')
|
||||
def preconditioning(vin, activate):
|
||||
return jsonify(myp.preconditioning(vin, activate))
|
||||
return jsonify(CONFIG.myp.preconditioning(vin, activate))
|
||||
|
||||
|
||||
@app.route('/position/<string:vin>')
|
||||
def get_position(vin):
|
||||
res = myp.get_vehicle_info(vin)
|
||||
res = CONFIG.myp.get_vehicle_info(vin)
|
||||
try:
|
||||
coordinates = res.last_position.geometry.coordinates
|
||||
except AttributeError:
|
||||
@@ -176,14 +193,14 @@ def get_position(vin):
|
||||
def get_charge_control():
|
||||
logger.info(request)
|
||||
vin = request.args['vin']
|
||||
charge_control = chc.get(vin)
|
||||
charge_control = CONFIG.chc.get(vin)
|
||||
if charge_control is None:
|
||||
return jsonify("error: VIN not in list")
|
||||
if 'hour' in request.args and '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'])
|
||||
chc.save_config()
|
||||
CONFIG.chc.save_config()
|
||||
return jsonify(charge_control.get_dict())
|
||||
|
||||
|
||||
@@ -199,12 +216,12 @@ def abrp():
|
||||
token = request.args.get('token', None)
|
||||
if vin is not None and enable is not None:
|
||||
if enable == '1':
|
||||
myp.abrp.abrp_enable_vin.add(vin)
|
||||
CONFIG.myp.abrp.abrp_enable_vin.add(vin)
|
||||
else:
|
||||
myp.abrp.abrp_enable_vin.discard(vin)
|
||||
CONFIG.myp.abrp.abrp_enable_vin.discard(vin)
|
||||
if token is not None:
|
||||
myp.abrp.token = token
|
||||
return jsonify(dict(myp.abrp))
|
||||
CONFIG.myp.abrp.token = token
|
||||
return jsonify(dict(CONFIG.myp.abrp))
|
||||
|
||||
|
||||
@app.after_request
|
||||
@@ -222,49 +239,50 @@ def update_trips():
|
||||
conn.close()
|
||||
min_date = None
|
||||
max_date = None
|
||||
car = myp.vehicles_list[0] # todo handle multiple car
|
||||
try:
|
||||
trips_by_vin = Trips.get_trips(Cars([car]))
|
||||
trips = trips_by_vin[car.vin]
|
||||
assert len(trips) > 0
|
||||
min_date = trips[0].start_at
|
||||
max_date = trips[-1].start_at
|
||||
figures.get_figures(trips[0].car)
|
||||
except (AssertionError, KeyError):
|
||||
logger.debug("No trips yet")
|
||||
figures.get_figures(Car("vin","vid","brand"))
|
||||
try:
|
||||
chargings = Charging.get_chargings()
|
||||
assert len(chargings) > 0
|
||||
if min_date:
|
||||
min_date = min(min_date, chargings[0]["start_at"])
|
||||
max_date = max(max_date, chargings[-1]["start_at"])
|
||||
else:
|
||||
min_date = chargings[0]["start_at"]
|
||||
max_date = chargings[-1]["start_at"]
|
||||
except AssertionError:
|
||||
logger.debug("No chargings yet")
|
||||
if min_date is None:
|
||||
return
|
||||
# update for slider
|
||||
try:
|
||||
logger.debug("min_date:%s - max_date:%s", min_date, max_date)
|
||||
min_millis = web.utils.unix_time_millis(min_date)
|
||||
max_millis = web.utils.unix_time_millis(max_date)
|
||||
step = (max_millis - min_millis) / 100
|
||||
marks = web.utils.get_marks_from_start_end(min_date, max_date)
|
||||
cached_layout = None # force regenerate layout
|
||||
figures.get_figures(car)
|
||||
except (ValueError, IndexError):
|
||||
logger.error("update_trips (slider): %s", exc_info=True)
|
||||
except AttributeError:
|
||||
logger.debug("position table is probably empty :", exc_info=True)
|
||||
if CONFIG.is_good:
|
||||
car = CONFIG.myp.vehicles_list[0] # todo handle multiple car
|
||||
try:
|
||||
trips_by_vin = Trips.get_trips(Cars([car]))
|
||||
trips = trips_by_vin[car.vin]
|
||||
assert len(trips) > 0
|
||||
min_date = trips[0].start_at
|
||||
max_date = trips[-1].start_at
|
||||
figures.get_figures(trips[0].car)
|
||||
except (AssertionError, KeyError):
|
||||
logger.debug("No trips yet")
|
||||
figures.get_figures(Car("vin", "vid", "brand"))
|
||||
try:
|
||||
chargings = Charging.get_chargings()
|
||||
assert len(chargings) > 0
|
||||
if min_date:
|
||||
min_date = min(min_date, chargings[0]["start_at"])
|
||||
max_date = max(max_date, chargings[-1]["start_at"])
|
||||
else:
|
||||
min_date = chargings[0]["start_at"]
|
||||
max_date = chargings[-1]["start_at"]
|
||||
except AssertionError:
|
||||
logger.debug("No chargings yet")
|
||||
if min_date is None:
|
||||
return
|
||||
# update for slider
|
||||
try:
|
||||
logger.debug("min_date:%s - max_date:%s", min_date, max_date)
|
||||
min_millis = web.utils.unix_time_millis(min_date)
|
||||
max_millis = web.utils.unix_time_millis(max_date)
|
||||
step = (max_millis - min_millis) / 100
|
||||
marks = web.utils.get_marks_from_start_end(min_date, max_date)
|
||||
cached_layout = None # force regenerate layout
|
||||
figures.get_figures(car)
|
||||
except (ValueError, IndexError):
|
||||
logger.error("update_trips (slider): %s", exc_info=True)
|
||||
except AttributeError:
|
||||
logger.debug("position table is probably empty :", exc_info=True)
|
||||
return
|
||||
|
||||
|
||||
def __get_control_tabs():
|
||||
tabs = []
|
||||
for car in myp.vehicles_list:
|
||||
for car in CONFIG.myp.vehicles_list:
|
||||
if car.label is None:
|
||||
label = car.vin
|
||||
else:
|
||||
@@ -273,7 +291,7 @@ def __get_control_tabs():
|
||||
tabs.append(dbc.Tab(label=label, id="tab-" + car.vin, children=[
|
||||
daq.ToggleSwitch(
|
||||
id={'role': ABRP_SWITCH, 'vin': car.vin},
|
||||
value=car.vin in myp.abrp.abrp_enable_vin,
|
||||
value=car.vin in CONFIG.myp.abrp.abrp_enable_vin,
|
||||
label="Send data to ABRP"
|
||||
),
|
||||
html.Div(id={'role': ABRP_SWITCH + RESPONSE, 'vin': car.vin})
|
||||
@@ -311,7 +329,7 @@ def serve_layout():
|
||||
fig_filter.src = {"trips": trips.get_trips_as_dict(), "chargings": chargings}
|
||||
fig_filter.set_clientside_callback(dash_app)
|
||||
create_callback()
|
||||
except (IndexError, TypeError, NameError, AssertionError, NameError):
|
||||
except (IndexError, TypeError, NameError, AssertionError, NameError, AttributeError):
|
||||
summary_tab = figures.ERROR_DIV
|
||||
maps = figures.ERROR_DIV
|
||||
logger.warning("Failed to generate figure, there is probably not enough data yet", exc_info_debug=True)
|
||||
@@ -320,8 +338,8 @@ def serve_layout():
|
||||
|
||||
data_div = html.Div([
|
||||
*fig_filter.get_store(),
|
||||
range_slider,
|
||||
html.Div([
|
||||
range_slider,
|
||||
dbc.Tabs([
|
||||
dbc.Tab(label="Summary", tab_id="summary", children=summary_tab),
|
||||
dbc.Tab(label="Trips", tab_id="trips", id="tab_trips",
|
||||
@@ -368,7 +386,7 @@ def serve_layout():
|
||||
html.Div(id=EMPTY_DIV),
|
||||
html.Div(id=EMPTY_DIV + "1")
|
||||
])])
|
||||
cached_layout = dbc.Container(fluid=True, children=[html.H1('My car info'), data_div])
|
||||
cached_layout = data_div
|
||||
return cached_layout
|
||||
|
||||
|
||||
@@ -379,4 +397,5 @@ try:
|
||||
except (IndexError, TypeError):
|
||||
logger.debug("Failed to get trips, there is probably not enough data yet:", exc_info=True)
|
||||
|
||||
dash_app.layout = serve_layout
|
||||
dash_app.layout = dbc.Container(fluid=True, children=[dcc.Location(id='url', refresh=False),
|
||||
html.H1('My car info'), html.Div(id='page-content')])
|
||||
|
||||
Reference in New Issue
Block a user