mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-22 09:26:16 +00:00
add charge and Preconditioning control and more info in control tab
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 " "
|
||||
@@ -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}
|
||||
@@ -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)
|
||||
@@ -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"})
|
||||
Reference in New Issue
Block a user