Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new feature #661

Merged
merged 5 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 10 additions & 15 deletions psa_car_controller/psa/RemoteClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,11 @@ def _refresh_remote_token(self, force=False):
bad_remote_token = True
if bad_remote_token:
otp_code = self.get_otp_code()
res = self.get_remote_access_token(otp_code)
self._get_remote_access_token(otp_code)
self.remote_token_last_update = datetime.now()
self.mqtt_client.username_pw_set("IMA_OAUTH_ACCESS_TOKEN", self.remoteCredentials.access_token)
return True
except (RequestException, RateLimitException) as e:
except (RequestException, RateLimitException, KeyError) as e:
logger.exception("Can't refresh remote token %s", e)
time.sleep(60)
return False
Expand All @@ -198,19 +198,14 @@ def get_otp_code(self):
save_otp(self.otp)
return otp_code

def get_remote_access_token(self, password):
try:
res = self.manager.post(REMOTE_URL + self.account_info.client_id,
json={"grant_type": "password", "password": password},
headers=self.headers)
data = res.json()
self.remoteCredentials.access_token = data["access_token"]
self.remoteCredentials.refresh_token = data["refresh_token"]
return res
except RequestException as e:
logger.error("Can't refresh remote token %s", e)
time.sleep(60)
return None
def _get_remote_access_token(self, password):
res = self.manager.post(REMOTE_URL + self.account_info.client_id,
json={"grant_type": "password", "password": password},
headers=self.headers)
data = res.json()
self.remoteCredentials.access_token = data["access_token"]
self.remoteCredentials.refresh_token = data["refresh_token"]
return res

def horn(self, vin, count):
msg = self.mqtt_request(vin, {"nb_horn": count, "action": "activate"}, "/Horn")
Expand Down
37 changes: 24 additions & 13 deletions psa_car_controller/psacc/application/psa_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime, timedelta, timezone
from json import JSONEncoder
from hashlib import md5
from sqlite3.dbapi2 import IntegrityError

