Skip to content

Commit

Permalink
feat: add battery soh record
Browse files Browse the repository at this point in the history
  • Loading branch information
flobz committed Nov 1, 2023
1 parent 3633551 commit f56244c
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 47 deletions.
33 changes: 22 additions & 11 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 @@ -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.info("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
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.
61 changes: 36 additions & 25 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,31 +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))
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 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)
54 changes: 44 additions & 10 deletions tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,10 @@
from psa_car_controller.psacc.repository.db import Database
from psa_car_controller.psacc.repository.trips import Trips
from psa_car_controller.psacc.utils.utils import get_temp
from tests.data.car_status import FUEL_CAR_STATUS, ELECTRIC_CAR_STATUS
from tests.data.car_status import FUEL_CAR_STATUS, ELECTRIC_CAR_STATUS, ELECTRIC_CAR_STATUS_V2, ELECTRIC_CAR_STATUS_V3
from tests.utils import DATA_DIR, record_position, latitude, longitude, date0, date1, date2, date3, record_charging, \
vehicule_list, get_new_test_db, get_date, date4
from psa_car_controller.web.figures import get_figures, get_battery_curve_fig, get_altitude_fig
from deepdiff import DeepDiff


def compare_dict(result, expected):
diff = DeepDiff(expected, result)
if diff != {}:
raise AssertionError(str(diff))
return True


dummy_value = 0
Expand Down Expand Up @@ -164,6 +156,37 @@ def test_electric_record_info(self, mock_db):
True)
self.assertEqual(db_record_position_arg, expected_result)

@patch("psa_car_controller.psacc.repository.db.Database.record_battery_soh")
@patch("psa_car_controller.psacc.repository.db.Database.record_position")
def test_electric_record_info_v2(self, mock_db, moock_soh):
api = ApiClient()
status: psa.connected_car_api.models.status.Status = api._ApiClient__deserialize(
ELECTRIC_CAR_STATUS_V2, "Status")
get_new_test_db()
car = self.vehicule_list[0]
car.status = status
myp = PSAClient.load_config(DATA_DIR + "config.json")
myp.record_info(car)
db_record_position_arg = mock_db.call_args_list[0][0]
expected_result = (None, 'VR3UHZKX', 3196.5, 47.274, -1.59008, 30,
datetime(2022, 3, 26, 11, 2, 54, tzinfo=tzutc()),
59.0,
None,
True)
self.assertEqual(db_record_position_arg, expected_result)
self.assertEqual(
('VR3UHZKX',
datetime(
2022,
3,
26,
11,
2,
54,
tzinfo=tzutc()),
90),
moock_soh.call_args_list[0][0])

def test_record_position_charging(self):
get_new_test_db()
config_repository.CONFIG_FILENAME = DATA_DIR + "config.ini"
Expand All @@ -189,7 +212,18 @@ def test_record_position_charging(self):
start_level = 40
end_level = 85
mileage = 123456789.1
Charging.record_charging(car, "InProgress", date0, start_level, latitude, longitude, None, "slow", 20, 60, mileage)
Charging.record_charging(
car,
"InProgress",
date0,
start_level,
latitude,
longitude,
None,
"slow",
20,
60,
mileage)
Charging.record_charging(car, "InProgress", date1, 70, latitude, longitude, "FR", "slow", 20, 60, mileage)
Charging.record_charging(car, "InProgress", date1, 70, latitude, longitude, "FR", "slow", 20, 60, mileage)
Charging.record_charging(car, "InProgress", date2, 80, latitude, longitude, "FR", "slow", 20, 60, mileage)
Expand Down
8 changes: 8 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from unittest.mock import MagicMock

import pytz
from deepdiff import DeepDiff

from psa_car_controller.psa.RemoteClient import RemoteClient
from psa_car_controller.psa.connected_car_api import Vehicles
Expand Down Expand Up @@ -62,3 +63,10 @@ def get_rc() -> RemoteClient:
account_info = MagicMock()
account_info.realm = ""
return RemoteClient(account_info, Vehicles, None, None)


def compare_dict(result, expected):
diff = DeepDiff(expected, result)
if diff != {}:
raise AssertionError(str(diff))
return True

0 comments on commit f56244c

Please sign in to comment.