mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-21 08:56:29 +00:00
Feature abrp (#79)
* add ABRP connection * update doc * fix get_last_temp() * add image * update abrp * fix abrpname * fix no last postition * add type point * find model by vin * add Spacetourer * fix indent
This commit is contained in:
@@ -2,66 +2,33 @@ import json
|
||||
from copy import copy
|
||||
|
||||
from MyLogger import logger
|
||||
from libs.car_model import CarModel
|
||||
from libs.car_status import CarStatus
|
||||
|
||||
ENERGY_CAPACITY = {'SUV 3008': {'BATTERY_POWER': 10.8, 'FUEL_CAPACITY': 43},
|
||||
'C5 Aircross': {'BATTERY_POWER': 10.8, 'FUEL_CAPACITY': 43},
|
||||
'e-208': {'BATTERY_POWER': 46, 'FUEL_CAPACITY': 0, "ABRP_NAME": "peugeot:e208:20:50"},
|
||||
'e-2008': {'BATTERY_POWER': 46, 'FUEL_CAPACITY': 0, "ABRP_NAME": "peugeot:e2008:20:48"},
|
||||
'corsa-e': {'BATTERY_POWER': 46, 'FUEL_CAPACITY': 0, "ABRP_NAME": "opel:corsae:20:50"}
|
||||
}
|
||||
DEFAULT_BATTERY_POWER = 46
|
||||
DEFAULT_FUEL_CAPACITY = 0
|
||||
DEFAULT_MAX_ELEC_CONSUMPTION = 70
|
||||
DEFAULT_MAX_FUEL_CONSUMPTION = 30
|
||||
DEFAULT_ABRP_NAME = "peugeot:e208:20:50"
|
||||
CARS_FILE = "cars.json"
|
||||
|
||||
|
||||
class Car:
|
||||
def __init__(self, vin, vehicle_id, brand, label="unknown", battery_power=None, fuel_capacity=None,
|
||||
max_elec_consumption=None, max_fuel_consumption=None):
|
||||
def __init__(self, vin, vehicle_id, brand, label=None, battery_power=None, fuel_capacity=None,
|
||||
max_elec_consumption=None, max_fuel_consumption=None, abrp_name=None):
|
||||
self.vin = vin
|
||||
if label is not None:
|
||||
model = CarModel.find_model_by_name(label)
|
||||
else:
|
||||
model = CarModel.find_model_by_vin(self.vin)
|
||||
label = model.name
|
||||
self.vehicle_id = vehicle_id
|
||||
self.label = label
|
||||
self.brand = brand
|
||||
self.battery_power = None
|
||||
self.fuel_capacity = None
|
||||
self.max_elec_consumption = 0 # kwh/100Km
|
||||
self.max_fuel_consumption = 0 # L/100Km
|
||||
self.set_energy_capacity(battery_power, fuel_capacity, max_elec_consumption, max_fuel_consumption)
|
||||
self.status = None
|
||||
self._status = None
|
||||
self.abrp_name = abrp_name or model.abrp_name
|
||||
self.battery_power = battery_power or model.battery_power
|
||||
self.fuel_capacity = fuel_capacity or model.fuel_capacity
|
||||
self.max_elec_consumption = max_elec_consumption or model.max_elec_consumption # kwh/100Km
|
||||
self.max_fuel_consumption = max_fuel_consumption or model.max_fuel_consumption # L/100Km
|
||||
|
||||
def set_energy_capacity(self, battery_power=None, fuel_capacity=None, max_elec_consumption=None,
|
||||
max_fuel_consumption=None):
|
||||
if battery_power is not None and fuel_capacity is not None:
|
||||
self.battery_power = battery_power
|
||||
self.fuel_capacity = fuel_capacity
|
||||
elif self.__get_model_name() is not None:
|
||||
model_name = self.__get_model_name()
|
||||
self.battery_power = ENERGY_CAPACITY[model_name]["BATTERY_POWER"]
|
||||
self.fuel_capacity = ENERGY_CAPACITY[model_name]["FUEL_CAPACITY"]
|
||||
else:
|
||||
logger.warning("Can't get car model please check %s", CARS_FILE)
|
||||
self.battery_power = DEFAULT_BATTERY_POWER
|
||||
self.fuel_capacity = DEFAULT_FUEL_CAPACITY
|
||||
if self.is_electric():
|
||||
self.max_fuel_consumption = 0
|
||||
else:
|
||||
self.max_fuel_consumption = max_fuel_consumption or DEFAULT_MAX_FUEL_CONSUMPTION
|
||||
if self.is_thermal():
|
||||
self.max_elec_consumption = 0
|
||||
else:
|
||||
self.max_elec_consumption = max_elec_consumption or DEFAULT_MAX_ELEC_CONSUMPTION
|
||||
|
||||
def __get_model_name(self):
|
||||
if self.label in ENERGY_CAPACITY:
|
||||
return self.label
|
||||
if self.__is_opel_corsa():
|
||||
return "corsa-e"
|
||||
return None
|
||||
|
||||
def __is_opel_corsa(self):
|
||||
return self.brand == "C" and self.label is None
|
||||
def set_model_name(self, name):
|
||||
self.label = name
|
||||
|
||||
def is_electric(self) -> bool:
|
||||
return self.fuel_capacity == 0 and self.battery_power > 0
|
||||
@@ -84,12 +51,27 @@ class Car:
|
||||
|
||||
def to_dict(self):
|
||||
car_dict = copy(self.__dict__)
|
||||
car_dict.pop("status")
|
||||
car_dict.pop("_status")
|
||||
return car_dict
|
||||
|
||||
def __str__(self):
|
||||
return str(self.to_dict())
|
||||
|
||||
def get_abrp_name(self):
|
||||
if self.abrp_name is not None:
|
||||
return self.abrp_name
|
||||
raise ValueError("ABRP model is not set")
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@status.setter
|
||||
def status(self, value: CarStatus):
|
||||
self._status = value
|
||||
if self._status is not None and self.status.__class__ != CarStatus:
|
||||
self._status.__class__ = CarStatus
|
||||
self._status.correct()
|
||||
|
||||
class Cars(list):
|
||||
def __init__(self, *args):
|
||||
@@ -129,7 +111,9 @@ class Cars(list):
|
||||
try:
|
||||
with open(name, "r") as f:
|
||||
json_str = f.read()
|
||||
return Cars.from_json(json.loads(json_str))
|
||||
cars=Cars.from_json(json.loads(json_str))
|
||||
cars.save_cars()
|
||||
return cars
|
||||
except (FileNotFoundError, TypeError) as e:
|
||||
logger.debug(e)
|
||||
return Cars()
|
||||
|
||||
+50
-47
@@ -3,7 +3,6 @@ import re
|
||||
import threading
|
||||
import traceback
|
||||
import uuid
|
||||
from copy import copy
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
from json import JSONEncoder
|
||||
@@ -24,7 +23,8 @@ from psa_connectedcar.rest import ApiException
|
||||
from MyLogger import logger
|
||||
|
||||
from utils import get_temp, rate_limit
|
||||
from web.db import get_db, clean_position
|
||||
from web.abrp import Abrp
|
||||
from web.db import get_db, clean_position, get_last_temp
|
||||
from geojson import Feature, Point, FeatureCollection
|
||||
from geojson import dumps as geo_dumps
|
||||
|
||||
@@ -48,17 +48,9 @@ MQTT_RESP_TOPIC = "psa/RemoteServices/to/cid/"
|
||||
MQTT_EVENT_TOPIC = "psa/RemoteServices/events/MPHRTServices/"
|
||||
MQTT_TOKEN_TTL = 890
|
||||
CARS_FILE = "cars.json"
|
||||
DEFAULT_CONFIG_FILENAME = "config.json"
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
class OpenIdCredentialManager(CredentialManager):
|
||||
@@ -128,13 +120,11 @@ def gen_correlation_id(date):
|
||||
|
||||
|
||||
class MyPSACC:
|
||||
vehicles_url = "https://idpcvs.peugeot.com/api/connectedcar/v2/oauth/authorize"
|
||||
|
||||
def connect(self, user, password):
|
||||
self.manager.init_with_user_credentials(user, password, self.realm)
|
||||
|
||||
def __init__(self, refresh_token, client_id, client_secret, remote_refresh_token, customer_id, realm, country_code,
|
||||
proxies=None, weather_api=None):
|
||||
proxies=None, weather_api=None, abrp=None):
|
||||
self.realm = realm
|
||||
self.service_information = ServiceInformation(authorize_service,
|
||||
realm_info[self.realm]['oauth_url'],
|
||||
@@ -149,7 +139,6 @@ class MyPSACC:
|
||||
self.remote_refresh_token = remote_refresh_token
|
||||
self.remote_access_token = None
|
||||
self.vehicles_list = Cars.load_cars(CARS_FILE)
|
||||
self.set_proxies(proxies)
|
||||
self.customer_id = customer_id
|
||||
self._config_hash = None
|
||||
self.api_config.verify_ssl = False
|
||||
@@ -169,12 +158,19 @@ class MyPSACC:
|
||||
self.precond_programs = {}
|
||||
self.info_callback = []
|
||||
self.info_refresh_rate = 120
|
||||
if abrp is None:
|
||||
self.abrp = Abrp()
|
||||
else:
|
||||
self.abrp: Abrp = Abrp(**abrp)
|
||||
self.set_proxies(proxies)
|
||||
self.config_file = DEFAULT_CONFIG_FILENAME
|
||||
|
||||
def get_app_name(self):
|
||||
return realm_info[self.realm]['app_name']
|
||||
|
||||
def refresh_token(self):
|
||||
self.manager._refresh_token()
|
||||
self.save_config()
|
||||
|
||||
def api(self) -> psac.VehiclesApi:
|
||||
self.api_config.access_token = self.manager._access_token
|
||||
@@ -188,6 +184,7 @@ class MyPSACC:
|
||||
else:
|
||||
self._proxies = proxies
|
||||
self.api_config.proxy = proxies['http']
|
||||
self.abrp.proxies = proxies
|
||||
self.manager.proxies = self._proxies
|
||||
Otp.set_proxies(proxies)
|
||||
|
||||
@@ -198,9 +195,10 @@ class MyPSACC:
|
||||
try:
|
||||
res = self.api().get_vehicle_status(car.vehicle_id, extension=["odometer"])
|
||||
if res is not None:
|
||||
car.status = res
|
||||
if self._record_enabled:
|
||||
self.record_info(vin, res)
|
||||
break
|
||||
self.record_info(car)
|
||||
return res
|
||||
except ApiException:
|
||||
logger.error(traceback.format_exc())
|
||||
car.status = res
|
||||
@@ -210,7 +208,7 @@ class MyPSACC:
|
||||
if self.info_refresh_rate is not None:
|
||||
while True:
|
||||
sleep(self.info_refresh_rate)
|
||||
logger.info("refresh_vehicle_info")
|
||||
logger.debug("refresh_vehicle_info")
|
||||
for car in self.vehicles_list:
|
||||
self.get_vehicle_info(car.vin)
|
||||
for callback in self.info_callback:
|
||||
@@ -276,7 +274,7 @@ class MyPSACC:
|
||||
last_update: datetime = self.remote_token_last_update
|
||||
if (datetime.now() - last_update).total_seconds() < MQTT_TOKEN_TTL:
|
||||
return None
|
||||
self.manager._refresh_token()
|
||||
self.refresh_token()
|
||||
if self.remote_refresh_token is None:
|
||||
logger.error("remote_refresh_token isn't defined")
|
||||
self.load_otp(force_new=True)
|
||||
@@ -295,6 +293,7 @@ class MyPSACC:
|
||||
otp_code = self.get_otp_code()
|
||||
res = self.get_remote_access_token(otp_code)
|
||||
self.mqtt_client.username_pw_set("IMA_OAUTH_ACCESS_TOKEN", self.remote_access_token)
|
||||
self.save_config()
|
||||
return res
|
||||
|
||||
def on_mqtt_connect(self, client, userdata, rc, a):
|
||||
@@ -471,7 +470,9 @@ class MyPSACC:
|
||||
self.mqtt_client.publish(MQTT_REQ_TOPIC + self.customer_id + "/ThermalPrecond", msg)
|
||||
return True
|
||||
|
||||
def save_config(self, name="config.json", force=False):
|
||||
def save_config(self, name=None, force=False):
|
||||
if name is None:
|
||||
name = self.config_file
|
||||
config_str = json.dumps(self, cls=MyPeugeotEncoder, sort_keys=True, indent=4).encode("utf8")
|
||||
new_hash = md5(config_str).hexdigest()
|
||||
if force or self._config_hash != new_hash:
|
||||
@@ -487,37 +488,35 @@ class MyPSACC:
|
||||
config = dict(**json.loads(config_str))
|
||||
if "country_code" not in config:
|
||||
config["country_code"] = input("What is your country code ? (ex: FR, GB, DE, ES...)\n")
|
||||
return MyPSACC(**config)
|
||||
if "abrp" not in config:
|
||||
config["abrp"] = None
|
||||
psacc = MyPSACC(**config)
|
||||
psacc.config_file = name
|
||||
return psacc
|
||||
|
||||
def set_record(self, value: bool):
|
||||
self._record_enabled = value
|
||||
|
||||
def record_info(self, vin, status: psac.models.status.Status):
|
||||
mileage = status.timed_odometer.mileage
|
||||
level = status.get_energy('Electric').level
|
||||
level_fuel = status.get_energy('Fuel').level
|
||||
charge_date = status.get_energy('Electric').updated_at
|
||||
try:
|
||||
moving = status.kinetic.moving
|
||||
logger.debug("")
|
||||
except AttributeError:
|
||||
logger.error("kinetic not available from api")
|
||||
moving = None
|
||||
try:
|
||||
longitude = status.last_position.geometry.coordinates[0]
|
||||
latitude = status.last_position.geometry.coordinates[1]
|
||||
date = status.last_position.properties.updated_at
|
||||
except AttributeError:
|
||||
logger.error("last_position not available from api")
|
||||
longitude = latitude = None
|
||||
def record_info(self, car: Car):
|
||||
mileage = car.status.timed_odometer.mileage
|
||||
level = car.status.get_energy('Electric').level
|
||||
level_fuel = car.status.get_energy('Fuel').level
|
||||
charge_date = car.status.get_energy('Electric').updated_at
|
||||
moving = car.status.kinetic.moving
|
||||
|
||||
longitude = car.status.last_position.geometry.coordinates[0]
|
||||
latitude = car.status.last_position.geometry.coordinates[1]
|
||||
date = car.status.last_position.properties.updated_at
|
||||
if date is None:
|
||||
date = charge_date
|
||||
logger.debug("vin:%s longitude:%s latitude:%s date:%s mileage:%s level:%s charge_date:%s level_fuel:"
|
||||
"%s moving:%s", vin, longitude, latitude, date, mileage, level, charge_date, level_fuel,
|
||||
"%s moving:%s", car.vin, longitude, latitude, date, mileage, level, charge_date, level_fuel,
|
||||
moving)
|
||||
self.record_position(vin, mileage, latitude, longitude, date, level, level_fuel, moving)
|
||||
self.record_position(car.vin, mileage, latitude, longitude, date, level, level_fuel, moving)
|
||||
self.abrp.call(car, get_last_temp(car.vin))
|
||||
try:
|
||||
charging_status = status.get_energy('Electric').charging.status
|
||||
self.record_charging(vin, charging_status, charge_date, level, latitude, longitude)
|
||||
charging_status = car.status.get_energy('Electric').charging.status
|
||||
self.record_charging(car.vin, charging_status, charge_date, level, latitude, longitude)
|
||||
logger.debug("charging_status:%s ", charging_status)
|
||||
except AttributeError:
|
||||
logger.error("charging status not available from api")
|
||||
@@ -547,8 +546,9 @@ class MyPSACC:
|
||||
conn.commit()
|
||||
logger.info("new position recorded for %s", vin)
|
||||
clean_position(conn)
|
||||
else:
|
||||
logger.debug("position already saved")
|
||||
return True
|
||||
logger.debug("position already saved")
|
||||
return False
|
||||
|
||||
def record_charging(self, vin, charging_status, charge_date, level, latitude, longitude):
|
||||
conn = get_db()
|
||||
@@ -609,12 +609,15 @@ class MyPSACC:
|
||||
res = conn.execute("select * from battery").fetchall()
|
||||
return tuple(map(dict, res))
|
||||
|
||||
def __iter__(self):
|
||||
for key, value in self.__dict__.items():
|
||||
yield key, value
|
||||
|
||||
class MyPeugeotEncoder(JSONEncoder):
|
||||
def default(self, mp: MyPSACC):
|
||||
data = copy(mp.__dict__)
|
||||
data = dict(mp)
|
||||
mpd = {"proxies": data["_proxies"], "refresh_token": mp.manager.refresh_token,
|
||||
"client_secret": mp.service_information.client_secret}
|
||||
"client_secret": mp.service_information.client_secret, "abrp":dict(mp.abrp)}
|
||||
for el in ["client_id", "realm", "remote_refresh_token", "customer_id", "weather_api", "country_code"]:
|
||||
mpd[el] = data[el]
|
||||
return mpd
|
||||
|
||||
@@ -108,7 +108,8 @@ We will retrieve these informations:
|
||||
|
||||
## FAQ
|
||||
If you have a problem, or a question please check if the answer isn't in the [FAQ](FAQ.md).
|
||||
|
||||
## Connect to A better Route Planner
|
||||
You can connect the app to ABRP, see [this page](docs/abrp.md)
|
||||
## API documentation
|
||||
The api documentation is described here : [api_spec.md](api_spec.md).
|
||||
You can use all functions from the doc, for example :
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
## Connect to A better Route Planner
|
||||
Thank's to this you will no longer have to edit manually:
|
||||
- Exterior temperature
|
||||
- Autonomy left
|
||||
- Consumption
|
||||
|
||||
ABRP will optimize your itinerary with these parameters.
|
||||
|
||||
### Prerequisite
|
||||
1. A working last version of psa_car_controller
|
||||
2. A abetterrouteplanner account
|
||||
3. label must be set in cars.json
|
||||
|
||||
### Procedure
|
||||
1. Go to [https://abetterrouteplanner.com/](https://abetterrouteplanner.com/)
|
||||
2. Edit the parameter of your car, you should have something like this:
|
||||

|
||||
3. Click on Generic Link
|
||||
4. Click on copy
|
||||
5. Open the following url after replacing YOURTOKEN by the value you just copied:
|
||||
```
|
||||
http://localhost:5000/abrp?token=YOURTOKEN
|
||||
```
|
||||
7. Go to [http://localhost:5000](http://localhost:5000)
|
||||
8. Click on control tab
|
||||
9. Enable ABRP for your car
|
||||
10. Open cars.json
|
||||
11. If abrp_name is null:
|
||||
|
||||
11.1 Get the name from this list : [model list](https://api.iternio.com/1/tlm/get_carmodels_list?api_key=32b2162f-9599-4647-8139-66e9f9528370)
|
||||
|
||||
11.2 stop the psa_car_controller
|
||||
|
||||
11.3 replace null with the correct name
|
||||
|
||||
11.4 save the file
|
||||
|
||||
11.5 restart psa_car_controller
|
||||
12. Enjoy
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -0,0 +1,55 @@
|
||||
import re
|
||||
|
||||
from MyLogger import logger
|
||||
|
||||
DEFAULT_BATTERY_POWER = 46
|
||||
DEFAULT_FUEL_CAPACITY = 0
|
||||
DEFAULT_MAX_ELEC_CONSUMPTION = 70
|
||||
DEFAULT_MAX_FUEL_CONSUMPTION = 30
|
||||
|
||||
|
||||
class CarModel:
|
||||
def __init__(self, name, battery_power, fuel_capacity, abrp_name=None, reg=None,
|
||||
max_elec_consumption=DEFAULT_MAX_ELEC_CONSUMPTION, max_fuel_consumption=DEFAULT_MAX_FUEL_CONSUMPTION):
|
||||
self.name = name
|
||||
self.battery_power = battery_power
|
||||
self.fuel_capacity = fuel_capacity
|
||||
self.abrp_name = abrp_name
|
||||
self.reg = reg
|
||||
self.max_elec_consumption = max_elec_consumption
|
||||
self.max_fuel_consumption = max_fuel_consumption
|
||||
|
||||
def match(self, vin):
|
||||
return re.match(self.reg, vin) is not None
|
||||
|
||||
@staticmethod
|
||||
def find_model_by_vin(vin):
|
||||
for carmodel in carmodels:
|
||||
if carmodel.match(vin):
|
||||
return carmodel
|
||||
logger.warning("Can't get car model, please report an issue on github with your car model"
|
||||
" and first ten letter of your VIN")
|
||||
return CarModel("unknown", DEFAULT_BATTERY_POWER, DEFAULT_FUEL_CAPACITY)
|
||||
|
||||
@staticmethod
|
||||
def find_model_by_name(name):
|
||||
for carmodel in carmodels:
|
||||
if carmodel.name == name:
|
||||
return carmodel
|
||||
return None
|
||||
|
||||
|
||||
class ElecModel(CarModel):
|
||||
def __init__(self, name, battery_power, abrp_name=None, reg=None,
|
||||
max_elec_consumption=DEFAULT_MAX_ELEC_CONSUMPTION):
|
||||
super().__init__(name, battery_power, 0, abrp_name, reg, max_elec_consumption, 0)
|
||||
|
||||
|
||||
carmodels = [
|
||||
ElecModel("e-208", 46, "peugeot:e208:20:50", r"VR3UHZKX.*"),
|
||||
ElecModel("e-2008", 46, "peugeot:e2008:20:48", r"VR3UKZKX.*"),
|
||||
ElecModel("e-Spacetourer", 46, "peugeot:etraveler:21:50:citroen", r"VF7VZZKX.*"),
|
||||
ElecModel("corsa-e", 46, "opel:corsae:20:50", r"VXKUHZKX.*"),
|
||||
CarModel("SUV 3008", 10.8, 43),
|
||||
CarModel("C5 Aircross", 10.8, 43)
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
from MyLogger import logger
|
||||
from psa_connectedcar import Position, Geometry, PositionProperties, Kinetic, Energy, EnergyCharging, Status
|
||||
|
||||
|
||||
class CarStatus(Status):
|
||||
def __init__(self, embedded=None, links=None, battery=None, doors_state=None, energy=None, environment=None,
|
||||
ignition=None, kinetic=None, last_position=None, preconditionning=None, privacy=None, safety=None,
|
||||
service=None, timed_odometer=None): # noqa: E501
|
||||
super().__init__(embedded, links, battery, doors_state, energy, environment, ignition, kinetic, last_position,
|
||||
preconditionning, privacy, safety, service, timed_odometer)
|
||||
self.correct()
|
||||
|
||||
def correct(self):
|
||||
try:
|
||||
if len(self.last_position.geometry.coordinates) < 2:
|
||||
raise AttributeError()
|
||||
if len(self.last_position.geometry.coordinates) < 3:
|
||||
# set altitude none
|
||||
self.last_position.geometry.coordinates.append(None)
|
||||
except AttributeError:
|
||||
self.last_position = Position(geometry=Geometry(coordinates=[None, None, None], type="Point"),
|
||||
properties=PositionProperties(updated_at=None))
|
||||
if self.kinetic is None:
|
||||
self.kinetic = Kinetic()
|
||||
|
||||
def is_moving(self):
|
||||
try:
|
||||
return self.kinetic.moving
|
||||
except AttributeError:
|
||||
logger.error("kinetic not available from api")
|
||||
return None
|
||||
|
||||
def get_energy(self, energy_type) -> Energy:
|
||||
for energy in self._energy:
|
||||
if energy.type == energy_type:
|
||||
return energy
|
||||
return Energy(charging=EnergyCharging())
|
||||
@@ -189,7 +189,6 @@ class Otp:
|
||||
return False
|
||||
|
||||
def activation_finalyze(self, random_bytes=None):
|
||||
|
||||
R = self.get_r()
|
||||
params = {"action": "ActionFinalize", "mode": self.mode, "id": self.data.iwid, "lastsync": self.data.iwTsync,
|
||||
"version": "Generator-1.0/0.2.11",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
paho-mqtt>=1.5.0
|
||||
dash>=1.18.0
|
||||
dash_daq
|
||||
plotly>=4
|
||||
cryptography>=3.0
|
||||
Werkzeug>=1.0.0
|
||||
|
||||
@@ -61,7 +61,7 @@ if __name__ == "__main__":
|
||||
logger.info("offline mode")
|
||||
else:
|
||||
try:
|
||||
web.app.myp.manager._refresh_token()
|
||||
web.app.myp.refresh_token()
|
||||
except OAuthError:
|
||||
if args.mail and args.password:
|
||||
client_email = args.mail
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import json
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
from Car import Car
|
||||
from MyLogger import logger
|
||||
|
||||
|
||||
class Abrp:
|
||||
api_key = "1e28ad14-df16-49f0-97da-364c9154b44a"
|
||||
url = "https://api.iternio.com/1/tlm/send"
|
||||
|
||||
def __init__(self, token: str = "", abrp_enable_vin=None):
|
||||
if abrp_enable_vin is None:
|
||||
abrp_enable_vin = list()
|
||||
self.token = token
|
||||
self.abrp_enable_vin = set(abrp_enable_vin)
|
||||
self.proxies = None
|
||||
|
||||
def call(self, car: Car, ext_temp: float = None):
|
||||
try:
|
||||
if self.token is None or len(self.token) == 0:
|
||||
logger.error("No token provided")
|
||||
elif car.vin in self.abrp_enable_vin:
|
||||
energy = car.status.get_energy('Electric')
|
||||
tlm = {"utc": int(datetime.timestamp(energy.updated_at)),
|
||||
"soc": energy.level,
|
||||
"speed": getattr(car.status.kinetic, "speed", None),
|
||||
"car_model": car.get_abrp_name(),
|
||||
"current": car.status.battery.current,
|
||||
"is_charging": energy.charging.status == "InProgress",
|
||||
"lat": car.status.last_position.geometry.coordinates[1],
|
||||
"lon": car.status.last_position.geometry.coordinates[0],
|
||||
"power": energy.consumption
|
||||
}
|
||||
if ext_temp is not None:
|
||||
tlm["ext_temp"] = ext_temp
|
||||
params = {"tlm": json.dumps(tlm), "token": self.token, "api_key": self.api_key}
|
||||
response = requests.request("POST", self.url, params=params, proxies=self.proxies,
|
||||
verify=self.proxies is None)
|
||||
logger.debug(response.text)
|
||||
return response.json()["status"] == "ok"
|
||||
except (AttributeError, IndexError, ValueError):
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
def __iter__(self):
|
||||
yield "abrp_enable_vin", list(self.abrp_enable_vin)
|
||||
yield "token", self.token
|
||||
@@ -46,3 +46,12 @@ def clean_position(conn):
|
||||
logger.debug("Delete duplicate line")
|
||||
conn.execute("DELETE FROM position where Timestamp=?;", (res[1]["Timestamp"],))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_last_temp(vin):
|
||||
conn = get_db()
|
||||
res = conn.execute("SELECT temperature FROM position WHERE VIN=? ORDER BY Timestamp DESC limit 1",
|
||||
(vin,)).fetchone()
|
||||
if res is None:
|
||||
return None
|
||||
return res[0]
|
||||
|
||||
+2
-2
@@ -173,10 +173,10 @@ def get_figures(trips: Trips, charging: Tuple[dict]):
|
||||
y=consumption_by_temp_df["consumption_km"], name="Trips"))
|
||||
consumption_fig_by_temp.update_layout(xaxis_title="average temperature in °C",
|
||||
yaxis_title="Consumption kWh/100Km")
|
||||
consumption_graph_by_temp = Graph(figure=consumption_fig_by_temp, id="consumption_fig_by_temp")
|
||||
consumption_graph_by_temp = html.Div(Graph(figure=consumption_fig_by_temp), id="consumption_graph_by_temp")
|
||||
|
||||
else:
|
||||
consumption_graph_by_temp = Graph(style={'display': 'none'})
|
||||
consumption_graph_by_temp = html.Div(Graph(style={'display': 'none'}), id="consumption_graph_by_temp")
|
||||
|
||||
|
||||
def __calculate_co2_per_kw(charging_data):
|
||||
|
||||
+59
-4
@@ -2,9 +2,10 @@ import json
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
import dash_bootstrap_components as dbc
|
||||
from dash.dependencies import Output, Input
|
||||
from dash.dependencies import Output, Input, MATCH
|
||||
import dash_core_components as dcc
|
||||
import dash_html_components as html
|
||||
import dash_daq as daq
|
||||
|
||||
from MyLogger import logger
|
||||
from flask import jsonify, request, Response as FlaskResponse
|
||||
@@ -16,6 +17,12 @@ from web import figures
|
||||
from web.app import app, dash_app, myp, chc
|
||||
import web.db
|
||||
|
||||
RESPONSE = "-response"
|
||||
|
||||
EMPTY_DIV = "empty-div"
|
||||
|
||||
ABRP_SWITCH = 'abrp-switch'
|
||||
|
||||
ERROR_DIV = dbc.Alert("No data to show, there is probably no trips recorded yet", color="danger")
|
||||
trips: Trips
|
||||
chargings: dict
|
||||
@@ -25,7 +32,7 @@ min_date = max_date = min_millis = max_millis = step = marks = cached_layout = N
|
||||
@dash_app.callback(Output('trips_map', 'figure'),
|
||||
Output('consumption_fig', 'figure'),
|
||||
Output('consumption_fig_by_speed', 'figure'),
|
||||
Output('consumption_fig_by_temp', 'graph'),
|
||||
Output('consumption_graph_by_temp', 'children'),
|
||||
Output('consumption', 'children'),
|
||||
Output('tab_trips', 'children'),
|
||||
Output('tab_battery', 'children'),
|
||||
@@ -49,6 +56,19 @@ def display_value(value):
|
||||
figures.battery_table, max_millis, step, marks
|
||||
|
||||
|
||||
@dash_app.callback(Output({'role': ABRP_SWITCH + RESPONSE, 'vin': MATCH}, 'children'),
|
||||
Input({'role': ABRP_SWITCH, 'vin': MATCH}, 'id'),
|
||||
Input({'role': ABRP_SWITCH, 'vin': MATCH}, 'value'))
|
||||
def update_abrp(div_id, value):
|
||||
vin = div_id["vin"]
|
||||
if value:
|
||||
myp.abrp.abrp_enable_vin.add(vin)
|
||||
else:
|
||||
myp.abrp.abrp_enable_vin.discard(vin)
|
||||
myp.save_config()
|
||||
return " "
|
||||
|
||||
|
||||
@app.route('/get_vehicles')
|
||||
def get_vehicules():
|
||||
response = app.response_class(
|
||||
@@ -127,6 +147,21 @@ def get_recorded_position():
|
||||
return FlaskResponse(myp.get_recorded_position(), mimetype='application/json')
|
||||
|
||||
|
||||
@app.route('/abrp')
|
||||
def abrp():
|
||||
vin = request.args.get('vin', None)
|
||||
enable = request.args.get('enable', None)
|
||||
token = request.args.get('token', None)
|
||||
if vin is not None and enable is not None:
|
||||
if enable == '1':
|
||||
myp.abrp.abrp_enable_vin.add(vin)
|
||||
else:
|
||||
myp.abrp.abrp_enable_vin.discard(vin)
|
||||
if token is not None:
|
||||
myp.abrp.token = token
|
||||
return jsonify(dict(myp.abrp))
|
||||
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
header = response.headers
|
||||
@@ -160,6 +195,25 @@ def update_trips():
|
||||
logger.error("update_trips (slider): %s", traceback.format_exc())
|
||||
return
|
||||
|
||||
|
||||
def __get_control_tabs():
|
||||
tabs = []
|
||||
for car in myp.vehicles_list:
|
||||
if car.label is None:
|
||||
label = car.vin
|
||||
else:
|
||||
label = car.label
|
||||
tabs.append(dbc.Tab(label=label, id="tab-" + car.vin, children=[
|
||||
daq.ToggleSwitch(
|
||||
id={'role': ABRP_SWITCH, 'vin': car.vin},
|
||||
value=car.vin in myp.abrp.abrp_enable_vin,
|
||||
label="Send data to ABRP"
|
||||
),
|
||||
html.Div(id={'role': ABRP_SWITCH + RESPONSE, 'vin': car.vin})
|
||||
]))
|
||||
return tabs
|
||||
|
||||
|
||||
def serve_layout():
|
||||
global cached_layout
|
||||
if cached_layout is None:
|
||||
@@ -188,13 +242,14 @@ def serve_layout():
|
||||
dbc.Tab(label="Charge", tab_id="charge", id="tab_charge", children=[figures.battery_table]),
|
||||
dbc.Tab(label="Map", tab_id="map", children=[
|
||||
dcc.Graph(figure=figures.trips_map, id="trips_map", style={"height": '90vh'})]),
|
||||
dbc.Tab(label="Control", tab_id="control", children=dbc.Tabs(id="control-tabs",
|
||||
children=__get_control_tabs()))
|
||||
],
|
||||
id="tabs",
|
||||
active_tab="summary",
|
||||
),
|
||||
html.Div(id="tab-content", className="p-4"),
|
||||
html.Div(id=EMPTY_DIV),
|
||||
])])
|
||||
|
||||
except (IndexError, TypeError, NameError):
|
||||
logger.warning("Failed to generate figure, there is probably not enough data yet")
|
||||
logger.debug(traceback.format_exc())
|
||||
|
||||
Reference in New Issue
Block a user