from oauth2_client.credentials_manager import ServiceInformation
from urllib3.exceptions import InvalidHeader
Expand Down Expand Up @@ -41,7 +42,7 @@ def __init__(self, refresh_token, client_id, client_secret, remote_refresh_token
realm_info[self.realm]['oauth_url'],
client_id,
client_secret,
SCOPE, False)
SCOPE, True)
self.client_id = client_id
self.manager = OpenIdCredentialManager(self.service_information)
self.api_config = Oauth2PSACCApiConfig()
Expand All @@ -52,7 +53,7 @@ def __init__(self, refresh_token, client_id, client_secret, remote_refresh_token
self.vehicles_list = Cars.load_cars(CARS_FILE)
self.customer_id = customer_id
self._config_hash = None
self.api_config.verify_ssl = False
self.api_config.verify_ssl = True
self.api_config.api_key['client_id'] = self.client_id
self.api_config.api_key['x-introspect-realm'] = self.realm
self.remote_token_last_update = None
Expand Down Expand Up @@ -194,17 +195,27 @@ def record_info(self, car: Car): # pylint: disable=too-many-locals
Database.record_position(self.weather_api, car.vin, mileage, latitude, longitude, altitude, date, level,
level_fuel, moving)
self.abrp.call(car, Database.get_last_temp(car.vin))
try:
charging_status = car.status.get_energy('Electric').charging.status
charging_mode = car.status.get_energy('Electric').charging.charging_mode
charging_rate = car.status.get_energy('Electric').charging.charging_rate
autonomy = car.status.get_energy('Electric').autonomy
Charging.record_charging(car, charging_status, charge_date, level, latitude, longitude, self.country_code,
charging_mode, charging_rate, autonomy, mileage)
logger.debug("charging_status:%s ", charging_status)
except AttributeError as ex:
logger.error("charging status not available from api")
logger.debug(ex)
if car.has_battery():
electric_energy_status = car.status.get_energy('Electric')
try:
charging_status = electric_energy_status.charging.status
charging_mode = electric_energy_status.charging.charging_mode
charging_rate = electric_energy_status.charging.charging_rate
autonomy = electric_energy_status.autonomy
Charging.record_charging(car, charging_status, charge_date, level, latitude, longitude,
self.country_code,
charging_mode, charging_rate, autonomy, mileage)
logger.debug("charging_status:%s ", charging_status)
except AttributeError as ex:
logger.error("charging status not available from api")
logger.debug(ex)
try:
soh = electric_energy_status.battery.health.resistance
Database.record_battery_soh(car.vin, charge_date, soh)
except IntegrityError:
logger.debug("SOH already recorded")
except AttributeError as ex:
logger.debug("Failed to record SOH: %s", ex)

def __iter__(self):
for key, value in self.__dict__.items():
Expand Down
10 changes: 10 additions & 0 deletions psa_car_controller/psacc/model/battery_soh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from datetime import datetime

from typing import List


class BatterySoh:
def __init__(self, vin, dates, levels):
self.vin = vin
self.dates: List[datetime] = dates
self.levels: List[float] = levels
31 changes: 30 additions & 1 deletion psa_car_controller/psacc/repository/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from psa_car_controller.common import utils
from psa_car_controller.psacc.model.battery_curve import BatteryCurveDto
from psa_car_controller.psacc.model.battery_soh import BatterySoh
from psa_car_controller.psacc.model.charge import Charge
from psa_car_controller.psacc.utils.utils import get_temp

Expand Down Expand Up @@ -103,6 +104,8 @@ def init_db(conn):
"start_level INTEGER, end_level INTEGER, co2 INTEGER, kw INTEGER);")
conn.execute("""CREATE TABLE IF NOT EXISTS battery_curve (start_at DATETIME, VIN TEXT, date DATETIME,
level INTEGER, UNIQUE(start_at, VIN, level));""")
conn.execute("""CREATE TABLE IF NOT EXISTS
battery_soh(date DATETIME, VIN TEXT, level FLOAT, UNIQUE(VIN, level));""")
table_to_update = [["position", NEW_POSITION_COLUMNS],
["battery", NEW_BATTERY_COLUMNS],
["battery_curve", NEW_BATTERY_CURVE_COLUMNS]]
Expand Down Expand Up @@ -182,7 +185,7 @@ def get_battery_curve(conn, start_at, stop_at, vin):
battery_curves = []
res = conn.execute("""SELECT date, level, rate, autonomy
FROM battery_curve
WHERE start_at=? and date<=? and VIN=?
WHERE start_at=? and date<=? and VIN=?
ORDER BY date asc;""",
(start_at, stop_at, vin)).fetchall()
for row in res:
Expand Down Expand Up @@ -267,6 +270,32 @@ def record_position(weather_api, vin, mileage, latitude, longitude, altitude, da
logger.debug("position already saved")
return False

@staticmethod
def record_battery_soh(vin: str, date: datetime, level: float):
conn = Database.get_db()
conn.execute("INSERT INTO battery_soh(date, VIN, level) VALUES(?,?,?)", (date, vin, level))
conn.commit()
conn.close()

@staticmethod
def get_soh_by_vin(vin) -> BatterySoh:
conn = Database.get_db()
res = conn.execute("SELECT date, level FROM battery_soh WHERE VIN=? ORDER BY date", (vin,)).fetchall()
dates = []
levels = []
for row in res:
dates.append(row[0])
levels.append(row[1])
return BatterySoh(vin, dates, levels)

@staticmethod
def get_last_soh_by_vin(vin) -> float:
conn = Database.get_db()
res = conn.execute("SELECT level FROM battery_soh WHERE VIN=? ORDER BY date DESC LIMIT 1", (vin,)).fetchall()
if res:
return res[0][0]
return None

@staticmethod
def get_last_charge(vin) -> Charge:
conn = Database.get_db()
Expand Down
59 changes: 59 additions & 0 deletions psa_car_controller/web/assets/images/battery-soh.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 36 additions & 24 deletions psa_car_controller/web/view/control.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
from collections import OrderedDict

import dash_bootstrap_components as dbc
from dash import html

from psa_car_controller.psacc.application.psa_client import PSAClient
from psa_car_controller.psacc.repository.db import Database
from psa_car_controller.web.tools.Button import Button
from psa_car_controller.web.tools.Switch import Switch
from psa_car_controller.web.tools.utils import card_value_div, create_card
Expand Down Expand Up @@ -33,30 +35,40 @@ def get_control_tabs(config):
myp: PSAClient = config.myp
el = []
buttons_row = []
if config.remote_control and car.status is not None:
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=convert_value_to_str(
car.status.get_energy('Electric').level))],
"src": "assets/images/battery-charge.svg"},
"Mileage": {"text": [card_value_div("mileage_value", "km",
value=convert_value_to_str(
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.Div([html.Img(src="assets/images/sync.svg", width="50px"),
car.status.get_energy('Electric').updated_at.strftime("%X %x")]),
myp.remote_client.wakeup).get_html(),
Switch(CHARGE_SWITCH, car.vin, "Charge", myp.remote_client.charge_now,
charging_state).get_html(),
Switch(PRECONDITIONING_SWITCH, car.vin, "Preconditioning",
myp.remote_client.preconditioning, preconditionning_state).get_html()])
except (AttributeError, TypeError):
logger.exception("get_control_tabs:")
if car.status is not None:
cards = OrderedDict({"Battery SOC": {"text": [card_value_div("battery_value", "%",
value=convert_value_to_str(
car.status.get_energy('Electric').level))],
"src": "assets/images/battery-charge.svg"},
"Mileage": {"text": [card_value_div("mileage_value", "km",
value=convert_value_to_str(
car.status.timed_odometer.mileage))],
"src": "assets/images/mileage.svg"}
})
soh = Database.get_last_soh_by_vin(car.vin)
if soh:
cards["Battery SOH"] = {"text": [card_value_div("battery_soh_value", "%",
value=convert_value_to_str(
soh))],
"src": "assets/images/battery-soh.svg"}
cards.move_to_end("Mileage")
el.append(dbc.Container(dbc.Row(children=create_card(cards)), fluid=True))
if config.remote_control:
try:
preconditionning_state = car.status.preconditionning.air_conditioning.status != "Disabled"
charging_state = car.status.get_energy('Electric').charging.status == "InProgress"

refresh_date = car.status.get_energy('Electric').updated_at.astimezone().strftime("%X %x")
buttons_row.extend([Button(REFRESH_SWITCH, car.vin,
html.Div([html.Img(src="assets/images/sync.svg", width="50px"),
refresh_date]),
myp.remote_client.wakeup).get_html(),
Switch(CHARGE_SWITCH, car.vin, "Charge", myp.remote_client.charge_now,
charging_state).get_html(),
Switch(PRECONDITIONING_SWITCH, car.vin, "Preconditioning",
myp.remote_client.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())
Expand Down
18 changes: 18 additions & 0 deletions tests/data/car_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,21 @@
"vehicles": {
"href": "https://api.groupe-psa.com/connectedcar/v4/user/vehicles/aa"}},
"odometer": {"createdAt": None, "mileage": 3196.5}, "updatedAt": "2022-03-26T11:02:54Z"}
ELECTRIC_CAR_STATUS_V2 = {
"lastPosition": {"type": "Feature", "geometry": {"type": "Point", "coordinates": [-1.59008, 47.274, 30]},
"properties": {"updatedAt": "2021-03-29T06:22:51Z", "type": "Acquire", "signalQuality": 9}},
"preconditionning": {"airConditioning": {"updatedAt": "2022-03-26T10:52:11Z", "status": "Disabled"}},
"energy": [{"createdAt": "2021-09-14T20:39:06Z", "type": "Fuel", "level": 0},
{"createdAt": "2022-03-26T11:02:54Z", "type": "Electric", "level": 59, "autonomy": 122,
"charging": {"plugged": False, "status": "Disconnected", "remainingTime": "PT0S", "chargingRate": 0,
"chargingMode": "No", "nextDelayedTime": "PT22H31M"},
"battery": {"health": {"resistance": 90}}}],
"createdAt": "2022-03-26T11:02:54Z",
"battery": {"voltage": 83.5, "current": 0, "createdAt": "2022-03-26T10:52:11Z"},
"kinetic": {"createdAt": "2021-03-29T06:22:51Z", "moving": True},
"privacy": {"createdAt": "2022-03-26T11:02:53Z", "state": "None"},
"service": {"type": "Electric", "createdAt": "2022-03-26T11:02:54Z"}, "_links": {"self": {
"href": "https://api.groupe-psa.com/connectedcar/v4/user/vehicles/aa/status"},
"vehicles": {
"href": "https://api.groupe-psa.com/connectedcar/v4/user/vehicles/aa"}},
"odometer": {"createdAt": None, "mileage": 3196.5}, "updatedAt": "2022-03-26T11:02:54Z"}
27 changes: 27 additions & 0 deletions tests/test_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import unittest
from sqlite3.dbapi2 import IntegrityError

from psa_car_controller.psacc.model.battery_soh import BatterySoh
from psa_car_controller.psacc.repository.db import Database
from tests.utils import get_new_test_db, compare_dict, get_date, vehicule_list


class TestUnit(unittest.TestCase):
def test_record_soh(self):
get_new_test_db()
car = vehicule_list[0]
soh_list = [99.0, 96.0, 90.2]
for x in range(len(soh_list)):
Database.record_battery_soh(car.vin, get_date(x), soh_list[x])
compare_dict(vars(BatterySoh(car.vin,
[get_date(0), get_date(1), get_date(2)],
soh_list)),
vars(Database.get_soh_by_vin(car.vin))
)
self.assertEqual(soh_list[-1], Database.get_last_soh_by_vin(car.vin))

def test_record_same_soh(self):
get_new_test_db()
car = vehicule_list[0]
Database.record_battery_soh(car.vin, get_date(0), 99.0)
self.assertRaises(IntegrityError, Database.record_battery_soh, car.vin, get_date(0), 99.0)
Loading
Loading