mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-23 09:56:14 +00:00
Merge pull request #26 from jlayec/master
first step to manage hybrid cars
This commit is contained in:
+4
-1
@@ -139,6 +139,9 @@ cython_debug/
|
||||
|
||||
.idea/
|
||||
backup.ab
|
||||
info.db
|
||||
otp.bin
|
||||
charge_config1.json
|
||||
config.json
|
||||
test.json
|
||||
test.json
|
||||
charge_config.json
|
||||
|
||||
+4
-4
@@ -54,12 +54,12 @@ class ChargeControl:
|
||||
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.get_energy('Electric').charging.status
|
||||
level = res.get_energy('Electric').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
|
||||
last_update = res.energy[0].updated_at
|
||||
last_update = res.get_energy('Electric').updated_at
|
||||
if (datetime.utcnow().replace(tzinfo=pytz.UTC) - last_update).total_seconds() > 60 * 10:
|
||||
self.psacc.wakeup(self.vin)
|
||||
if (level >= self.percentage_threshold and self.retry_count < 2) or stop_charge:
|
||||
@@ -67,7 +67,7 @@ class ChargeControl:
|
||||
self.retry_count += 1
|
||||
sleep(ChargeControl.MQTT_TIMEOUT)
|
||||
res = self.psacc.get_vehicle_info(self.vin)
|
||||
status = res.energy[0].charging.status
|
||||
status = res.get_energy('Electric').charging.status
|
||||
if status == "InProgress":
|
||||
logger.warn(f"retry to stop the charge of {self.vin}")
|
||||
self.psacc.charge_now(self.vin, False)
|
||||
|
||||
+56
-15
@@ -29,7 +29,10 @@ import sqlite3
|
||||
|
||||
from web.db import get_db
|
||||
|
||||
BATTERY_POWER = 46
|
||||
BATTERY_POWER = 46 #e208
|
||||
FUEL_CAPACITY = 0 #e208
|
||||
#BATTERY_POWER = 10.8 #3008
|
||||
#FUEL_CAPACITY = 43 #3008
|
||||
|
||||
oauhth_url = {"clientsB2CPeugeot": "https://idpcvs.peugeot.com/am/oauth2/access_token",
|
||||
"clientsB2CCitroen": "https://idpcvs.citroen.com/am/oauth2/access_token",
|
||||
@@ -311,6 +314,8 @@ class MyPSACC:
|
||||
elif data["return_code"] == "400":
|
||||
self.refresh_remote_token(force=True)
|
||||
logger.error("retry last request, token was expired")
|
||||
elif data["return_code"] == "300":
|
||||
logger.error(f'{data["return_code"]}')
|
||||
else:
|
||||
logger.error(f'{data["return_code"]} : {data["reason"]}')
|
||||
else:
|
||||
@@ -365,7 +370,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.next_delayed_time
|
||||
hour_str = data.get_energy('Electric').charging.next_delayed_time
|
||||
hour = re.findall(reg, hour_str)[0]
|
||||
h = int(hour[0])
|
||||
if hour[1] == '':
|
||||
@@ -376,7 +381,7 @@ class MyPSACC:
|
||||
|
||||
def get_charge_status(self, vin):
|
||||
data = self.get_vehicle_info(vin)
|
||||
status = data.energy[0].charging.status
|
||||
status = data.get_energy('Electric').charging.status
|
||||
return status
|
||||
|
||||
def veh_charge_request(self, vin, hour, miinute, charge_type):
|
||||
@@ -473,9 +478,12 @@ class MyPSACC:
|
||||
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
|
||||
level = status.get_energy('Electric').level
|
||||
charging_status = status.get_energy('Electric').charging.status
|
||||
charge_date = status.get_energy('Electric').updated_at
|
||||
level_fuel = status.get_energy('Fuel').level
|
||||
moving = status.kinetic.moving
|
||||
logger.debug(f"vin:{vin} longitude:{longitude} latitude:{latitude} date:{date} mileage:{mileage} level:{level} charging_status:{charging_status} charge_date:{charge_date} level_fuel:{level_fuel} moving:{moving}")
|
||||
conn = get_db()
|
||||
if mileage == 0: # fix a bug of the api
|
||||
logger.error(f"The api return a wrong mileage for {vin} : {mileage}")
|
||||
@@ -493,9 +501,17 @@ class MyPSACC:
|
||||
except Exception as e:
|
||||
logger.error(f"Unable to get temperature from openweathermap :{e}")
|
||||
|
||||
if level_fuel == 0: # fix fuel level not provided when car is off
|
||||
try:
|
||||
level_fuel = conn.execute("SELECT level_fuel FROM position WHERE level_fuel>0 AND VIN=? ORDER BY Timestamp DESC LIMIT 1",(vin,)).fetchone()[0]
|
||||
logger.info(f"level_fuel fixed with last real value {level_fuel} for {vin}")
|
||||
except TypeError:
|
||||
level_fuel = None
|
||||
logger.info(f"level_fuel unfixed for {vin}")
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO position(Timestamp,VIN,longitude,latitude,mileage,level, moving, temperature) VALUES(?,?,?,?,?,?,?,?)",
|
||||
(date, vin, longitude, latitude, mileage, level, moving, temp))
|
||||
"INSERT INTO position(Timestamp,VIN,longitude,latitude,mileage,level,level_fuel,moving,temperature) VALUES(?,?,?,?,?,?,?,?,?)",
|
||||
(date, vin, longitude, latitude, mileage, level, level_fuel, moving, temp))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"new position recorded for {vin}")
|
||||
@@ -512,7 +528,6 @@ class MyPSACC:
|
||||
logger.debug("position already saved")
|
||||
|
||||
# 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",
|
||||
@@ -551,8 +566,8 @@ class MyPSACC:
|
||||
features_list = []
|
||||
for row in res:
|
||||
feature = Feature(geometry=Point((row["longitude"], row["latitude"])),
|
||||
properties={"vin": row["vin"], "date": row["Timestamp"], "mileage": row["mileage"],
|
||||
"level": row["level"]})
|
||||
properties={"vin": row["vin"], "date": row["Timestamp"].strftime("%x %X"), "mileage": row["mileage"],
|
||||
"level": row["level"],"level_fuel": row["level_fuel"]})
|
||||
features_list.append(feature)
|
||||
feature_collection = FeatureCollection(features_list)
|
||||
conn.close()
|
||||
@@ -569,15 +584,23 @@ class MyPSACC:
|
||||
tr = Trip()
|
||||
#res = list(map(dict,res))
|
||||
for x in range(0, len(res) - 2):
|
||||
logger.debug(f"{res[x]['Timestamp']} mileage : {res[x]['mileage']}")
|
||||
next_el = res[x + 2]
|
||||
if end["mileage"] - start["mileage"] == 0 or \
|
||||
(end["Timestamp"] - start["Timestamp"]).total_seconds() / 3600 > 3:
|
||||
(end["Timestamp"] - start["Timestamp"]).total_seconds() / 3600 > 10: # condition useless ???
|
||||
logger.debug(f"restart trip")
|
||||
start = end
|
||||
tr = Trip()
|
||||
else:
|
||||
distance = next_el["mileage"] - end["mileage"] # km
|
||||
duration = (next_el["Timestamp"] - end["Timestamp"]).total_seconds() / 3600
|
||||
if (distance == 0 and duration > 0.08) or duration > 2: # check the speed to handle missing point
|
||||
charge = next_el["level"] - end["level"]
|
||||
if next_el["level_fuel"] != None and end["level_fuel"] != None:
|
||||
refuel = next_el["level_fuel"] - end["level_fuel"]
|
||||
else:
|
||||
refuel = None
|
||||
if ((distance == 0 and duration > 0.08) or duration > 2 or # check the speed to handle missing point
|
||||
(refuel != None and refuel > 0) or (distance == 0 and charge > 0)):
|
||||
tr.distance = end["mileage"] - start["mileage"] # km
|
||||
if tr.distance > 0:
|
||||
tr.start_at = start["Timestamp"]
|
||||
@@ -588,11 +611,19 @@ class MyPSACC:
|
||||
diff_level = start["level"] - end["level"]
|
||||
tr.consumption = diff_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")
|
||||
if start["level_fuel"] != None and end["level_fuel"] != None:
|
||||
diff_level_fuel = start["level_fuel"] - end["level_fuel"]
|
||||
tr.consumption_fuel = round(diff_level_fuel / 100 * FUEL_CAPACITY,2) # L
|
||||
tr.consumption_fuel_km = round(100 * tr.consumption_fuel / tr.distance,2) # L/100 km
|
||||
tr.mileage = end["mileage"]
|
||||
logger.debug(
|
||||
f"Trip: {start['Timestamp']} {tr.distance:.1f}km {tr.duration:.2f}h {tr.speed_average:.0f}km/h "
|
||||
f"{tr.consumption:.2f}kw {tr.consumption_km:.2f}kw/100km {tr.consumption_fuel}L {tr.consumption_fuel_km}L/100km {tr.mileage:.1f}km")
|
||||
# filter bad value
|
||||
if tr.consumption_km < 70:
|
||||
if tr.consumption_km < 70 and (tr.consumption_fuel_km == None or tr.consumption_fuel_km < 30):
|
||||
trips.append(tr)
|
||||
else:
|
||||
logger.debug(f"trip discarded")
|
||||
start = next_el
|
||||
tr = Trip()
|
||||
else:
|
||||
@@ -625,3 +656,13 @@ class MyPeugeotEncoder(JSONEncoder):
|
||||
for el in ["client_id", "realm", "remote_refresh_token", "customer_id","weather_api"]:
|
||||
mpd[el] = data[el]
|
||||
return mpd
|
||||
|
||||
|
||||
#add method to class Energy
|
||||
def get_energy(self,energy_type):
|
||||
for energy in self._energy:
|
||||
if energy.type == energy_type:
|
||||
return energy
|
||||
return psac.models.energy.Energy(charging=psac.models.energy_charging.EnergyCharging())
|
||||
|
||||
psac.models.status.Status.get_energy = get_energy
|
||||
|
||||
@@ -31,8 +31,11 @@ class Trip:
|
||||
self.speed_average = None
|
||||
self.consumption = None
|
||||
self.consumption_km = None
|
||||
self.consumption_fuel = None
|
||||
self.consumption_fuel_km = None
|
||||
self.distance = None
|
||||
self.duration = None
|
||||
self.mileage = None
|
||||
|
||||
def add_points(self, longitude, latitude):
|
||||
self.positions.append(Points(longitude, latitude))
|
||||
@@ -43,13 +46,20 @@ class Trip:
|
||||
'consumption': self.consumption_km,
|
||||
}
|
||||
|
||||
def get_consumption_fuel(self):
|
||||
return {
|
||||
'date': self.start_at,
|
||||
'consumption': self.consumption_fuel_km,
|
||||
}
|
||||
|
||||
def to_geojson(self):
|
||||
multi_line_string = MultiLineString(tuple(map(list, self.positions)))
|
||||
return Feature(geometry=multi_line_string, properties={"start_at": self.start_at, "end_at": self.end_at,
|
||||
"average speed": self.speed_average,
|
||||
"average consumption": self.consumption_km})
|
||||
"average consumption": self.consumption_km,
|
||||
"average consumption fuel": self.consumption_fuel_km})
|
||||
|
||||
def get_info(self):
|
||||
res = {"start_at": self.start_at.astimezone(None).strftime("%x %X"), "end_at": self.end_at.astimezone(None).strftime("%x %X"), "duration": self.duration*60,
|
||||
"speed_average": self.speed_average, "consumption_km": self.consumption_km, "distance": self.distance}
|
||||
"speed_average": self.speed_average, "consumption_km": self.consumption_km, "consumption_fuel_km": self.consumption_fuel_km, "distance": self.distance, "mileage": self.mileage}
|
||||
return res
|
||||
|
||||
@@ -26,7 +26,8 @@ def parse_args():
|
||||
parser.add_argument("-r", "--record", help="save vehicle data to db", action='store_true')
|
||||
parser.add_argument("-m", "--mail", help="set the email address")
|
||||
parser.add_argument("-P", "--password", help="set the password")
|
||||
parser.add_argument("--remote-disable", help="disable remote control")
|
||||
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("-b", "--base-path", help="base path for web app",default="/")
|
||||
parser.parse_args()
|
||||
return parser
|
||||
@@ -46,24 +47,27 @@ if __name__ == "__main__":
|
||||
atexit.register(web.app.myp.save_config)
|
||||
if args.record:
|
||||
web.app.myp.set_record(True)
|
||||
try:
|
||||
web.app.myp.manager._refresh_token()
|
||||
except OAuthError:
|
||||
if args.mail and args.password:
|
||||
client_email = args.mail
|
||||
client_password = args.password
|
||||
else:
|
||||
client_email = input("mypeugeot email: ")
|
||||
client_password = input("mypeugeot password: ")
|
||||
web.app.myp.connect(client_email, client_password)
|
||||
logger.info(web.app.myp.get_vehicles())
|
||||
t1 = Thread(target=start_app, args=["My car info", args.base_path, args.debug < 20, args.listen, int(args.port)])
|
||||
t1.start()
|
||||
if args.remote_disable:
|
||||
if args.offline:
|
||||
logger.info("offline mode")
|
||||
else:
|
||||
try:
|
||||
web.app.myp.manager._refresh_token()
|
||||
except OAuthError:
|
||||
if args.mail and args.password:
|
||||
client_email = args.mail
|
||||
client_password = args.password
|
||||
else:
|
||||
client_email = input("mypeugeot email: ")
|
||||
client_password = input("mypeugeot password: ")
|
||||
web.app.myp.connect(client_email, client_password)
|
||||
logger.info(web.app.myp.get_vehicles())
|
||||
if args.offline or args.remote_disable:
|
||||
logger.info("mqtt disabled")
|
||||
else:
|
||||
web.app.myp.start_mqtt()
|
||||
if args.charge_control:
|
||||
web.app.chc = ChargeControls.load_config(web.app.myp, name=args.charge_control)
|
||||
web.app.chc.start()
|
||||
save_config(web.app.myp)
|
||||
save_config(web.app.myp)
|
||||
t1 = Thread(target=start_app, args=["My car info", args.base_path, args.debug < 20, args.listen, int(args.port)])
|
||||
t1.start()
|
||||
|
||||
@@ -18,7 +18,19 @@ def get_db(db_file=default_db_file):
|
||||
conn = sqlite3.connect(db_file, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
|
||||
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, moving BOOLEAN, temperature INTEGER);")
|
||||
"latitude REAL, mileage REAL, level INTEGER, level_fuel INTEGER, moving BOOLEAN, temperature INTEGER);")
|
||||
try:
|
||||
conn.execute("ALTER TABLE position ADD level_fuel INTEGER;")
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
conn.execute("ALTER TABLE position ADD moving BOOLEAN;")
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
conn.execute("ALTER TABLE position ADD temperature INTEGER;")
|
||||
except:
|
||||
pass
|
||||
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)
|
||||
|
||||
+10
-3
@@ -75,10 +75,13 @@ def get_figures(trips: List[Trip], charging: List[dict]):
|
||||
{'id': 'duration', 'name': 'duration', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" min").precision(0)},
|
||||
{'id': 'speed_average', 'name': 'average speed', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" km/h")},
|
||||
'format': deepcopy(nb_format).symbol_suffix(" km/h").precision(0)},
|
||||
{'id': 'consumption_km', 'name': 'average consumption', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" kw/100km")},
|
||||
{'id': 'distance', 'name': 'distance', 'type': 'numeric', 'format': nb_format.symbol_suffix(" km")}],
|
||||
{'id': 'consumption_fuel_km', 'name': 'average consumption fuel', 'type': 'numeric',
|
||||
'format': deepcopy(nb_format).symbol_suffix(" L/100km")},
|
||||
{'id': 'distance', 'name': 'distance', 'type': 'numeric', 'format': nb_format.symbol_suffix(" km")},
|
||||
{'id': 'mileage', 'name': 'mileage', 'type': 'numeric', 'format': nb_format.symbol_suffix(" km")}],
|
||||
data=[tr.get_info() for tr in trips],
|
||||
)
|
||||
# consumption_fig
|
||||
@@ -105,12 +108,16 @@ def get_figures(trips: List[Trip], charging: List[dict]):
|
||||
co2_per_kw = charging_data["co2"].sum() / charging_data["kw"].sum()
|
||||
except ZeroDivisionError:
|
||||
co2_per_kw = 0
|
||||
except KeyError: # when there is no data yet:
|
||||
co2_per_kw = 0
|
||||
co2_per_km = co2_per_kw * kw_per_km / 100
|
||||
try:
|
||||
charge_speed = 3600 * charging_data["kw"].mean() / \
|
||||
(charging_data["stop_at"] - charging_data["start_at"]).mean().total_seconds()
|
||||
except TypeError: # when there is no data yet:
|
||||
charge_speed = 0
|
||||
except KeyError: # when there is no data yet:
|
||||
charge_speed = 0
|
||||
battery_info = html.Div(children=[html.P("Average gC02/kW: {:.1f}".format(co2_per_kw)),
|
||||
html.P("Average gC02/km: {:1f}".format(co2_per_km)),
|
||||
html.P("Average Charge SPEED {:1f} kW/h".format(charge_speed))])
|
||||
html.P("Average Charge SPEED {:1f} kW/h".format(charge_speed))])
|
||||
|
||||
Reference in New Issue
Block a user