update api, update battery record

This commit is contained in:
Florian Bezannier
2020-12-31 00:26:33 +01:00
parent f501d67ed6
commit ed3ee579b9
10 changed files with 391 additions and 59 deletions
+20 -19
View File
@@ -9,25 +9,28 @@ from MyPSACC import MyPSACC
from MyLogger import logger
from psa_connectedcar.rest import ApiException
class ChargeControl:
periodicity = 120
MQTT_TIMEOUT = 60
def __init__(self, psacc:MyPSACC, vin, percentage_threshold, stop_hour):
def __init__(self, psacc: MyPSACC, vin, percentage_threshold, stop_hour):
self.vin = vin
self.percentage_threshold = percentage_threshold
self.set_stop_hour(stop_hour)
self.psacc = psacc
self.retry_count = 0
self.thread:threading.Timer = None
self.thread: threading.Timer = None
self.always_check = True
def set_stop_hour(self,stop_hour):
def set_stop_hour(self, stop_hour):
if stop_hour is None or stop_hour == [0, 0]:
self._stop_hour = None
self._next_stop_hour = None
else:
self._stop_hour = stop_hour
self._next_stop_hour = datetime.now().replace(hour=stop_hour[0], minute=stop_hour[1], second=0)
if self._next_stop_hour < datetime.now():
if self._next_stop_hour < datetime.now():
self._next_stop_hour += timedelta(days=1)
def start(self):
@@ -38,18 +41,18 @@ class ChargeControl:
stop_charge = True
self._next_stop_hour += timedelta(days=1)
logger.info("it's time to stop the charge")
else :
else:
stop_charge = False
if self.percentage_threshold != 100 or stop_charge:
if self.percentage_threshold != 100 or stop_charge or self.always_check:
res = None
try:
res = self.psacc.get_vehicle_info(self.vin)
except ApiException:
logger.error(traceback.format_exc())
if res is not None:
status = res.energy[0]['charging']['status']
level = res.energy[0]["level"]
status = res.energy[0].charging.status
level = res.energy[0].level
logger.info(f"charging status of {self.vin} is {status}, battery level: {level}")
if status == "InProgress":
# force update if the car doesn't send info during 10 minutes
@@ -57,17 +60,17 @@ class ChargeControl:
if (datetime.utcnow() - last_update).total_seconds() > 60 * 10:
self.psacc.wakeup(self.vin)
if (level >= self.percentage_threshold and self.retry_count < 2) or stop_charge:
self.psacc.charge_now(self.vin,False)
self.psacc.charge_now(self.vin, False)
self.retry_count += 1
sleep(ChargeControl.MQTT_TIMEOUT)
res = self.psacc.get_vehicle_info(self.vin)
status = res.energy[0]['charging']['status']
status = res.energy[0].charging.status
if status == "InProgress":
logger.warn(f"retry to stop the charge of {self.vin}")
self.psacc.charge_now(self.vin, False)
self.retry_count += 1
if self._next_stop_hour is not None:
next_in_second = (self._next_stop_hour- now).total_seconds()
next_in_second = (self._next_stop_hour - now).total_seconds()
if next_in_second < periodicity:
periodicity = next_in_second
else:
@@ -95,25 +98,25 @@ class ChargeControls:
def save_config(self, name="charge_config.json", force=False):
chd = {}
for key, el in self.list.items():
chd[el.vin] = {"percentage_threshold": el.percentage_threshold, "stop_hour": el._stop_hour}
chd[el.vin] = {"percentage_threshold": el.percentage_threshold, "stop_hour": el._stop_hour}
config_str = json.dumps(chd, sort_keys=True, indent=4).encode('utf-8')
new_hash = md5(config_str).hexdigest()
if force or self._confighash != new_hash :
if force or self._confighash != new_hash:
with open(name, "wb") as f:
f.write(config_str)
self._confighash = new_hash
logger.info("save config change")
def load_config(psacc:MyPSACC, name="charge_config.json"):
def load_config(psacc: MyPSACC, name="charge_config.json"):
with open(name, "r") as f:
str = f.read()
chd = json.loads(str)
chd = json.loads(str)
charge_control_list = ChargeControls()
for vin, el in chd.items():
charge_control_list.list[vin] = ChargeControl(psacc,vin,**el)
charge_control_list.list[vin] = ChargeControl(psacc, vin, **el)
return charge_control_list
def get(self,vin) -> ChargeControl:
def get(self, vin) -> ChargeControl:
try:
return self.list[vin]
except KeyError:
@@ -122,5 +125,3 @@ class ChargeControls:
def start(self):
for vin, charge_control in self.list.items():
charge_control.start()
+57 -26
View File
@@ -16,6 +16,7 @@ from typing import List
import psa_connectedcar as psac
from Trip import Trip
from ecomix import Ecomix
from psa_connectedcar import ApiClient
from psa_connectedcar.rest import ApiException
from MyLogger import logger
@@ -25,6 +26,8 @@ import sqlite3
from web.db import get_db
BATTERY_POWER = 46
oauhth_url = {"clientsB2CPeugeot": "https://idpcvs.peugeot.com/am/oauth2/access_token",
"clientsB2CCitroen": "https://idpcvs.citroen.com/am/oauth2/access_token",
"clientsB2CDS": "https://idpcvs.driveds.com/am/oauth2/access_token",
@@ -185,7 +188,7 @@ class MyPSACC:
if res is None:
res = self.api().get_vehicle_status(self.get_vehicle_id_with_vin(vin), extension=["odometer"])
if self._record_enabled:
self.record_position(vin, res)
self.record_info(vin, res)
return res
# monitor doesn't seem to work
@@ -311,7 +314,7 @@ class MyPSACC:
def get_charge_hour(self, vin):
reg = r"PT([0-9]{1,2})H([0-9]{1,2})?"
data = self.get_vehicle_info(vin)
hour_str = data.energy[0]['charging']['nextDelayedTime']
hour_str = data.energy[0].charging.next_delayed_time
hour = re.findall(reg, hour_str)[0]
h = int(hour[0])
if hour[1] == '':
@@ -322,7 +325,7 @@ class MyPSACC:
def get_charge_status(self, vin):
data = self.get_vehicle_info(vin)
status = data.energy[0]['charging']['status']
status = data.energy[0].charging.status
return status
def veh_charge_request(self, vin, hour, miinute, charge_type):
@@ -407,32 +410,50 @@ class MyPSACC:
self._record_enabled = value
@staticmethod
def record_position(vin, res: psac.models.status.Status):
longitude = res.last_position.geometry.coordinates[0]
latitude = res.last_position.geometry.coordinates[1]
date = res.last_position.properties.updated_at
mileage = res.timed_odometer.mileage
level = res.energy[0]["level"]
charging_status = res.energy[0]['charging']['status']
if mileage == 0: # fix a bug of the api
def record_info(vin, status: psac.models.status.Status):
longitude = status.last_position.geometry.coordinates[0]
latitude = status.last_position.geometry.coordinates[1]
date = status.last_position.properties.updated_at
mileage = status.timed_odometer.mileage
level = status.energy[0].level
charging_status = status.energy[0].charging.status
conn = get_db()
if mileage == 0: # fix a bug of the api
logger.error(f"The api return a wrong mileage for {vin} : {mileage}")
else:
try:
conn = get_db()
conn.execute("INSERT INTO position(Timestamp,VIN,longitude,latitude,mileage,level) VALUES(?,?,?,?,?,?)",
(date, vin, longitude, latitude, mileage, level))
conn.commit()
logger.info(f"new position recorded for {vin}")
except sqlite3.IntegrityError:
logger.debug("position already saved")
finally:
conn.close()
#todo handle battery status
if charging_status is "InProgress":
#create a new line
pass
elif charging_status is "Stopped" or "Finished":
pass
# todo handle battery status
charge_date = status.energy[0].updated_at
if charging_status == "InProgress":
try:
in_progress = conn.execute("SELECT stop_at FROM battery WHERE VIN=? ORDER BY start_at DESC limit 1", (vin,)).fetchone()[0] is None
except TypeError:
in_progress = False
if not in_progress:
res = conn.execute("INSERT INTO battery(start_at,start_level,VIN) VALUES(?,?,?)",
(charge_date, level, vin))
conn.commit()
else:
try:
start_at, stop_at, start_level = conn.execute("SELECT start_at, stop_at, start_level, from battery WHERE VIN=? ORDER BY start_at "
"DESC limit 1", (vin,)).fetchone()
in_progress = stop_at is None
if in_progress:
co2_per_kw = Ecomix.get_co2_per_kw(start_at, stop_at, latitude, longitude)
kw = (level-start_level)/100*BATTERY_POWER
res = conn.execute("UPDATE battery set stop_at=?, end_level=?, co2=?, kw=? WHERE start_at=? and VIN=?",
(charge_date, level, co2_per_kw, kw, start_at, vin))
conn.commit()
except:
logger.debug("Error when saving status " + traceback.format_exc())
pass
conn.close()
@staticmethod
def get_recorded_position():
@@ -458,9 +479,8 @@ class MyPSACC:
end = res[1]
trips = []
tr = Trip()
battery_power = 46
for x in range(0,len(res)-2):
next_el = res[x+2]
for x in range(0, len(res) - 2):
next_el = res[x + 2]
if end["mileage"] - start["mileage"] == 0 or \
(end["Timestamp"] - start["Timestamp"]).total_seconds() / 3600 > 3:
start = end
@@ -476,7 +496,7 @@ class MyPSACC:
tr.add_points(end["longitude"], end["latitude"])
tr.duration = (end["Timestamp"] - start["Timestamp"]).total_seconds() / 3600
tr.speed_average = tr.distance / tr.duration
tr.consumption = (start["level"] - end["level"]) / 100 * battery_power # kw
tr.consumption = (start["level"] - end["level"]) / 100 * BATTERY_POWER # kw
tr.consumption_km = 100 * tr.consumption / tr.distance # kw/100 km
logger.debug(
f"Trip: {start['Timestamp']} {tr.distance:.1f}km {tr.duration:.2f}h {tr.speed_average:.2f} km/h {tr.consumption:.2f} kw {tr.consumption_km:.2f}kw/100km")
@@ -488,6 +508,19 @@ class MyPSACC:
end = next_el
return trips
@staticmethod
def get_chargings(min=None, max=None):
conn = get_db()
if min is not None:
if max is not None:
res = conn.execute("select * from battery WHERE start_at>=? and start_at<=?",(min,max)).fetchall()
else:
res = conn.execute("select * from battery WHERE start_at>=?",(min,)).fetchall()
elif max is not None:
res = conn.execute("select * from battery WHERE start_at<=?", (max,)).fetchall()
else:
res = conn.execute("select * from battery").fetchall()
return tuple(map(dict,res))
class MyPeugeotEncoder(JSONEncoder):
def default(self, mp: MyPSACC):
@@ -499,5 +532,3 @@ class MyPeugeotEncoder(JSONEncoder):
for el in ["client_id", "realm", "remote_refresh_token", "customer_id"]:
mpd[el] = data[el]
return mpd
+47
View File
@@ -0,0 +1,47 @@
from datetime import datetime, timedelta
from statistics import mean
import xml.etree.ElementTree as ET
import requests
import reverse_geocode
class Ecomix:
@staticmethod
def get_data_france(start, end):
start_str = start.strftime("%d/%m/%Y")
end_str = end.strftime("%d/%m/%Y")
res = requests.get(
f"https://eco2mix.rte-france.com/curves/eco2mixWeb?type=co2&&dateDeb={start_str}&dateFin={end_str}&mode=NORM",
headers={
"Origin": "https://www.rte-france.com",
"Referer": "https://www.rte-france.com/eco2mix/les-emissions-de-co2-par-kwh-produit-en-france",
}
)
etree = ET.fromstring(res.text)
period_start = (start.hour + int(start.minute / 30)) * 4
period_end = (end.hour + int(end.minute / 30)) * 4
valeurs = etree.iter("valeur")
co2_per_kw = []
valeur = next(valeurs)
while int(valeur.attrib["periode"]) != period_start:
valeur = next(valeurs)
while int(valeur.attrib["periode"]) != period_end:
co2_per_kw.append(int(valeur.text))
valeur = next(valeurs)
return mean(co2_per_kw)
@staticmethod
def get_co2_per_kw(start: datetime, end: datetime, latitude, longitude):
location = reverse_geocode.search([(latitude, longitude)])[0]
country_code = location["country_code"]
# todo implement other countries
if country_code == 'FR':
co2_per_kw = Ecomix.get_data_france(start, end)
else:
co2_per_kw = None
return co2_per_kw
+3
View File
@@ -55,6 +55,9 @@ from psa_connectedcar.models.e_coaching import ECoaching
from psa_connectedcar.models.e_coaching_links import ECoachingLinks
from psa_connectedcar.models.e_coaching_scores import ECoachingScores
from psa_connectedcar.models.energy import Energy
from psa_connectedcar.models.energy_battery import EnergyBattery
from psa_connectedcar.models.energy_battery_health import EnergyBatteryHealth
from psa_connectedcar.models.energy_charging import EnergyCharging
from psa_connectedcar.models.engine import Engine
from psa_connectedcar.models.engine_oil import EngineOil
from psa_connectedcar.models.environment import Environment
+3
View File
@@ -46,6 +46,9 @@ from psa_connectedcar.models.e_coaching import ECoaching
from psa_connectedcar.models.e_coaching_links import ECoachingLinks
from psa_connectedcar.models.e_coaching_scores import ECoachingScores
from psa_connectedcar.models.energy import Energy
from psa_connectedcar.models.energy_battery import EnergyBattery
from psa_connectedcar.models.energy_battery_health import EnergyBatteryHealth
from psa_connectedcar.models.energy_charging import EnergyCharging
from psa_connectedcar.models.engine import Engine
from psa_connectedcar.models.engine_oil import EngineOil
from psa_connectedcar.models.environment import Environment
+229 -1
View File
@@ -31,15 +31,243 @@ class Energy(object):
and the value is json key in definition.
"""
swagger_types = {
'updated_at': 'datetime',
'autonomy': 'float',
'battery': 'EnergyBattery',
'charging': 'EnergyCharging',
'consumption': 'float',
'level': 'float',
'residual': 'float',
'type': 'str'
}
attribute_map = {
'updated_at': 'updatedAt',
'autonomy': 'autonomy',
'battery': 'battery',
'charging': 'charging',
'consumption': 'consumption',
'level': 'level',
'residual': 'residual',
'type': 'type'
}
def __init__(self): # noqa: E501
def __init__(self, updated_at=None, autonomy=None, battery=None, charging=None, consumption=None, level=None, residual=None, type=None): # noqa: E501
"""Energy - a model defined in Swagger""" # noqa: E501
self._updated_at = None
self._autonomy = None
self._battery = None
self._charging = None
self._consumption = None
self._level = None
self._residual = None
self._type = None
self.discriminator = None
if updated_at is not None:
self.updated_at = updated_at
if autonomy is not None:
self.autonomy = autonomy
if battery is not None:
self.battery = battery
if charging is not None:
self.charging = charging
if consumption is not None:
self.consumption = consumption
if level is not None:
self.level = level
if residual is not None:
self.residual = residual
if type is not None:
self.type = type
@property
def updated_at(self):
"""Gets the updated_at of this Energy. # noqa: E501
Date when the resource has been updated. # noqa: E501
:return: The updated_at of this Energy. # noqa: E501
:rtype: datetime
"""
return self._updated_at
@updated_at.setter
def updated_at(self, updated_at):
"""Sets the updated_at of this Energy.
Date when the resource has been updated. # noqa: E501
:param updated_at: The updated_at of this Energy. # noqa: E501
:type: datetime
"""
self._updated_at = updated_at
@property
def autonomy(self):
"""Gets the autonomy of this Energy. # noqa: E501
Vehicle autonomy for this energy class expressed in KM. # noqa: E501
:return: The autonomy of this Energy. # noqa: E501
:rtype: float
"""
return self._autonomy
@autonomy.setter
def autonomy(self, autonomy):
"""Sets the autonomy of this Energy.
Vehicle autonomy for this energy class expressed in KM. # noqa: E501
:param autonomy: The autonomy of this Energy. # noqa: E501
:type: float
"""
self._autonomy = autonomy
@property
def battery(self):
"""Gets the battery of this Energy. # noqa: E501
:return: The battery of this Energy. # noqa: E501
:rtype: EnergyBattery
"""
return self._battery
@battery.setter
def battery(self, battery):
"""Sets the battery of this Energy.
:param battery: The battery of this Energy. # noqa: E501
:type: EnergyBattery
"""
self._battery = battery
@property
def charging(self):
"""Gets the charging of this Energy. # noqa: E501
:return: The charging of this Energy. # noqa: E501
:rtype: EnergyCharging
"""
return self._charging
@charging.setter
def charging(self, charging):
"""Sets the charging of this Energy.
:param charging: The charging of this Energy. # noqa: E501
:type: EnergyCharging
"""
self._charging = charging
@property
def consumption(self):
"""Gets the consumption of this Energy. # noqa: E501
Instant consumption for thermic vehicles. # noqa: E501
:return: The consumption of this Energy. # noqa: E501
:rtype: float
"""
return self._consumption
@consumption.setter
def consumption(self, consumption):
"""Sets the consumption of this Energy.
Instant consumption for thermic vehicles. # noqa: E501
:param consumption: The consumption of this Energy. # noqa: E501
:type: float
"""
self._consumption = consumption
@property
def level(self):
"""Gets the level of this Energy. # noqa: E501
:return: The level of this Energy. # noqa: E501
:rtype: float
"""
return self._level
@level.setter
def level(self, level):
"""Sets the level of this Energy.
:param level: The level of this Energy. # noqa: E501
:type: float
"""
if level is not None and level > 100: # noqa: E501
raise ValueError("Invalid value for `level`, must be a value less than or equal to `100`") # noqa: E501
if level is not None and level < 0: # noqa: E501
raise ValueError("Invalid value for `level`, must be a value greater than or equal to `0`") # noqa: E501
self._level = level
@property
def residual(self):
"""Gets the residual of this Energy. # noqa: E501
Residual electric energy avaialble only for electric energy class expressed in KWh. # noqa: E501
:return: The residual of this Energy. # noqa: E501
:rtype: float
"""
return self._residual
@residual.setter
def residual(self, residual):
"""Sets the residual of this Energy.
Residual electric energy avaialble only for electric energy class expressed in KWh. # noqa: E501
:param residual: The residual of this Energy. # noqa: E501
:type: float
"""
self._residual = residual
@property
def type(self):
"""Gets the type of this Energy. # noqa: E501
:return: The type of this Energy. # noqa: E501
:rtype: str
"""
return self._type
@type.setter
def type(self, type):
"""Sets the type of this Energy.
:param type: The type of this Energy. # noqa: E501
:type: str
"""
allowed_values = ["Fuel", "Electric"] # noqa: E501
if type not in allowed_values:
raise ValueError(
"Invalid value for `type` ({0}), must be one of {1}" # noqa: E501
.format(type, allowed_values)
)
self._type = type
def to_dict(self):
"""Returns the model properties as a dict"""
result = {}
+2 -1
View File
@@ -11,9 +11,10 @@ argparse
flask
dash_bootstrap_components
geojson
reverse_geocode
#swagger req
certifi >= 14.05.14
six >= 1.10
python_dateutil >= 2.5.3
urllib3 >= 1.15.1
urllib3 >= 1.15.1
+12 -6
View File
@@ -16,12 +16,15 @@ from web.app import app, dash_app, myp, chc
import web.db
trips = None
chargings = None
@dash_app.callback(Output('trips_map', 'figure'),
Output('consumption_fig', 'figure'),
Output('consumption_fig_by_speed', 'figure'),
Output('consumption', 'children'),
Output('tab_trips', 'children'),
Output('tab_battery', 'children'),
Input('date-slider', 'value'))
def display_value(value):
min = datetime.fromtimestamp(value[0], tz=timezone.utc)
@@ -30,7 +33,8 @@ def display_value(value):
for trip in trips:
if min <= trip.start_at <= max:
filtered_trips.append(trip)
figures.get_figures(filtered_trips)
filtered_chargings = MyPSACC.get_chargings(min,max)
figures.get_figures(filtered_trips,filtered_chargings)
consumption = "Average consumption: {:.1f} kW/100km".format(float(figures.consumption_df.mean(numeric_only=True)))
return figures.trips_map, figures.consumption_fig, figures.consumption_fig_by_speed, consumption, figures.table_fig
@@ -107,23 +111,25 @@ def after_request(response):
def update_trips():
global trips
logger.info("update_trips")
global trips, chargings
logger.info("update_data")
try:
trips = MyPSACC.get_trips()
chargings = MyPSACC.get_chargings()
except:
logger.error("update_trips: "+traceback.format_exc())
logger.error("update_trips: " + traceback.format_exc())
try:
web.db.callback_fct = update_trips
update_trips()
min_date = trips[0].start_at
max_date = trips[-1].start_at
min_millis = figures.unix_time_millis(min_date)
max_millis = figures.unix_time_millis(max_date)
step = (max_millis - min_millis) / 100
figures.get_figures(trips)
figures.get_figures(trips, chargings)
data_div = html.Div([dcc.RangeSlider(
id='date-slider',
min=min_millis,
@@ -142,6 +148,7 @@ try:
dcc.Graph(figure=figures.consumption_fig_by_speed, id="consumption_fig_by_speed")
]),
dbc.Tab(label="Trips", tab_id="trips", id="tab_trips", children=[figures.table_fig]),
dbc.Tab(label="Battery", tab_id="battery", id="tab_battery", children=[figures.battery_info]),
dbc.Tab(label="Map", tab_id="map", children=[
dcc.Graph(figure=figures.trips_map, id="trips_map", style={"height": '90vh'})]),
],
@@ -159,4 +166,3 @@ dash_app.layout = dbc.Container(fluid=True, children=[
html.H1('My car info'),
data_div
])
+2 -2
View File
@@ -19,8 +19,8 @@ def get_db():
conn.row_factory = sqlite3.Row
conn.execute("CREATE TABLE IF NOT EXISTS position (Timestamp DATETIME PRIMARY KEY, VIN TEXT, longitude REAL, "
"latitude REAL, mileage REAL, level INTEGER);")
conn.execute("CREATE TABLE IF NOT EXISTS battery (start_at DATETIME PRIMARY KEY,stop_at DATETIME, start_level, "
"end_level)")
conn.execute("CREATE TABLE IF NOT EXISTS battery (start_at DATETIME PRIMARY KEY,stop_at DATETIME,VIN TEXT, "
"start_level INTEGER, end_level INTEGER, co2 INTEGER, kw INTEGER);")
conn.create_function("update_trips", 0, update_callback)
conn.execute("CREATE TEMP TRIGGER IF NOT EXISTS update_trigger AFTER INSERT ON position BEGIN "
"SELECT update_trips(); END;")
+16 -4
View File
@@ -1,6 +1,7 @@
from copy import deepcopy
from typing import List
import dash_bootstrap_components as dbc
import dash_table
import numpy as np
from dash_table.Format import Format, Scheme, Symbol
@@ -46,10 +47,11 @@ consumption_fig_by_speed = None
table_fig = None
pandas_options.display.float_format = '${:.2f}'.format
info = ""
battery_info = dbc.Alert("No data to show", color="danger")
def get_figures(trips: List[Trip]):
global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig, info
def get_figures(trips: List[Trip], charging: List[dict]):
global consumption_fig, consumption_df, trips_map, consumption_fig_by_speed, table_fig, info, battery_info
lats = []
lons = []
names = []
@@ -94,5 +96,15 @@ def get_figures(trips: List[Trip]):
go.Scatter(mode="markers", x=consum_df_by_speed["speed"], y=consum_df_by_speed["consumption"],
name="Trips"))
consumption_fig_by_speed.update_layout(xaxis_title="average Speed km/h", yaxis_title="Consumption kWh/100Km")
info = "Average consumption: {:.1f} kW/100km".format(
float(consumption_df.mean(numeric_only=True)))
kw_per_km = float(consumption_df.mean(numeric_only=True))
info = "Average consumption: {:.1f} kW/100km".format(kw_per_km)
# charging
charging_data = DataFrame.from_records(charging)
co2_per_kw = charging_data["co2"].sum() / charging_data["kw"].sum()
co2_per_km = co2_per_kw * kw_per_km / 100
charge_speed = 3600 * charging_data["kw"].mean() / \
(charging_data["stop_at"] - charging_data["start_at"]).mean().total_seconds()
battery_info = "Average gC02/kW: {:.1f}\n" \
"Average gC02/km: {:1f}\n" \
"Average Charge SPEED {:1f} gC02/kW".format(co2_per_kw, co2_per_km, charge_speed)