mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-22 01:16:14 +00:00
+3
-3
@@ -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"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
version: "2"
|
||||
|
||||
services:
|
||||
psacc:
|
||||
image: flobz/psa_car_controller:latest
|
||||
ports:
|
||||
- "5000:5000"
|
||||
volumes:
|
||||
- ./config:/config
|
||||
+8
-3
@@ -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 \
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user