diff --git a/libs/config.py b/libs/config.py
index 8f0d141..a388033 100644
--- a/libs/config.py
+++ b/libs/config.py
@@ -51,6 +51,8 @@ class Config(metaclass=Singleton):
self.chc: ChargeControls
self.config_name = DEFAULT_NAME
self.is_good: bool = False
+ self.offline = self.args.offline
+ self.remote_control = not (self.args.remote_disable or self.offline)
def start_remote_control(self):
if self.args.remote_disable:
diff --git a/web/__init__.py b/web/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/web/abrp.py b/web/abrp.py
index 61d926b..b169d26 100644
--- a/web/abrp.py
+++ b/web/abrp.py
@@ -19,6 +19,12 @@ class Abrp:
self.abrp_enable_vin = set(abrp_enable_vin)
self.proxies = None
+ def enable_abrp(self, vin, enable):
+ if enable:
+ self.abrp_enable_vin.add(vin)
+ else:
+ self.abrp_enable_vin.discard(vin)
+
def call(self, car: Car, ext_temp: float = None):
try:
if self.token is None or len(self.token) == 0:
diff --git a/web/app.py b/web/app.py
index fdb3216..fdc1f46 100644
--- a/web/app.py
+++ b/web/app.py
@@ -41,13 +41,12 @@ class MyProxyFix(ProxyFix):
return super().__call__(environ, start_response)
-
def start_app(*args, **kwargs):
run(config_flask(*args, **kwargs))
def config_flask(title, base_path, debug: bool, host, port, reloader=False, # pylint: disable=too-many-arguments
- unminified=False, view="web.views"):
+ unminified=False, view="web.view.views"):
global app, dash_app, dispatcher
reload_view = app is not None
app = Flask(__name__)
@@ -68,9 +67,9 @@ 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 = DashCustom(external_stylesheets=[dbc.themes.BOOTSTRAP], external_scripts=locale_url, title=title,
- server=app, requests_pathname_prefix=requests_pathname_prefix,
- suppress_callback_exceptions=True, serve_locally=False)
- dash_app.enable_dev_tools(reloader)
+ server=app, requests_pathname_prefix=requests_pathname_prefix,
+ suppress_callback_exceptions=True, serve_locally=False)
+ dash_app.enable_dev_tools(debug)
app.wsgi_app = MyProxyFix(dash_app)
# keep this line
importlib.import_module(view)
diff --git a/web/assets/images/battery-charge.svg b/web/assets/images/battery-charge.svg
new file mode 100644
index 0000000..ca6b546
--- /dev/null
+++ b/web/assets/images/battery-charge.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/assets/images/mileage.svg b/web/assets/images/mileage.svg
new file mode 100644
index 0000000..2548148
--- /dev/null
+++ b/web/assets/images/mileage.svg
@@ -0,0 +1,71 @@
+
+
diff --git a/web/assets/images/sync.svg b/web/assets/images/sync.svg
new file mode 100644
index 0000000..d0d7154
--- /dev/null
+++ b/web/assets/images/sync.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/web/config_views.py b/web/config_views.py
index d9ac622..a052280 100644
--- a/web/config_views.py
+++ b/web/config_views.py
@@ -106,9 +106,9 @@ config_otp_layout = dbc.Row(dbc.Col(className="col-md-12 col-lg-2 ml-2", childre
"It's a digit password",
color="secondary",
),
+ dbc.Button("Submit", color="primary", id="finish-otp")
]
),
- dbc.Button("Submit", color="primary", id="finish-otp"),
html.Div(id="opt-result")
])]))
@@ -152,7 +152,7 @@ def connectPSA(n_clicks, app_name, email, password, countrycode): # pylint: dis
@dash_app.callback(
Output("sms-demand-result", "children"),
Input("ask-sms", "n_clicks"))
-def askCode(n_clicks): # pylint: disable=unused-argument
+def askCode(n_clicks): # pylint: disable=unused-argument
ctx = callback_context
if ctx.triggered:
try:
@@ -163,6 +163,7 @@ def askCode(n_clicks): # pylint: disable=unused-argument
return dbc.Alert(res, color="danger")
raise PreventUpdate()
+
@dash_app.callback(
Output("opt-result", "children"),
Input("finish-otp", "n_clicks"),
@@ -174,7 +175,9 @@ def finishOtp(n_clicks, code_pin, sms_code): # pylint: disable=unused-argument
try:
otp_session = new_otp_session(smscode=sms_code, codepin=code_pin)
config.myp.otp = otp_session
- Config().start_remote_control()
+ config.myp.refresh_remote_token(force=True)
+ config.save_config()
+ 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:
diff --git a/web/tools/Button.py b/web/tools/Button.py
new file mode 100644
index 0000000..56dfd09
--- /dev/null
+++ b/web/tools/Button.py
@@ -0,0 +1,30 @@
+import dash_bootstrap_components as dbc
+from dash._utils import create_callback_id
+from dash.dependencies import Output, MATCH, Input
+
+from web.app import dash_app
+
+RESPONSE = "-response"
+
+
+class Button:
+ def __init__(self, role, button_id, label, fct, prevent_initial_call=True): # pylint: disable=too-many-arguments
+ self.role = role
+ self.button_id = button_id
+ self.label = label
+ self.html_el = self.get_html()
+ self._fct = fct
+ self._output = Output({'role': role + RESPONSE, 'id': MATCH}, 'children')
+ callback_id = create_callback_id(self._output)
+ if callback_id not in dash_app.callback_map:
+ dash_app.callback(self._output, [Input({'role': role, 'id': MATCH}, 'id'),
+ Input({'role': role, 'id': MATCH}, 'value')],
+ prevent_initial_call=prevent_initial_call)(self.call)
+
+ def get_html(self):
+ return dbc.Button(self.label, id={'role': self.role, 'id': self.button_id}, n_clicks=0, color="light",
+ className="col")
+
+ def call(self, div_id, value): # pylint: disable=unused-argument
+ self._fct(div_id["id"])
+ return " "
diff --git a/web/tools/Switch.py b/web/tools/Switch.py
new file mode 100644
index 0000000..e45f7cc
--- /dev/null
+++ b/web/tools/Switch.py
@@ -0,0 +1,22 @@
+import dash_bootstrap_components as dbc
+import dash_daq as daq
+import dash_html_components as html
+
+from web.tools.Button import Button, RESPONSE
+
+
+class Switch(Button):
+ def __init__(self, role, button_id, label, fct, value): # pylint: disable=too-many-arguments
+ self.value = value
+ super().__init__(role, button_id, label, fct)
+
+ def get_html(self):
+ return dbc.Col([daq.ToggleSwitch( # pylint: disable=not-callable
+ id={'role': self.role, 'id': self.button_id},
+ value=self.value,
+ label=self.label
+ ), html.Div(id={'role': self.role + RESPONSE, 'id': self.button_id})])
+
+ def call(self, div_id, value):
+ self._fct(div_id["id"], value)
+ return " "
\ No newline at end of file
diff --git a/web/figurefilter.py b/web/tools/figurefilter.py
similarity index 92%
rename from web/figurefilter.py
rename to web/tools/figurefilter.py
index 319741c..722d9f6 100644
--- a/web/figurefilter.py
+++ b/web/tools/figurefilter.py
@@ -51,10 +51,13 @@ class FigureFilter:
return dash_Graph
def add_table(self, src, figure):
- table = Table(figure.id, src, figure)
- table.date_columns = [col["id"][:-4] for col in figure.columns if col["type"] == "datetime" and
- col["id"].endswith("_str")]
- self.tables.append(table)
+ try:
+ table = Table(figure.id, src, figure)
+ table.date_columns = [col["id"][:-4] for col in figure.columns if col["type"] == "datetime" and
+ col["id"].endswith("_str")]
+ self.tables.append(table)
+ except AttributeError:
+ logger.debug("figure isn't a table")
def __get_table_date_column_id(self):
res = {table.src: table.date_columns for table in self.tables}
diff --git a/web/view/control.py b/web/view/control.py
new file mode 100644
index 0000000..3e59568
--- /dev/null
+++ b/web/view/control.py
@@ -0,0 +1,48 @@
+import dash_bootstrap_components as dbc
+import dash_html_components as html
+
+from mylogger import logger
+from web.tools.Button import Button
+from web.tools.Switch import Switch
+from web.utils import card_value_div, create_card
+
+REFRESH_SWITCH = "refresh-switch"
+ABRP_SWITCH = 'abrp-switch'
+CHARGE_SWITCH = "charge-switch"
+PRECONDITIONING_SWITCH = "preconditioning-switch"
+
+
+def get_control_tabs(config):
+ tabs = []
+ for car in config.myp.vehicles_list:
+ if car.label is None:
+ label = car.vin
+ else:
+ label = car.label
+ myp = config.myp
+ el = []
+ buttons_row = []
+ if config.remote_control:
+ try:
+ preconditionning_state = car.status.preconditionning.air_conditioning.status != "Disabled"
+ charging_state = car.status.get_energy('Electric').charging.status == "InProgress"
+ cards = {"Battery": {"text": [card_value_div("battery_value", "%",
+ value=str(int(car.status.get_energy('Electric').level)))],
+ "src": "assets/images/battery-charge.svg"},
+ "Mileage": {"text": [card_value_div("mileage_value", "km",
+ value=str(int(car.status.timed_odometer.mileage)))],
+ "src": "assets/images/mileage.svg"}
+ }
+ el.append(dbc.Container(dbc.Row(children=create_card(cards)), fluid=True))
+ buttons_row.extend([Button(REFRESH_SWITCH, car.vin,
+ html.Img(src="assets/images/sync.svg", width="50px"), myp.wakeup).get_html(),
+ Switch(CHARGE_SWITCH, car.vin, "Charge", myp.charge_now, charging_state).get_html(),
+ Switch(PRECONDITIONING_SWITCH, car.vin, "Preconditioning",
+ myp.preconditioning, preconditionning_state).get_html()])
+ except (AttributeError, TypeError):
+ logger.exception("get_control_tabs:")
+ if not config.offline:
+ buttons_row.append(Switch(ABRP_SWITCH, car.vin, "Send data to ABRP", myp.abrp.enable_abrp,
+ car.vin in config.myp.abrp.abrp_enable_vin).get_html())
+ tabs.append(dbc.Tab(label=label, id="tab-" + car.vin, children=[dbc.Row(buttons_row), *el]))
+ return dbc.Tabs(id="control-tabs", children=tabs)
diff --git a/web/views.py b/web/view/views.py
similarity index 88%
rename from web/views.py
rename to web/view/views.py
index 27f3ef2..f9800e3 100644
--- a/web/views.py
+++ b/web/view/views.py
@@ -1,12 +1,12 @@
import json
from typing import List
+from urllib.parse import parse_qs, urlparse
import dash_bootstrap_components as dbc
-from dash.dependencies import Output, Input, MATCH, State
+from dash.dependencies import Output, Input, State
from dash.exceptions import PreventUpdate
import dash_core_components as dcc
import dash_html_components as html
-import dash_daq as daq
from flask import jsonify, request, Response as FlaskResponse
import web.utils
@@ -24,13 +24,12 @@ 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.tools.figurefilter import FigureFilter
from web.utils import create_card
from libs.config import Config
+from web.view.control import get_control_tabs
-RESPONSE = "-response"
EMPTY_DIV = "empty-div"
-ABRP_SWITCH = 'abrp-switch'
CALLBACK_CREATED = False
trips: Trips = Trips()
@@ -39,19 +38,32 @@ min_date = max_date = min_millis = max_millis = step = marks = cached_layout = N
CONFIG = Config()
+def add_header(el):
+ return html.H1('My car info'), el
+
+
@dash_app.callback(Output('page-content', 'children'),
- [Input('url', 'pathname')])
-def display_page(pathname):
- pathname = pathname[len(dash_app.requests_pathname_external_prefix)-1:]
+ [Input('url', 'pathname'),
+ Input('url', 'search')])
+def display_page(pathname, search):
+ pathname = pathname[len(dash_app.requests_pathname_external_prefix) - 1:]
+ query_params = parse_qs(urlparse(search).query)
+ no_header = query_params.get("header", None) == ["false"]
if pathname == "/config":
- return config_layout
- if pathname == "/log":
- return log_layout()
- if not CONFIG.is_good:
- return dcc.Location(pathname=dash_app.requests_pathname_external_prefix + "config", id="config_redirect")
- if pathname == "/config_otp":
- return config_otp_layout
- return serve_layout()
+ page = config_layout
+ elif pathname == "/log":
+ page = log_layout()
+ elif not CONFIG.is_good:
+ page = dcc.Location(pathname=dash_app.requests_pathname_external_prefix + "config", id="config_redirect")
+ elif pathname == "/config_otp":
+ page = config_otp_layout
+ elif pathname == "/control":
+ page = get_control_tabs(CONFIG)
+ else:
+ page = serve_layout()
+ if no_header:
+ return page
+ return add_header(page)
def create_callback(): # noqa: MC0001
@@ -103,19 +115,6 @@ def create_callback(): # noqa: MC0001
CALLBACK_CREATED = True
-@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'))
-def update_abrp(div_id, value):
- vin = div_id["vin"]
- if value:
- CONFIG.myp.abrp.abrp_enable_vin.add(vin)
- else:
- CONFIG.myp.abrp.abrp_enable_vin.discard(vin)
- CONFIG.myp.save_config()
- return " "
-
-
@app.route('/get_vehicles')
def get_vehicules():
response = app.response_class(
@@ -281,25 +280,6 @@ def update_trips():
return
-def __get_control_tabs():
- tabs = []
- for car in CONFIG.myp.vehicles_list:
- if car.label is None:
- label = car.vin
- else:
- label = car.label
- # pylint: disable=not-callable
- tabs.append(dbc.Tab(label=label, id="tab-" + car.vin, children=[
- daq.ToggleSwitch(
- id={'role': ABRP_SWITCH, 'vin': car.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})
- ]))
- return tabs
-
-
def serve_layout():
global cached_layout
if cached_layout is None:
@@ -379,13 +359,16 @@ def serve_layout():
)
]),
dbc.Tab(label="Map", tab_id="map", children=[maps]),
- dbc.Tab(label="Control", tab_id="control", children=dbc.Tabs(id="control-tabs",
- children=__get_control_tabs()))],
+ dbc.Tab(label="Control", tab_id="control", children=html.Iframe(src="/control?header=false",
+ style={"position": "absolute",
+ "height": "100%",
+ "width": "100%",
+ "border": "none"}))
+ ],
id="tabs",
active_tab="summary",
persistence=True),
- html.Div(id=EMPTY_DIV),
- html.Div(id=EMPTY_DIV + "1")
+ html.Div(id=EMPTY_DIV)
])])
cached_layout = data_div
return cached_layout
@@ -399,4 +382,5 @@ except (IndexError, TypeError):
logger.debug("Failed to get trips, there is probably not enough data yet:", exc_info=True)
dash_app.layout = dbc.Container(fluid=True, children=[dcc.Location(id='url', refresh=False),
- html.H1('My car info'), html.Div(id='page-content')])
+ html.Div(id='page-content')],
+ style={"height": "100vh"})