add charge and Preconditioning control and more info in control tab

This commit is contained in:
Florian Bezannier
2021-09-16 20:41:47 +02:00
parent 6c83b3fb00
commit 7eeb3c0b64
13 changed files with 238 additions and 65 deletions
+2
View File
@@ -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:
View File
+6
View File
@@ -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:
+4 -5
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13 12h3l-5 7v-5H8l5-7v5zm-2-6H7v14h10V6h-4V4h-2v2zM9 4V3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1h3a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h3z"/></svg>

After

Width:  |  Height:  |  Size: 295 B

+71
View File
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
data-name="Layer 1"
viewBox="0 0 100 125"
x="0px"
y="0px"
version="1.1"
id="svg16"
sodipodi:docname="mileage.svg"
inkscape:version="1.1 (c4e8f9ed74, 2021-05-24)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs20" />
<sodipodi:namedview
id="namedview18"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="7.024"
inkscape:cx="50.042711"
inkscape:cy="48.903759"
inkscape:window-width="1890"
inkscape:window-height="1051"
inkscape:window-x="30"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg16" />
<title
id="title2">Artboard 84</title>
<rect
x="63.57"
y="63.57"
width="13.57"
height="6.79"
id="rect4" />
<rect
x="63.57"
y="29.64"
width="13.57"
height="6.79"
id="rect6" />
<rect
x="29.64"
y="36.43"
width="13.57"
height="20.36"
id="rect8" />
<path
d="M83.93,9.29H16.07A13.56,13.56,0,0,0,2.5,22.86V77.14A13.56,13.56,0,0,0,16.07,90.71H83.93A13.57,13.57,0,0,0,97.5,77.14V22.86A13.57,13.57,0,0,0,83.93,9.29ZM16.07,63.57H9.29V29.64h6.78Zm33.93,0H22.86V29.64H50Zm33.93,0V83.93H77.14V77.14H56.79V56.79H83.93Zm0-40.71V43.21H56.79V16.07h6.78v6.79H77.14V16.07h6.79Z"
id="path10"
clip-path="none" />
<metadata
id="metadata278">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:title>Artboard 84</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-repeat" viewBox="0 0 16 16">
<path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/>
<path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 582 B

+6 -3
View File
@@ -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:
+30
View File
@@ -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 " "
+22
View File
@@ -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 " "
@@ -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}
+48
View File
@@ -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)
+37 -53
View File
@@ -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"})