Merge pull request #257 from flobz/develop

v2.5.3
This commit is contained in:
Florian BEZANNIER
2021-10-23 20:56:37 +02:00
committed by GitHub
16 changed files with 142 additions and 109 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
ARG PYTHON_DEP='python3 python3-wheel python3-typing-extensions python3-pandas python3-plotly python3-six python3-dateutil python3-brotli python3-pycryptodome libatlas3-base python3-cryptography python3-scipy androguard'
ARG PYTHON_DEP='python3 python3-wheel python3-typing-extensions python3-pandas python3-six python3-dateutil python3-brotli python3-pycryptodome libatlas3-base python3-cryptography python3-scipy androguard python3-flask'
FROM debian:buster-slim AS builder
FROM debian:bullseye-slim AS builder
ARG PYTHON_DEP
RUN BUILD_DEP='python3-pip python3-setuptools python3-dev libblas-dev liblapack-dev gfortran libatlas3-base' ; \
apt-get update && apt-get install -y --no-install-recommends $BUILD_DEP $PYTHON_DEP;
@@ -8,7 +8,7 @@ COPY . /psa_car_controller/
RUN pip3 install --system --no-cache-dir -r /psa_car_controller/requirements.txt
EXPOSE 5000
FROM debian:buster-slim
FROM debian:bullseye-slim
ARG PYTHON_DEP
WORKDIR /config
ENV PSACC_BASE_PATH=/ PSACC_PORT=5000 PSACC_OPTIONS="-c -r --web-conf" PSACC_CONFIG_DIR="/config"
+9
View File
@@ -0,0 +1,9 @@
version: "2"
services:
psacc:
image: flobz/psa_car_controller:latest
ports:
- "5000:5000"
volumes:
- ./config:/config
+8 -3
View File
@@ -1,14 +1,19 @@
# Docker installation
A containerised version of the psa_car_controller Python scripts by Flobz.
A containerised version of the psa_car_controller.
- Docker Hub: https://hub.docker.com/r/flobz/psa_car_controller
### Overview
Once the container is running, the configuration of the psa_car_controller app is near-identical to setup instructions detailed in the app's readme.
Once the container is running, the configuration of the psa_car_controller app is near-identical classic Linux/Windows installtion.
### Installation
Create the container, detached, exposing port 5000, and mapping the a new config folder on your host to /config inside the container:
Create the container, detached, exposing port 5000, and mapping config folder on your host to /config inside the container:
#### With docker-compose
```
docker-compose up -d
```
#### With Docker:
```
docker run -d -ti --name psa_car_controller1 \
--publish 5000:5000 \
+1
View File
@@ -64,6 +64,7 @@ car_models = [
CarModel("308", 0, 56, reg=r"VF3L35GG.*"),
CarModel("208", 0, 44, reg=r"VR3UPHN[SE].*"), # VR3UPHNSSM VR3UPHNEKM
CarModel("2008", 0, 44, reg=r"VR3USHNS.*"), # VR3USHNSKM
CarModel("2008 II", 0, 45, reg=r"VR3USHNK.*"), # VR3USHNKKL
CarModel("SUV 5008 II", 0, 56, reg=r"VF3MRHNS.*"), # vf3mrhnsum
CarModel("SUV 5008 II 2018", 0, 56, reg=r"VF3MRHNY.*"), # VF3MRHNYHH
CarModel("C5 Aircross Hybrid", 13.2, 43, reg=r"VR7A4DGZ.*"), # VR7A4DGZSM
+4 -2
View File
@@ -57,7 +57,7 @@ class Config(metaclass=Singleton):
def start_remote_control(self):
if self.args.remote_disable:
logger.info("mqtt disabled")
elif not self.args.web_conf or path.exists(OTP_CONFIG_NAME):
elif not self.args.web_conf or path.isfile(OTP_CONFIG_NAME):
if self.myp.mqtt_client is not None:
self.myp.mqtt_client.disconnect()
self.myp.start_mqtt()
@@ -70,9 +70,10 @@ class Config(metaclass=Singleton):
my_logger(handler_level=int(self.args.debug))
if self.args.config:
self.config_name = self.args.config
if path.exists(self.config_name):
if path.isfile(self.config_name):
self.myp = MyPSACC.load_config(name=self.config_name)
elif self.args.web_conf:
self.is_good = False
return False
else:
raise FileNotFoundError(self.config_name)
@@ -94,6 +95,7 @@ class Config(metaclass=Singleton):
logger.info(str(self.myp.get_vehicles()))
self.is_good = True
else:
self.is_good = False
logger.error("Please reconnect by going to config web page")
if self.args.refresh:
self.myp.info_refresh_rate = self.args.refresh * 60
+27
View File
@@ -0,0 +1,27 @@
import sys
import pkg_resources
from pathlib import Path
from mylogger import logger
class TestRequirements:
"""Test availability of required packages."""
def __init__(self, requirement_path):
self.requirement_path = Path(requirement_path)
def test_requirements(self):
"""Test that each required package is available."""
requirements = pkg_resources.parse_requirements(self.requirement_path.open())
missing_requirement = False
for requirement in requirements:
requirement = str(requirement)
try:
pkg_resources.require(requirement)
except (pkg_resources.VersionConflict, pkg_resources.DistributionNotFound) as ex:
logger.error("%s\nYou need to install or update %s: pip3 install -U %s", ex, requirement, requirement)
missing_requirement = True
if missing_requirement:
sys.exit(10)
+6 -5
View File
@@ -1,17 +1,17 @@
paho-mqtt>=1.5.0
dash>=1.18.0
dash>=2
dash_daq
plotly>=4
plotly>=5
cryptography>=2.6
Werkzeug>=1.0.0
flask>=1.0.4
dash_bootstrap_components>=1
pandas
oauth2_client
requests
pytz
typing
argparse
flask
dash_bootstrap_components
geojson
reverse_geocode
androguard
@@ -21,4 +21,5 @@ pycryptodomex
certifi >= 14.05.14
six >= 1.10
python_dateutil >= 2.5.3
urllib3 >= 1.15.1
urllib3 >= 1.15.1
+7
View File
@@ -1,10 +1,17 @@
#!/usr/bin/env python3
# pylint: disable=wrong-import-position
import os
import sys
from threading import Thread
from libs.requirements import TestRequirements
DIR = os.path.dirname(os.path.realpath(__file__))
if sys.version_info < (3, 6):
raise RuntimeError("This application requires Python 3.6+")
TestRequirements(DIR + "/requirements.txt").test_requirements()
import web.app
from libs.config import Config
from mylogger import logger
+6 -6
View File
@@ -2,12 +2,12 @@ from copy import deepcopy
from statistics import mean
import dash_bootstrap_components as dbc
import dash_table
from dash_core_components import Graph
from dash_table.Format import Format, Scheme, Symbol
from dash import html
from dash.dash_table import DataTable
import plotly.express as px
import plotly.graph_objects as go
import dash_html_components as html
from dash.dash_table.Format import Scheme, Symbol, Format
from dash.dcc import Graph
from libs.car import Car
from libs.elec_price import ElecPrice
@@ -69,7 +69,7 @@ def get_figures(car: Car):
style_cell_conditional.append({'if': {'column_id': 'consumption_fuel_km', }, 'display': 'None', })
if car.is_thermal():
style_cell_conditional.append({'if': {'column_id': 'consumption_km', }, 'display': 'None', })
table_fig = dash_table.DataTable(
table_fig = DataTable(
id='trips-table',
sort_action='custom',
sort_by=[{'column_id': 'id', 'direction': 'desc'}],
@@ -116,7 +116,7 @@ def get_figures(car: Car):
consumption_fig_by_speed.update_layout(xaxis_title="average Speed km/h", yaxis_title="Consumption kWh/100Km")
# battery_table
battery_table = dash_table.DataTable(
battery_table = DataTable(
id='battery-table',
sort_action='custom',
sort_by=[{'column_id': 'start_at_str', 'direction': 'desc'}],
+1 -1
View File
@@ -1,7 +1,7 @@
import dash_bootstrap_components as dbc
from dash import html
from dash._utils import create_callback_id
from dash.dependencies import Output, Input
import dash_html_components as html
from web.app import dash_app
+1 -1
View File
@@ -1,6 +1,6 @@
import dash_bootstrap_components as dbc
import dash_daq as daq
import dash_html_components as html
from dash import html
from dash.dependencies import Input
from web.app import dash_app
+4 -3
View File
@@ -1,9 +1,10 @@
import json
from logging import DEBUG
from dash import dcc
from dash._utils import create_callback_id
from dash.dependencies import Output, Input
from dash_core_components import Store
from mylogger import logger
@@ -135,5 +136,5 @@ class FigureFilter:
return False
def get_store(self):
return [Store(id='clientside-figure-store', data=self.__get_figures()),
Store(id='clientside-data-store', data=self.src)]
return [dcc.Store(id='clientside-figure-store', data=self.__get_figures()),
dcc.Store(id='clientside-data-store', data=self.src)]
+1 -1
View File
@@ -1,7 +1,7 @@
from datetime import datetime, timedelta
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash import html
from dash.development.base_component import Component
from pandas import DataFrame
from pytz import UTC
+57 -75
View File
@@ -1,4 +1,4 @@
from dash import callback_context
from dash import callback_context, html, dcc
from dash.exceptions import PreventUpdate
from flask import request
@@ -9,37 +9,32 @@ 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()
login_config_layout = dbc.Row(dbc.Col(md=12, lg=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": "Citroën", "value": "com.psa.mym.mycitroen"},
{"label": "DS", "value": "com.psa.mym.myds"},
{"label": "Vauxhall", "value": "com.psa.mym.myvauxhall"}
],
)]),
dbc.FormGroup(
[
login_config_layout = dbc.Row(dbc.Col(md=12, lg=2, className="m-3", children=[
dbc.Row(html.H2('Config')),
dbc.Row(className="ms-2", children=[
dbc.Form([
html.Div([
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": "Citroën", "value": "com.psa.mym.mycitroen"},
{"label": "DS", "value": "com.psa.mym.myds"},
{"label": "Vauxhall", "value": "com.psa.mym.myvauxhall"}
],
)]),
html.Div([
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(
[
)]),
html.Div([
dbc.Label("Password", html_for="psa-password"),
dbc.Input(
type="password",
@@ -49,11 +44,8 @@ login_config_layout = dbc.Row(dbc.Col(md=12, lg=2, children=[
dbc.FormText(
"PSA account password",
color="secondary",
),
]
),
dbc.FormGroup(
[
)]),
html.Div([
dbc.Label("Country code", html_for="countrycode"),
dbc.Input(
type="text",
@@ -63,60 +55,48 @@ login_config_layout = dbc.Row(dbc.Col(md=12, lg=2, children=[
dbc.FormText(
"Example: FR for FRANCE or GB for Great Britain...",
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",
),
)]),
dbc.Row(dbc.Button("Submit", color="primary", id="submit-form")),
dbc.Row(
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"),
]
config_otp_layout = dbc.Row(dbc.Col(className="col-md-12 col-lg-2 m-3", children=[
dbc.Row(html.H2('Config OTP')),
dbc.Form(className="ms-2", children=[
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.Label("Write the code you just received by SMS", html_for="psa-email"),
dbc.Input(type="text", id="psa-code", placeholder="Enter code"),
dbc.Label("Enter your PIN code", html_for="psa-pin"),
dbc.Input(
type="password",
id="psa-pin",
placeholder="Enter codepin",
),
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")
]
dbc.FormText(
"It's a digit password",
color="secondary",
),
html.Div(id="opt-result")
html.Div([
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.Container(
return html.H3(className="m-2", children=["Log:", dbc.Container(
fluid=True,
style={"height": "80vh",
"overflow": "auto",
@@ -125,13 +105,14 @@ def log_layout():
"white-space": "pre-line"},
children=log_text,
className="m-3 bg-light h5"),
html.Div(id="empty-div")])
html.Div(id="empty-div")])
config_layout = dbc.Tabs([
dbc.Tab([log_layout()], label="Log"),
dbc.Tab([login_config_layout], label="User config"),
dbc.Tab([config_otp_layout], label="OTP config")])
def config_layout(activeTabs="log"):
return dbc.Tabs(active_tab=activeTabs, children=[
dbc.Tab([log_layout()], label="Log", tab_id="log"),
dbc.Tab([login_config_layout], label="User config", tab_id="login"),
dbc.Tab([config_otp_layout], label="OTP config", tab_id="otp")])
@dash_app.callback(
@@ -147,6 +128,7 @@ def connectPSA(n_clicks, app_name, email, password, countrycode): # pylint: dis
try:
res = firstLaunchConfig(app_name, email, password, countrycode)
config.load_app()
config.start_remote_control()
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)
+1 -1
View File
@@ -1,5 +1,5 @@
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash import html
from mylogger import logger
from web.tools.Button import Button
+6 -8
View File
@@ -3,10 +3,9 @@ from typing import List
from urllib.parse import parse_qs, urlparse
import dash_bootstrap_components as dbc
from dash import dcc, html
from dash.dependencies import Output, Input, State
from dash.exceptions import PreventUpdate
import dash_core_components as dcc
import dash_html_components as html
from flask import jsonify, request, Response as FlaskResponse
import web.utils
@@ -20,7 +19,7 @@ from web import figures
from web.app import app, dash_app
from web.db import Database
from web.view.config_views import login_config_layout, config_otp_layout, log_layout, config_layout
from web.view.config_views import log_layout, config_layout
from web.utils import diff_dashtable, dash_date_to_datetime
# pylint: disable=invalid-name
@@ -42,8 +41,7 @@ def add_header(el):
return dbc.Row([dbc.Col(dcc.Link(html.H1('My car info'), href="/", style={"text-decoration": "none"})),
dbc.Col(dcc.Link(html.Img(src="assets/images/settings.svg", width="30veh"),
href="/config",
className="float-right"))],
className="justify-content-between"), el
className="float-end"))]), el
@dash_app.callback(Output('page-content', 'children'),
@@ -54,15 +52,15 @@ def display_page(pathname, search):
query_params = parse_qs(urlparse(search).query)
no_header = query_params.get("header", None) == ["false"]
if pathname == "/config":
page = config_layout
page = config_layout()
elif pathname == "/config_login":
page = login_config_layout
page = config_layout("login")
elif pathname == "/log":
page = log_layout()
elif not CONFIG.is_good:
page = dcc.Location(pathname=dash_app.requests_pathname_external_prefix + "config_login", id="config_redirect")
elif pathname == "/config_otp":
page = config_otp_layout
page = config_layout("otp")
elif pathname == "/control":
page = get_control_tabs(CONFIG)
else: