Merge branch 'master' into patch-1

This commit is contained in:
Florian BEZANNIER
2024-02-26 14:22:28 +01:00
committed by GitHub
14 changed files with 299 additions and 137 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ The official API is documented [here](https://developer.groupe-psa.io/webapi/b2c
- [Installation on Raspberry Pi with docker-compose (external Tutorial)](https://return2.net/opel-peugeot-electric-vehicle-set-charging-threshold-limit/)
## II. Use the API
Look at [API documentation](./docs/psacc_api)
Look at [API documentation](./docs/psacc_api.md)
## III. Use the dashboard
+19 -6
View File
@@ -3,12 +3,20 @@ IMMEDIATE_CHARGE = "immediate"
PSA_CORRELATION_DATE_FORMAT = "%Y%m%d%H%M%S%f"
PSA_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
realm_info = {
"clientsB2CPeugeot": {"oauth_url": "https://idpcvs.peugeot.com/am/oauth2/access_token", "app_name": "MyPeugeot"},
"clientsB2CCitroen": {"oauth_url": "https://idpcvs.citroen.com/am/oauth2/access_token", "app_name": "MyCitroen"},
"clientsB2CDS": {"oauth_url": "https://idpcvs.driveds.com/am/oauth2/access_token", "app_name": "MyDS"},
"clientsB2COpel": {"oauth_url": "https://idpcvs.opel.com/am/oauth2/access_token", "app_name": "MyOpel"},
"clientsB2CPeugeot": {"oauth_url": "https://idpcvs.peugeot.com/am/oauth2/access_token", "app_name": "MyPeugeot",
"scheme": "mymap"},
"clientsB2CCitroen": {"oauth_url": "https://idpcvs.citroen.com/am/oauth2/access_token", "app_name": "MyCitroen",
"scheme": "mymacsdk"},
"clientsB2CDS": {"oauth_url": "https://idpcvs.driveds.com/am/oauth2/access_token", "app_name": "MyDS",
"scheme": "mymdssdk"},
"clientsB2COpel": {"oauth_url": "https://idpcvs.opel.com/am/oauth2/access_token", "app_name": "MyOpel",
"scheme": "mymopsdk"},
"clientsB2CVauxhall": {"oauth_url": "https://idpcvs.vauxhall.co.uk/am/oauth2/access_token",
"app_name": "MyVauxhall"}
"app_name": "MyVauxhall",
"scheme": "mymvxsdk",
}
}
MQTT_BRANDCODE = {"AP": "AP",
"AC": "AC",
@@ -28,7 +36,12 @@ DEFAULT_PRECONDITIONING_PROGRAM = {
"program3": {"day": [0, 0, 0, 0, 0, 0, 0], "hour": 34, "minute": 7, "on": 0},
"program4": {"day": [0, 0, 0, 0, 0, 0, 0], "hour": 34, "minute": 7, "on": 0}
}
AUTHORIZE_SERVICE = "https://api.mpsa.com/api/connectedcar/v2/oauth/authorize"
AUTHORIZE_SERVICE = {"clientsB2COpel": "https://idpcvs.opel.com/am/oauth2/authorize",
"clientsB2CPeugeot": "https://idpcvs.peugeot.com/am/oauth2/authorize",
"clientsB2CCitroen": "https://idpcvs.citroen.com/am/oauth2/authorize",
"clientsB2CDS": "https://idpcvs.driveds.com/am/oauth2/authorize",
"clientsB2CVauxhall": "https://idpcvs.vauxhall.co.uk/am/oauth2/authorize"
}
REMOTE_URL = "https://api.groupe-psa.com/connectedcar/v4/virtualkey/remoteaccess/token?client_id="
BRAND = {"com.psa.mym.myopel": {"realm": "clientsB2COpel", "brand_code": "OP", "app_name": "MyOpel"},
"com.psa.mym.mypeugeot": {"realm": "clientsB2CPeugeot", "brand_code": "AP", "app_name": "MyPeugeot"},
+33 -2
View File
@@ -1,4 +1,9 @@
import logging
import hashlib
import secrets
import base64
from typing import Tuple
from http import HTTPStatus
from typing import Optional
@@ -13,17 +18,43 @@ from psa_car_controller.psa.connected_car_api.rest import ApiException
logger = logging.getLogger(__name__)
def generate_sha256_pkce(length: int) -> Tuple[str, str]:
if not (43 <= length <= 128):
raise ValueError("Invalid length: %d" % length)
verifier = secrets.token_urlsafe(length)
encoded = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode('ascii')).digest())
challenge = encoded.decode('ascii')[:-1]
return verifier, challenge
class OpenIdCredentialManager(CredentialManager):
@staticmethod
def create(service_information: ServiceInformation, scheme: str, country_code: str,
proxies: Optional[dict] = None):
manager = OpenIdCredentialManager(service_information, proxies)
manager.redirect_uri = scheme + "://oauth2redirect/" + country_code.lower()
return manager
def __init__(self, service_information: ServiceInformation, proxies: Optional[dict] = None):
super().__init__(service_information, proxies)
self.refresh_callbacks = []
self.code_verifier = None
self.redirect_uri = None
def _grant_password_request_realm(self, login: str, password: str, realm: str) -> dict:
return {"grant_type": 'password', "username": login, "scope": ' '.join(self.service_information.scopes),
"password": password, "realm": realm}
def init_with_user_credentials_realm(self, login: str, password: str, realm: str):
self._token_request(self._grant_password_request_realm(login, password, realm), True)
def generate_redirect_url(self):
self.code_verifier, code_challenge = generate_sha256_pkce(64)
return self.generate_authorize_url(self.redirect_uri, secrets.token_urlsafe(16),
code_challenge=code_challenge, code_challenge_method="S256")
def connect_with_code(self, code: str):
assert len(code) == 36, "Invalid code length"
self._token_request({"grant_type": 'authorization_code', "code": code,
"redirect_uri": self.redirect_uri, "code_verifier": self.code_verifier},
False)
@staticmethod
def _is_token_expired(response: Response) -> bool:
+13 -11
View File
@@ -266,17 +266,19 @@ class Otp:
def get_otp_code(self):
self.mode = Otp.OTP_MODE
otp_code = None
if self.activation_start():
res = self.activation_finalyze()
if res != Otp.NOK:
if res == Otp.OTP_TWICE:
self.mode = Otp.OTP_MODE
self.activation_start()
self.activation_finalyze()
otp_code = self._get_otp_code()
logger.debug("otp code: %s", otp_code)
if otp_code is None:
raise ConfigException("Can't get otp code")
try:
if self.activation_start():
res = self.activation_finalyze()
if res != Otp.NOK:
if res == Otp.OTP_TWICE:
self.mode = Otp.OTP_MODE
self.activation_start()
assert self.activation_finalyze() == Otp.OK
otp_code = self._get_otp_code()
assert otp_code is not None
logger.debug("otp code: %s", otp_code)
except AssertionError as e:
raise ConfigException("Can't get otp code") from e
return otp_code
def __getstate__(self):
@@ -1,4 +1,5 @@
import json
import logging
import os
from androguard.core.bytecodes.apk import APK
@@ -8,6 +9,8 @@ from cryptography.hazmat.primitives.serialization import pkcs12
from psa_car_controller.psa.constants import BRAND
logging.getLogger("androguard").setLevel(logging.ERROR)
class ApkParser:
def __init__(self, filename, country_code):
+100 -81
View File
@@ -8,6 +8,7 @@ import requests
from psa_car_controller.psa.constants import BRAND
from psa_car_controller.psa.setup.apk_parser import ApkParser
from psa_car_controller.psa.setup.github import urlretrieve_from_github
from psa_car_controller.psacc.application.car_controller import PSACarController
from psa_car_controller.psacc.application.psa_client import PSAClient
from psa_car_controller.psacc.application.charge_control import ChargeControl, ChargeControls
@@ -17,6 +18,7 @@ APP_VERSION = "1.33.0"
GITHUB_USER = "flobz"
GITHUB_REPO = "psa_apk"
TIMEOUT_IN_S = 10
app = PSACarController()
def get_content_from_apk(filename: str, country_code: str) -> ApkParser:
@@ -26,91 +28,108 @@ def get_content_from_apk(filename: str, country_code: str) -> ApkParser:
return apk_parser
def firstLaunchConfig(package_name, client_email, client_password, country_code, # pylint: disable=too-many-locals
config_prefix=""):
filename = package_name.split(".")[-1] + ".apk"
apk_parser = get_content_from_apk(filename, country_code)
try:
res = requests.post(apk_parser.host_brandid_prod + "/GetAccessToken",
headers={
"Connection": "Keep-Alive",
"Content-Type": "application/json",
"User-Agent": "okhttp/2.3.0"
},
params={"jsonRequest": json.dumps(
{"siteCode": apk_parser.site_code, "culture": "fr-FR", "action": "authenticate",
"fields": {"USR_EMAIL": {"value": client_email},
"USR_PASSWORD": {"value": client_password}}
}
)},
timeout=TIMEOUT_IN_S
)
token = res.json()["accessToken"]
except Exception as ex:
msg = traceback.format_exc() + f"\nHOST_BRANDID : {apk_parser.host_brandid_prod} " \
f"sitecode: {apk_parser.site_code}"
class InitialSetup:
def __init__(self, package_name, client_email, client_password, country_code):
self.package_name = package_name
filename = package_name.split(".")[-1] + ".apk"
apk_parser = get_content_from_apk(filename, country_code)
self.culture = apk_parser.culture
self.site_code = apk_parser.site_code
self.client_id = apk_parser.client_id
self.client_secret = apk_parser.client_secret
self.country_code = country_code
self.user_info = None
self.customer_id = None
try:
msg += res.text
except BaseException:
pass
logger.error(msg)
raise ConnectionError(msg) from ex
try:
res2 = requests.post(
f"https://mw-{BRAND[package_name]['brand_code'].lower()}-m2c.mym.awsmpsa.com/api/v1/user",
params={
"culture": apk_parser.culture,
"width": 1080,
"version": APP_VERSION
},
data=json.dumps({"site_code": apk_parser.site_code, "ticket": token}),
headers={
"Connection": "Keep-Alive",
"Content-Type": "application/json;charset=UTF-8",
"Source-Agent": "App-Android",
"Token": token,
"User-Agent": "okhttp/4.8.0",
"Version": APP_VERSION
},
cert=("certs/public.pem", "certs/private.pem"),
timeout=TIMEOUT_IN_S
)
res = requests.post(apk_parser.host_brandid_prod + "/GetAccessToken",
headers={
"Connection": "Keep-Alive",
"Content-Type": "application/json",
"User-Agent": "okhttp/2.3.0"
},
params={"jsonRequest": json.dumps(
{"siteCode": apk_parser.site_code, "culture": "fr-FR", "action": "authenticate",
"fields": {"USR_EMAIL": {"value": client_email},
"USR_PASSWORD": {"value": client_password}}
}
)},
timeout=TIMEOUT_IN_S
)
res_dict = res2.json()["success"]
customer_id = BRAND[package_name]["brand_code"] + "-" + res_dict["id"]
except Exception as ex:
msg = traceback.format_exc()
self.token = res.json()["accessToken"]
except Exception as ex:
msg = traceback.format_exc() + f"\nHOST_BRANDID : {apk_parser.host_brandid_prod} " \
f"sitecode: {apk_parser.site_code}"
try:
msg += res.text
except BaseException:
pass
logger.error(msg)
raise ConnectionError(msg) from ex
# Psacc
self.user_info = self.__fetch_user_info()
self.customer_id = BRAND[self.package_name]["brand_code"] + "-" + self.user_info["id"]
self.psacc = PSAClient(None, self.client_id, self.client_secret,
None, self.customer_id, BRAND[self.package_name]["realm"],
self.country_code, BRAND[self.package_name]["brand_code"])
def __fetch_user_info(self):
try:
msg += res2.text
except BaseException:
pass
logger.error(msg)
raise ConnectionError(msg) from ex
# Psacc
psacc = PSAClient(None, apk_parser.client_id, apk_parser.client_secret,
None, customer_id, BRAND[package_name]["realm"],
country_code)
psacc.connect(client_email, client_password)
psacc.save_config(name=config_prefix + "config.json")
res = psacc.get_vehicles()
res2 = requests.post(
f"https://mw-{BRAND[self.package_name]['brand_code'].lower()}-m2c.mym.awsmpsa.com/api/v1/user",
params={
"culture": self.culture,
"width": 1080,
"version": APP_VERSION
},
data=json.dumps({"site_code": self.site_code, "ticket": self.token}),
headers={
"Connection": "Keep-Alive",
"Content-Type": "application/json;charset=UTF-8",
"Source-Agent": "App-Android",
"Token": self.token,
"User-Agent": "okhttp/4.8.0",
"Version": APP_VERSION
},
cert=("certs/public.pem", "certs/private.pem"),
timeout=TIMEOUT_IN_S
)
if len(res) == 0:
raise ValueError("No vehicle in your account is compatible with this API, you vehicle is probably too old...")
res_dict = res2.json()["success"]
except Exception as ex:
msg = traceback.format_exc()
try:
msg += res2.text
except BaseException:
pass
logger.error(msg)
raise ConnectionError(msg) from ex
return res_dict
for vehicle in res_dict["vehicles"]:
car = psacc.vehicles_list.get_car_by_vin(vehicle["vin"])
if car is not None and "short_label" in vehicle and car.label == "unknown":
car.label = vehicle["short_label"].split(" ")[-1] # remove new, nouvelle, neu word....
psacc.vehicles_list.save_cars()
def connect(self, code, config_prefix=""):
self.psacc.connect(code)
self.psacc.save_config(name=config_prefix + "config.json")
res = self.psacc.get_vehicles()
logger.info("\nYour vehicles: %s", res)
if len(res) == 0:
raise ValueError(
"No vehicle in your account is compatible with this API, you vehicle is probably too old...")
# Charge control
charge_controls = ChargeControls(config_prefix + "charge_config.json")
for vehicle in res:
chc = ChargeControl(psacc, vehicle.vin, 100, [0, 0])
charge_controls[vehicle.vin] = chc
charge_controls.save_config()
return "Success !!!"
for vehicle in self.user_info["vehicles"]:
car = self.psacc.vehicles_list.get_car_by_vin(vehicle["vin"])
if car is not None and "short_label" in vehicle and car.label == "unknown":
car.label = vehicle["short_label"].split(" ")[-1] # remove new, nouvelle, neu word....
self.psacc.vehicles_list.save_cars()
logger.info("\nYour vehicles: %s", res)
# Charge control
charge_controls = ChargeControls(config_prefix + "charge_config.json")
for vehicle in res:
chc = ChargeControl(self.psacc, vehicle.vin, 100, [0, 0])
charge_controls[vehicle.vin] = chc
charge_controls.save_config()
app.load_app()
app.start_remote_control()
@@ -4,7 +4,7 @@ import logging
import socket
import sys
import threading
from os import environ, path
from os import path
import psa_car_controller
from oauth2_client.credentials_manager import OAuthError
@@ -35,8 +35,6 @@ def parse_args():
parser.add_argument("-p", "--port", help="change server listen port", default="5000")
parser.add_argument("-r", "--record", help="save vehicle data to db", action='store_true')
parser.add_argument("-R", "--refresh", help="refresh vehicles status every x min", type=int)
parser.add_argument("-m", "--mail", default=environ.get('USER_EMAIL', None), help="set the email address")
parser.add_argument("-P", "--password", default=environ.get('USER_PASSWORD', None), help="set the password")
parser.add_argument("--remote-disable", help="disable remote control", action='store_true')
parser.add_argument("--offline", help="offline limited mode", action='store_true')
parser.add_argument("--web-conf", help="ignore if config files not existing yet", action='store_true')
@@ -76,7 +74,6 @@ class PSACarController(metaclass=Singleton):
logger.error("start_remote_control failed redo otp config")
def load_app(self) -> bool:
# pylint: disable=too-many-branches
my_logger(handler_level=int(self.args.debug))
logger.info("App version %s", __version__)
@@ -105,16 +102,11 @@ class PSACarController(metaclass=Singleton):
if self.is_good:
logger.info(str(self.myp.get_vehicles()))
except OAuthError:
if self.args.mail and self.args.password:
self.myp.connect(self.args.mail, self.args.password)
logger.info(str(self.myp.get_vehicles()))
self.is_good = True
self.is_good = False
if self.args.web_conf:
logger.error("Please reconnect by going to config web page")
else:
self.is_good = False
if self.args.web_conf:
logger.error("Please reconnect by going to config web page")
else:
logger.error("Connection need to be updated, Please redo authentication process.")
logger.error("Connection need to be updated, Please redo authentication process.")
if self.args.refresh:
self.myp.info_refresh_rate = self.args.refresh * 60
if self.is_good:
@@ -31,20 +31,22 @@ logger = CustomLogger.getLogger(__name__)
class PSAClient:
def connect(self, user, password):
self.manager.init_with_user_credentials_realm(user, password, self.realm)
def connect(self, code: str):
self.manager.connect_with_code(code)
# pylint: disable=too-many-arguments
def __init__(self, refresh_token, client_id, client_secret, remote_refresh_token, customer_id, realm, country_code,
proxies=None, weather_api=None, abrp=None, co2_signal_api=None):
brand=None, proxies=None, weather_api=None, abrp=None, co2_signal_api=None):
self.realm = realm
self.service_information = ServiceInformation(AUTHORIZE_SERVICE,
self.service_information = ServiceInformation(AUTHORIZE_SERVICE[self.realm],
realm_info[self.realm]['oauth_url'],
client_id,
client_secret,
SCOPE, True)
self.client_id = client_id
self.manager = OpenIdCredentialManager(self.service_information)
self.country_code = country_code
self.manager = OpenIdCredentialManager.create(self.service_information,
realm_info[self.realm]["scheme"], self.country_code)
self.api_config = Oauth2PSACCApiConfig()
self.api_config.set_refresh_callback(self.manager.refresh_token_now)
self.manager.refresh_token = refresh_token
@@ -59,7 +61,7 @@ class PSAClient:
self.remote_token_last_update = None
self._record_enabled = False
self.weather_api = weather_api
self.country_code = country_code
self.brand = brand
self.info_callback = []
self.info_refresh_rate = 120
if abrp is None:
@@ -12,6 +12,13 @@
abrp_name: Citroen;e-Berlingo;2022+ (alpha)
reg: VR7EZZKXZP.*
max_elec_consumption: 70
- !ElecModel
name: e-berlingo 2022+
battery_power: 46
fuel_capacity: 0
abrp_name: Citroen;e-Berlingo;2022+ (alpha)
reg: VR7EZZKXZM.*
max_elec_consumption: 70
- !ElecModel
name: e-berlingo 2022+
battery_power: 46
@@ -31,7 +38,7 @@
battery_power: 46
fuel_capacity: 0
abrp_name: peugeot:e208:20:50
reg: VR3UHZKX.*
reg: VR3UHZK[WX].*
max_elec_consumption: 70
- !ElecModel
name: e-2008
@@ -278,6 +285,14 @@
reg: VR3F45GG.*
max_elec_consumption: 70
max_fuel_consumption: 30
- !CarModel
name: 508 BlueHDI 130
battery_power: 0
fuel_capacity: 55
abrp_name:
reg: VR3FBYHZ.*
max_elec_consumption: 0
max_fuel_consumption: 30
- !CarModel
name: Grandland X Hybrid
battery_power: 13.2
@@ -350,6 +365,14 @@
reg: VF73ABHZMJ.*
max_elec_consumption: 70
max_fuel_consumption: 30
- !CarModel
name: C4 2021
battery_power: 0
fuel_capacity: 50
abrp_name:
reg: VR7BAHNSAM.*
max_elec_consumption: 0
max_fuel_consumption: 30
- !ElecModel
name: Combo-e Cargo
battery_power: 50
@@ -531,3 +554,9 @@
fuel_capacity: 42
abrp_name:
reg: VF7SXHNP.*
- !CarModel
name: C4 Shine 2023
battery_power: 0
fuel_capacity: 50
abrp_name:
reg: VR7BAHNSBP.*
+3 -3
View File
@@ -9,7 +9,7 @@ TIMEOUT_IN_S = 10
def get_temp(latitude: str, longitude: str, api_key: str) -> float:
try:
if not (latitude is None or longitude is None or api_key is None):
if latitude and longitude and api_key:
weather_rep = requests.get("https://api.openweathermap.org/data/2.5/onecall",
params={"lat": latitude, "lon": longitude,
"exclude": "minutely,hourly,daily,alerts",
@@ -22,8 +22,8 @@ def get_temp(latitude: str, longitude: str, api_key: str) -> float:
return temp
except ConnectionError:
logger.error("Can't connect to openweathermap :", exc_info=True)
except KeyError:
logger.error("Unable to get temperature from openweathermap :", exc_info=True)
except (KeyError, TypeError):
logger.exception("Unable to get temperature from openweathermap :")
return None
@@ -0,0 +1,63 @@
import logging
from dash import callback_context, html, dcc
from dash.exceptions import PreventUpdate
from flask import request
from psa_car_controller.web.app import dash_app
import dash_bootstrap_components as dbc
from dash.dependencies import Output, Input, State
from psa_car_controller.web.view import config_views
logger = logging.getLogger(__name__)
def get_oauth_config_layout(redirect_url):
return dbc.Row(dbc.Col(md=12, lg=2, className="m-3", children=[
dbc.Row(html.H2('Connection to PSA')),
dbc.Row(className="ms-2", children=[
html.Div(html.P([
html.A("1. Click here", href=redirect_url, target="_blank"), html.Br(),
"2. Complete the login procedure there too until you see 'LOGIN SUCCESSFUL'", html.Br(),
"3. Open your browser's DevTools (F12) and then the click on 'Network' tab", html.Br(),
"4. Hit the final 'OK' button, under 'LOGIN SUCCESSFUL'", html.Br(),
"5. Find in the network tab: xxxx://oauth2redirect....?code=<copy this part>&scope=openid... ",
html.Br(),
html.A("You can find more info here",
href="https://github.com/flobz/psa_car_controller/discussions/779"), html.Br()]
)),
dbc.Form([
html.Div([
dbc.Label("Code", html_for="psa-oauth-code"),
dbc.Input(type="text", id="psa-oauth-code", placeholder="Enter login code"),
dbc.FormText(
"PSA code from step above",
color="secondary",
)]),
dbc.Row(dbc.Button("Submit", color="primary", id="finish-oauth")),
dcc.Loading(
id="loading-2",
children=[html.Div([html.Div(id="oauth-result")])],
type="circle",
),
])
])]))
@dash_app.callback(
Output("oauth-result", "children"),
Input("finish-oauth", "n_clicks"),
State("psa-oauth-code", "value"))
def finish_oauth(n_clicks, code): # pylint: disable=unused-argument
ctx = callback_context
if ctx.triggered:
try:
config_views.INITIAL_SETUP.connect(code)
return dbc.Alert(["PSA login finish !",
html.A(" Go to otp config", href=request.url_root + "config_otp")],
color="success")
except Exception as e:
logger.exception("finish_oauth:")
return dbc.Alert(str(e), color="danger")
raise PreventUpdate()
+16 -11
View File
@@ -1,4 +1,5 @@
import logging
from urllib import parse
from dash import callback_context, html, dcc
from dash.exceptions import PreventUpdate
@@ -6,7 +7,7 @@ from flask import request
from psa_car_controller.psa.otp.otp import new_otp_session
from psa_car_controller.psacc.application.car_controller import PSACarController
from psa_car_controller.psa.setup.app_decoder import firstLaunchConfig
from psa_car_controller.psa.setup.app_decoder import InitialSetup
from psa_car_controller.common.mylogger import LOG_FILE
from psa_car_controller.web.app import dash_app
import dash_bootstrap_components as dbc
@@ -15,7 +16,9 @@ from dash.dependencies import Output, Input, State
logger = logging.getLogger(__name__)
app = PSACarController()
login_config_layout = dbc.Row(dbc.Col(md=12, lg=2, className="m-3", children=[
INITIAL_SETUP: InitialSetup = None
setup_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([
@@ -61,10 +64,9 @@ login_config_layout = dbc.Row(dbc.Col(md=12, lg=2, className="m-3", children=[
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")),
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")])],
@@ -115,7 +117,7 @@ def log_layout():
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([setup_config_layout], label="User config", tab_id="login"),
dbc.Tab([config_otp_layout], label="OTP config", tab_id="otp")])
@@ -129,11 +131,14 @@ def config_layout(activeTabs="log"):
def connectPSA(n_clicks, app_name, email, password, countrycode): # pylint: disable=unused-argument
ctx = callback_context
if ctx.triggered:
logger.info("Initial setup...")
try:
res = firstLaunchConfig(app_name, email, password, countrycode)
app.load_app()
app.start_remote_control()
return dbc.Alert([res, html.A(" Go to otp config", href=request.url_root + "config_otp")], color="success")
global INITIAL_SETUP
INITIAL_SETUP = InitialSetup(app_name, email, password, countrycode)
redirect_uri = parse.quote(INITIAL_SETUP.psacc.manager.generate_redirect_url())
return dbc.Alert(["Success !", html.A(" Go to login",
href=f"{request.url_root}config_connect?url={redirect_uri}")],
color="success")
except Exception as e:
res = str(e)
logger.exception(e)
+3
View File
@@ -22,6 +22,7 @@ from psa_car_controller.web import figures
from psa_car_controller.web.app import dash_app
from psa_car_controller.psacc.repository.db import Database
from psa_car_controller.web.tools.utils import diff_dashtable, unix_time_millis, get_marks_from_start_end, create_card
from psa_car_controller.web.view.config_oauth import get_oauth_config_layout
from psa_car_controller.web.view.config_views import log_layout, config_layout
# pylint: disable=invalid-name
@@ -75,6 +76,8 @@ def display_page(pathname, search):
page = config_layout()
elif pathname == "/config_login":
page = config_layout("login")
elif pathname == "/config_connect":
page = get_oauth_config_layout(query_params["url"])
elif pathname == "/log":
page = log_layout()
elif not APP.is_good:
+2 -2
View File
@@ -12,7 +12,7 @@ include = [
[tool.poetry.dependencies]
python = ">=3.7.2, <4.0.0"
paho-mqtt = ">=1.5.0"
paho-mqtt = ">=1.5.0, <2.0.0"
dash = ">=2.9.0, <3.0.0"
dash-daq = "^0.5.0"
plotly = ">=5"
@@ -21,7 +21,7 @@ Werkzeug = ">=1.0.0"
Flask = ">=1.0.4"
dash-bootstrap-components = ">=1"
ConfigUpdater = ">=3.0"
oauth2-client = "^1.2.1"
oauth2-client = "^1.3.0"
requests = "^2.27.1"
pytz = "^2021.0"
argparse = "^1.4.0"