2017-11-12 15:38:32 +01:00
|
|
|
from sqlalchemy import or_
|
|
|
|
import requests
|
2016-04-28 06:32:51 +02:00
|
|
|
import logging
|
2017-11-12 15:38:32 +01:00
|
|
|
from datetime import date, datetime, timedelta
|
2016-04-28 06:32:51 +02:00
|
|
|
|
2017-11-12 15:38:32 +01:00
|
|
|
from .entities import Pitstop, FillingStation
|
|
|
|
from . import db, app
|
2016-04-28 06:32:51 +02:00
|
|
|
|
2016-07-10 13:40:49 +02:00
|
|
|
|
2021-06-17 18:28:19 +02:00
|
|
|
class RegularCostInstance:
|
|
|
|
def __init__(self, regular_id, date, odometer, costs, name):
|
|
|
|
self.id = regular_id
|
|
|
|
self.date = date
|
|
|
|
self.odometer = odometer
|
|
|
|
self.costs = costs
|
|
|
|
self.name = name
|
|
|
|
|
|
|
|
|
2016-07-10 13:40:49 +02:00
|
|
|
class ConsumableStats:
|
|
|
|
def __init__(self, vehicle, consumable):
|
|
|
|
self.name = consumable.name
|
|
|
|
self.id = consumable.id
|
|
|
|
self.unit = consumable.unit
|
2016-07-11 06:59:34 +02:00
|
|
|
self.overall_amount = 0
|
2016-07-10 13:40:49 +02:00
|
|
|
self.average_distance = 0
|
|
|
|
self.average_amount_fuelled = 0
|
|
|
|
self.average_amount_used = 0
|
|
|
|
self.average_amount = []
|
2016-07-11 06:59:34 +02:00
|
|
|
self.amounts = []
|
2016-07-10 13:40:49 +02:00
|
|
|
|
2021-06-17 18:28:19 +02:00
|
|
|
pitstops = [
|
|
|
|
stop for stop in vehicle.pitstops if stop.consumable_id == consumable.id
|
|
|
|
]
|
2016-07-10 13:40:49 +02:00
|
|
|
pitstop_count = len(pitstops)
|
|
|
|
|
|
|
|
if pitstop_count > 0:
|
|
|
|
for pitstop in pitstops:
|
2016-07-11 06:59:34 +02:00
|
|
|
self.overall_amount += pitstop.amount
|
|
|
|
self.amounts.append(StatsEvent(pitstop.date, pitstop.amount))
|
|
|
|
self.average_amount_fuelled = self.overall_amount / pitstop_count
|
2016-07-10 13:40:49 +02:00
|
|
|
if pitstop_count > 1:
|
2021-06-17 18:28:19 +02:00
|
|
|
overall_distance = (
|
|
|
|
vehicle.pitstops[-1].odometer - vehicle.pitstops[0].odometer
|
|
|
|
)
|
2016-07-10 13:40:49 +02:00
|
|
|
self.average_distance = overall_distance / (pitstop_count - 1)
|
2021-06-17 18:28:19 +02:00
|
|
|
self.average_amount_used = (
|
|
|
|
100 * (self.overall_amount - pitstops[0].amount) / overall_distance
|
|
|
|
)
|
2016-07-10 13:40:49 +02:00
|
|
|
for index in range(1, pitstop_count):
|
|
|
|
last_ps = pitstops[index - 1]
|
|
|
|
current_ps = pitstops[index]
|
|
|
|
self.average_amount.append(
|
|
|
|
StatsEvent(
|
|
|
|
current_ps.date,
|
2021-06-17 18:28:19 +02:00
|
|
|
round(
|
|
|
|
100
|
|
|
|
* current_ps.amount
|
|
|
|
/ (current_ps.odometer - last_ps.odometer),
|
|
|
|
2,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
2016-04-28 06:32:51 +02:00
|
|
|
|
|
|
|
|
2016-04-25 22:28:35 +02:00
|
|
|
class VehicleStats:
|
|
|
|
def __init__(self, vehicle):
|
|
|
|
self.name = vehicle.name
|
|
|
|
self.id = vehicle.id
|
|
|
|
self.overall_distance = 0
|
2016-06-21 21:06:41 +02:00
|
|
|
self.overall_costs = 0
|
2016-07-10 13:40:49 +02:00
|
|
|
self.consumables = []
|
|
|
|
self.odometers = []
|
2021-06-17 18:28:19 +02:00
|
|
|
self.costs = []
|
|
|
|
self.costs_per_distance = "N/A"
|
2016-07-10 13:40:49 +02:00
|
|
|
|
|
|
|
for consumable in vehicle.consumables:
|
|
|
|
self.consumables.append(ConsumableStats(vehicle, consumable))
|
|
|
|
|
2016-11-06 13:22:54 +01:00
|
|
|
events = get_event_line_for_vehicle(vehicle)
|
|
|
|
pitstop_count = len(events)
|
2016-07-10 13:40:49 +02:00
|
|
|
|
|
|
|
if pitstop_count > 0:
|
2016-11-06 13:22:54 +01:00
|
|
|
for pitstop in events:
|
|
|
|
self.odometers.append(StatsEvent(pitstop.date, pitstop.odometer))
|
|
|
|
if pitstop.costs is not None:
|
2016-07-16 12:07:01 +02:00
|
|
|
self.overall_costs += pitstop.costs
|
2021-06-17 18:28:19 +02:00
|
|
|
self.costs.append(StatsEvent(pitstop.date, pitstop.costs))
|
|
|
|
|
|
|
|
# add the instances to the overall costs
|
|
|
|
for regular_cost_instance in calculate_regular_cost_instances(vehicle):
|
|
|
|
self.overall_costs += regular_cost_instance.costs
|
|
|
|
self.costs.append(
|
|
|
|
StatsEvent(regular_cost_instance.date, regular_cost_instance.costs)
|
|
|
|
)
|
2016-07-10 13:40:49 +02:00
|
|
|
|
|
|
|
if pitstop_count > 1:
|
2016-11-06 13:22:54 +01:00
|
|
|
self.overall_distance = events[-1].odometer - events[0].odometer
|
2016-04-28 06:32:51 +02:00
|
|
|
|
2021-06-17 18:28:19 +02:00
|
|
|
self.costs.sort(key=lambda x: x.date)
|
|
|
|
accumulated_costs = 0
|
|
|
|
for c in self.costs:
|
|
|
|
accumulated_costs += c.value
|
|
|
|
c.value = accumulated_costs
|
|
|
|
|
|
|
|
if self.overall_distance > 0:
|
|
|
|
self.costs_per_distance = float(self.overall_costs) / (float(self.overall_distance) / 100)
|
|
|
|
|
2016-04-28 06:32:51 +02:00
|
|
|
|
2016-05-02 06:42:43 +02:00
|
|
|
class StatsEvent:
|
|
|
|
def __init__(self, date, value):
|
|
|
|
self.date = date
|
|
|
|
self.value = value
|
|
|
|
|
2021-06-17 18:28:19 +02:00
|
|
|
def __repr__(self):
|
|
|
|
return str(self.date)
|
|
|
|
|
2016-05-02 06:42:43 +02:00
|
|
|
|
2016-04-28 06:32:51 +02:00
|
|
|
def db_log_add(entity):
|
2021-06-17 18:28:19 +02:00
|
|
|
logging.info("db_add: %s" % str(entity))
|
2016-04-28 06:32:51 +02:00
|
|
|
|
|
|
|
|
|
|
|
def db_log_delete(entity):
|
2021-06-17 18:28:19 +02:00
|
|
|
logging.info("db_delete: %s" % str(entity))
|
2016-04-28 06:32:51 +02:00
|
|
|
|
|
|
|
|
|
|
|
def db_log_update(entity):
|
2021-06-17 18:28:19 +02:00
|
|
|
logging.info("db_update: %s" % str(entity))
|
2016-05-16 18:46:24 +02:00
|
|
|
|
|
|
|
|
|
|
|
def check_vehicle_name_is_unique(current_user, name_field):
|
|
|
|
"""
|
|
|
|
Checks if the vehicle name given in the name_field is unique for the vehicles of the current user. An error is added
|
|
|
|
to the field it the name is not unique.
|
|
|
|
|
|
|
|
:param current_user: the user currently logged in
|
|
|
|
:param name_field: the form field to enter the name to
|
|
|
|
:return: True if the name is unique, False otherwise.
|
|
|
|
"""
|
|
|
|
vehicle_name = name_field.data
|
|
|
|
for vehicle in current_user.vehicles:
|
|
|
|
if vehicle.name == vehicle_name:
|
|
|
|
name_field.default = vehicle_name
|
|
|
|
name_field.errors.append('Vehicle "%s" already exists.' % vehicle_name)
|
|
|
|
return False
|
2016-07-04 20:19:59 +02:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def get_latest_pitstop_for_vehicle(vehicle_id):
|
|
|
|
"""
|
|
|
|
return the latest pit stop for the vehicle with the given id.
|
|
|
|
:param vehicle_id: the id of the vehicle
|
|
|
|
:return: the latest pitstop or None if no pitstop exists
|
|
|
|
"""
|
2021-06-17 18:28:19 +02:00
|
|
|
latest_pitstop = (
|
|
|
|
Pitstop.query.filter(Pitstop.vehicle_id == vehicle_id)
|
|
|
|
.order_by(Pitstop.id.desc())
|
2016-07-04 20:19:59 +02:00
|
|
|
.first()
|
2021-06-17 18:28:19 +02:00
|
|
|
)
|
2016-07-04 20:19:59 +02:00
|
|
|
return latest_pitstop
|
|
|
|
|
|
|
|
|
|
|
|
def get_latest_pitstop_for_vehicle_and_consumable(vehicle_id, consumable_id):
|
|
|
|
"""
|
|
|
|
return the latest pit stop for the vehicle and consumable with the given ids.
|
|
|
|
:param vehicle_id: the id of the vehicle
|
|
|
|
:param consumable_id: the id of the consumable
|
|
|
|
:return: the latest pitstop or None if no pitstop exists
|
|
|
|
"""
|
2021-06-17 18:28:19 +02:00
|
|
|
latest_pitstop_consumable = (
|
|
|
|
Pitstop.query.filter(Pitstop.vehicle_id == vehicle_id)
|
|
|
|
.filter(Pitstop.consumable_id == consumable_id)
|
|
|
|
.order_by(Pitstop.id.desc())
|
2016-07-04 20:19:59 +02:00
|
|
|
.first()
|
2021-06-17 18:28:19 +02:00
|
|
|
)
|
2016-07-04 20:19:59 +02:00
|
|
|
return latest_pitstop_consumable
|
|
|
|
|
|
|
|
|
2021-06-17 18:28:19 +02:00
|
|
|
def compute_lower_limits_for_new_pitstop(
|
|
|
|
latest_pitstop, last_pitstop_consumable, consumable_id
|
|
|
|
):
|
2016-07-04 20:19:59 +02:00
|
|
|
"""
|
|
|
|
This function figures out the lower limits for date and odometer of a new pitstop.
|
|
|
|
:param latest_pitstop:
|
|
|
|
:param last_pitstop_consumable:
|
|
|
|
:param consumable_id:
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
odometer = 0
|
|
|
|
date_of_pitstop = date(1970, 1, 1)
|
|
|
|
amount = 0
|
|
|
|
costs = 0
|
|
|
|
|
|
|
|
if latest_pitstop is not None:
|
|
|
|
odometer = latest_pitstop.odometer
|
|
|
|
date_of_pitstop = latest_pitstop.date
|
|
|
|
|
|
|
|
if last_pitstop_consumable is not None:
|
|
|
|
amount = last_pitstop_consumable.amount
|
|
|
|
costs = last_pitstop_consumable.costs
|
|
|
|
|
|
|
|
return Pitstop(odometer, amount, date_of_pitstop, costs, consumable_id)
|
|
|
|
|
|
|
|
|
2016-11-01 19:48:24 +01:00
|
|
|
def pitstop_service_key(x):
|
2021-06-17 18:28:19 +02:00
|
|
|
return x.date, x.odometer
|
2016-11-01 19:48:24 +01:00
|
|
|
|
|
|
|
|
2016-11-06 13:22:54 +01:00
|
|
|
def get_event_line_for_vehicle(vehicle):
|
|
|
|
data = []
|
|
|
|
for pitstop in vehicle.pitstops:
|
|
|
|
data.append(pitstop)
|
|
|
|
for service in vehicle.services:
|
|
|
|
data.append(service)
|
|
|
|
data.sort(key=pitstop_service_key)
|
2017-11-12 15:38:32 +01:00
|
|
|
return data
|
|
|
|
|
|
|
|
|
2017-11-24 09:08:04 +01:00
|
|
|
def chunks(l, n):
|
|
|
|
"""Yield successive n-sized chunks from l."""
|
|
|
|
for i in range(0, len(l), n):
|
2021-06-17 18:28:19 +02:00
|
|
|
yield l[i : i + n]
|
2017-11-24 09:08:04 +01:00
|
|
|
|
|
|
|
|
2017-11-12 15:38:32 +01:00
|
|
|
def update_filling_station_prices(ids):
|
2021-06-17 18:28:19 +02:00
|
|
|
max_age = (datetime.now() - timedelta(minutes=15)).strftime("%Y-%m-%d %H:%M")
|
|
|
|
|
|
|
|
res = (
|
|
|
|
db.session.query(FillingStation)
|
|
|
|
.filter(FillingStation.id.in_(ids))
|
|
|
|
.filter(
|
|
|
|
or_(
|
|
|
|
FillingStation.last_update == None, FillingStation.last_update < max_age
|
|
|
|
)
|
|
|
|
)
|
|
|
|
.all()
|
|
|
|
)
|
2017-11-12 15:38:32 +01:00
|
|
|
|
|
|
|
if len(res) > 0:
|
2017-11-24 09:08:04 +01:00
|
|
|
id_map = {x.id: x for x in res}
|
2017-11-12 15:38:32 +01:00
|
|
|
query_ids = [x.id for x in res]
|
2021-06-17 18:28:19 +02:00
|
|
|
api_key = app.config["TANKERKOENIG_API_KEY"]
|
|
|
|
url = "https://creativecommons.tankerkoenig.de/json/prices.php"
|
2017-11-24 09:08:04 +01:00
|
|
|
|
|
|
|
# documentation tells us to query max 10 filling stations at a time...
|
|
|
|
for c in chunks(query_ids, 10):
|
2021-06-17 18:28:19 +02:00
|
|
|
params = {"apikey": api_key, "ids": ",".join(c)}
|
2017-11-24 09:08:04 +01:00
|
|
|
response = requests.get(url, params=params)
|
|
|
|
response_json = response.json()
|
2021-06-17 18:28:19 +02:00
|
|
|
if response_json["ok"]:
|
|
|
|
prices = response_json["prices"]
|
2017-11-24 09:08:04 +01:00
|
|
|
for price in prices:
|
|
|
|
id = price
|
|
|
|
station_status = prices[id]
|
2021-06-17 18:28:19 +02:00
|
|
|
id_map[id].open = station_status["status"] == "open"
|
2017-11-24 09:08:04 +01:00
|
|
|
if id_map[id].open:
|
2021-06-17 18:28:19 +02:00
|
|
|
id_map[id].diesel = station_status["diesel"]
|
|
|
|
id_map[id].e10 = station_status["e10"]
|
|
|
|
id_map[id].e5 = station_status["e5"]
|
2017-11-24 09:08:04 +01:00
|
|
|
id_map[id].last_update = datetime.now()
|
|
|
|
else:
|
|
|
|
logging.error(
|
2021-06-17 18:28:19 +02:00
|
|
|
"could not update filling stations because of {r} on URL {u}.".format(
|
|
|
|
r=str(response_json), u=response.url
|
|
|
|
)
|
|
|
|
)
|
2017-11-12 15:38:32 +01:00
|
|
|
db.session.commit()
|
2021-06-17 18:28:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
def calculate_regular_cost_instances(vehicle):
|
|
|
|
data = []
|
|
|
|
for regular in vehicle.regulars:
|
|
|
|
if date.today() < regular.start_at:
|
|
|
|
# skip regular costs that are not yet active
|
|
|
|
continue
|
|
|
|
if regular.ends_at:
|
|
|
|
end_date = regular.ends_at
|
|
|
|
else:
|
|
|
|
end_date = date.today()
|
|
|
|
|
|
|
|
start_year = regular.start_at.year
|
|
|
|
end_year = end_date.year
|
|
|
|
|
|
|
|
for year in range(start_year, end_year + 1):
|
|
|
|
for day in regular.days.split(","):
|
|
|
|
m, d = day.split("-")
|
|
|
|
d = date(year, int(m), int(d))
|
|
|
|
if regular.start_at <= d and d <= end_date:
|
|
|
|
r = RegularCostInstance(
|
|
|
|
regular.id,
|
|
|
|
date=d,
|
|
|
|
odometer=None,
|
|
|
|
costs=regular.costs,
|
|
|
|
name=regular.description,
|
|
|
|
)
|
|
|
|
data.append(r)
|
|
|
|
return data
|