carconnectivity-plugin-database 0.1a19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- carconnectivity_database/__init__.py +0 -0
- carconnectivity_database/carconnectivity_database_base.py +26 -0
- carconnectivity_plugin_database-0.1a19.dist-info/METADATA +74 -0
- carconnectivity_plugin_database-0.1a19.dist-info/RECORD +48 -0
- carconnectivity_plugin_database-0.1a19.dist-info/WHEEL +5 -0
- carconnectivity_plugin_database-0.1a19.dist-info/entry_points.txt +2 -0
- carconnectivity_plugin_database-0.1a19.dist-info/licenses/LICENSE +21 -0
- carconnectivity_plugin_database-0.1a19.dist-info/top_level.txt +2 -0
- carconnectivity_plugins/database/__init__.py +0 -0
- carconnectivity_plugins/database/_version.py +34 -0
- carconnectivity_plugins/database/agents/base_agent.py +2 -0
- carconnectivity_plugins/database/agents/charging_agent.py +523 -0
- carconnectivity_plugins/database/agents/climatization_agent.py +80 -0
- carconnectivity_plugins/database/agents/drive_state_agent.py +368 -0
- carconnectivity_plugins/database/agents/state_agent.py +162 -0
- carconnectivity_plugins/database/agents/trip_agent.py +225 -0
- carconnectivity_plugins/database/model/__init__.py +19 -0
- carconnectivity_plugins/database/model/alembic.ini +100 -0
- carconnectivity_plugins/database/model/base.py +4 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/README +1 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/__init__.py +0 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/env.py +78 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/script.py.mako +24 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/versions/__init__.py +0 -0
- carconnectivity_plugins/database/model/charging_power.py +52 -0
- carconnectivity_plugins/database/model/charging_rate.py +52 -0
- carconnectivity_plugins/database/model/charging_session.py +144 -0
- carconnectivity_plugins/database/model/charging_state.py +57 -0
- carconnectivity_plugins/database/model/charging_station.py +66 -0
- carconnectivity_plugins/database/model/climatization_state.py +57 -0
- carconnectivity_plugins/database/model/connection_state.py +57 -0
- carconnectivity_plugins/database/model/datetime_decorator.py +44 -0
- carconnectivity_plugins/database/model/drive.py +89 -0
- carconnectivity_plugins/database/model/drive_consumption.py +48 -0
- carconnectivity_plugins/database/model/drive_level.py +50 -0
- carconnectivity_plugins/database/model/drive_range.py +53 -0
- carconnectivity_plugins/database/model/drive_range_full.py +53 -0
- carconnectivity_plugins/database/model/location.py +85 -0
- carconnectivity_plugins/database/model/migrations.py +24 -0
- carconnectivity_plugins/database/model/outside_temperature.py +52 -0
- carconnectivity_plugins/database/model/state.py +56 -0
- carconnectivity_plugins/database/model/tag.py +31 -0
- carconnectivity_plugins/database/model/timedelta_decorator.py +33 -0
- carconnectivity_plugins/database/model/trip.py +85 -0
- carconnectivity_plugins/database/model/vehicle.py +186 -0
- carconnectivity_plugins/database/plugin.py +167 -0
- carconnectivity_plugins/database/ui/plugin_ui.py +48 -0
- carconnectivity_plugins/database/ui/templates/database/status.html +8 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime, timezone, timedelta
|
|
8
|
+
|
|
9
|
+
from sqlalchemy.exc import DatabaseError
|
|
10
|
+
|
|
11
|
+
from carconnectivity.observable import Observable
|
|
12
|
+
from carconnectivity.vehicle import GenericVehicle
|
|
13
|
+
|
|
14
|
+
from carconnectivity_plugins.database.agents.base_agent import BaseAgent
|
|
15
|
+
from carconnectivity_plugins.database.model.trip import Trip
|
|
16
|
+
from carconnectivity_plugins.database.model.location import Location
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from typing import Optional
|
|
20
|
+
from sqlalchemy.orm import scoped_session
|
|
21
|
+
from sqlalchemy.orm.session import Session
|
|
22
|
+
|
|
23
|
+
from carconnectivity.attributes import EnumAttribute, FloatAttribute
|
|
24
|
+
|
|
25
|
+
from carconnectivity_plugins.database.plugin import Plugin
|
|
26
|
+
from carconnectivity_plugins.database.model.vehicle import Vehicle
|
|
27
|
+
|
|
28
|
+
LOG: logging.Logger = logging.getLogger("carconnectivity.plugins.database.agents.trip_agent")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TripAgent(BaseAgent):
|
|
32
|
+
"""
|
|
33
|
+
Agent responsible for tracking vehicle trips based on vehicle state changes.
|
|
34
|
+
This agent monitors the vehicle's state (ignition on/off, driving) and automatically
|
|
35
|
+
creates and manages trip records in the database. It tracks trip start/end times and
|
|
36
|
+
odometer readings.
|
|
37
|
+
Attributes:
|
|
38
|
+
session (Session): SQLAlchemy database session for persisting trip data.
|
|
39
|
+
vehicle (Vehicle): The vehicle being monitored for trip tracking.
|
|
40
|
+
last_carconnectivity_state (Optional[GenericVehicle.State]): The last known state
|
|
41
|
+
of the vehicle to detect state transitions.
|
|
42
|
+
trip (Optional[Trip]): The currently active trip, if any.
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If vehicle or vehicle.carconnectivity_vehicle is None during initialization.
|
|
45
|
+
Notes:
|
|
46
|
+
- A new trip is started when the vehicle transitions to IGNITION_ON or DRIVING state.
|
|
47
|
+
- A trip is ended when the vehicle transitions from IGNITION_ON/DRIVING to another state.
|
|
48
|
+
- If a previous trip is still open during startup or when starting a new trip,
|
|
49
|
+
it will be logged and closed.
|
|
50
|
+
- Trip records include start/end dates and odometer readings when available.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, database_plugin: Plugin, session_factory: scoped_session[Session], vehicle: Vehicle) -> None:
|
|
54
|
+
if vehicle is None or vehicle.carconnectivity_vehicle is None:
|
|
55
|
+
raise ValueError("Vehicle or its carconnectivity_vehicle attribute is None")
|
|
56
|
+
self.database_plugin: Plugin = database_plugin
|
|
57
|
+
self.session_factory: scoped_session[Session] = session_factory
|
|
58
|
+
self.vehicle: Vehicle = vehicle
|
|
59
|
+
self.last_carconnectivity_state: Optional[GenericVehicle.State] = None
|
|
60
|
+
self.last_parked_position_latitude: Optional[float] = None
|
|
61
|
+
self.last_parked_position_longitude: Optional[float] = None
|
|
62
|
+
self.last_parked_position_time: Optional[datetime] = None
|
|
63
|
+
|
|
64
|
+
with self.session_factory() as session:
|
|
65
|
+
self.trip: Optional[Trip] = session.query(Trip).filter(Trip.vehicle == vehicle).order_by(Trip.start_date.desc()).first()
|
|
66
|
+
self.trip_lock: threading.RLock = threading.RLock()
|
|
67
|
+
if self.trip is not None:
|
|
68
|
+
if self.trip.destination_date is None:
|
|
69
|
+
LOG.info("Last trip for vehicle %s is still open during startup, closing it now", vehicle.vin)
|
|
70
|
+
self.session_factory.remove()
|
|
71
|
+
|
|
72
|
+
vehicle.carconnectivity_vehicle.state.add_observer(self.__on_state_change, Observable.ObserverEvent.UPDATED)
|
|
73
|
+
self.__on_state_change(vehicle.carconnectivity_vehicle.state, Observable.ObserverEvent.UPDATED)
|
|
74
|
+
|
|
75
|
+
vehicle.carconnectivity_vehicle.position.latitude.add_observer(self._on_position_latitude_change, Observable.ObserverEvent.UPDATED)
|
|
76
|
+
self._on_position_latitude_change(vehicle.carconnectivity_vehicle.position.latitude, Observable.ObserverEvent.UPDATED)
|
|
77
|
+
|
|
78
|
+
vehicle.carconnectivity_vehicle.position.longitude.add_observer(self._on_position_longitude_change, Observable.ObserverEvent.UPDATED)
|
|
79
|
+
self._on_position_longitude_change(vehicle.carconnectivity_vehicle.position.longitude, Observable.ObserverEvent.UPDATED)
|
|
80
|
+
|
|
81
|
+
def __on_state_change(self, element: EnumAttribute[GenericVehicle.State], flags: Observable.ObserverEvent) -> None:
|
|
82
|
+
del flags
|
|
83
|
+
if element.enabled:
|
|
84
|
+
if self.vehicle.carconnectivity_vehicle is None:
|
|
85
|
+
raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
|
|
86
|
+
if element.enabled and element.value is not None:
|
|
87
|
+
if self.last_carconnectivity_state is not None:
|
|
88
|
+
with self.session_factory() as session:
|
|
89
|
+
with self.trip_lock:
|
|
90
|
+
if self.trip is not None:
|
|
91
|
+
self.trip = session.merge(self.trip)
|
|
92
|
+
session.refresh(self.trip)
|
|
93
|
+
if self.last_carconnectivity_state not in (GenericVehicle.State.IGNITION_ON, GenericVehicle.State.DRIVING) \
|
|
94
|
+
and element.value in (GenericVehicle.State.IGNITION_ON, GenericVehicle.State.DRIVING):
|
|
95
|
+
if self.trip is not None:
|
|
96
|
+
LOG.warning("Starting new trip for vehicle %s while previous trip is still open, closing previous trip first",
|
|
97
|
+
self.vehicle.vin)
|
|
98
|
+
self.trip = None
|
|
99
|
+
LOG.info("Starting new trip for vehicle %s", self.vehicle.vin)
|
|
100
|
+
start_date: datetime = element.last_updated if element.last_updated is not None else datetime.now(tz=timezone.utc)
|
|
101
|
+
new_trip: Trip = Trip(vin=self.vehicle.vin, start_date=start_date)
|
|
102
|
+
if self.vehicle.carconnectivity_vehicle.odometer.enabled and \
|
|
103
|
+
self.vehicle.carconnectivity_vehicle.odometer.value is not None:
|
|
104
|
+
new_trip.start_odometer = self.vehicle.carconnectivity_vehicle.odometer.value
|
|
105
|
+
if not self._update_trip_position(session=session, trip=new_trip, start=True):
|
|
106
|
+
# if now no position is available try the last known position that is not older than 5min
|
|
107
|
+
if self.last_parked_position_latitude is not None and self.last_parked_position_longitude is not None \
|
|
108
|
+
and self.last_parked_position_time is not None \
|
|
109
|
+
and self.last_parked_position_time < (start_date - timedelta(minutes=5)):
|
|
110
|
+
self._update_trip_position(session=session, trip=new_trip, start=True,
|
|
111
|
+
latitude=self.last_parked_position_latitude,
|
|
112
|
+
longitude=self.last_parked_position_longitude)
|
|
113
|
+
try:
|
|
114
|
+
session.add(new_trip)
|
|
115
|
+
session.commit()
|
|
116
|
+
LOG.debug('Added new trip for vehicle %s to database', self.vehicle.vin)
|
|
117
|
+
self.trip = new_trip
|
|
118
|
+
except DatabaseError as err:
|
|
119
|
+
session.rollback()
|
|
120
|
+
LOG.error('DatabaseError while adding trip for vehicle %s to database: %s', self.vehicle.vin, err)
|
|
121
|
+
self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
122
|
+
elif self.last_carconnectivity_state in (GenericVehicle.State.IGNITION_ON, GenericVehicle.State.DRIVING) \
|
|
123
|
+
and element.value not in (GenericVehicle.State.IGNITION_ON, GenericVehicle.State.DRIVING):
|
|
124
|
+
if self.trip is not None and not self.trip.is_completed():
|
|
125
|
+
LOG.info("Ending trip for vehicle %s", self.vehicle.vin)
|
|
126
|
+
try:
|
|
127
|
+
self.trip.destination_date = element.last_updated if element.last_updated is not None else datetime.now(tz=timezone.utc)
|
|
128
|
+
if self.vehicle.carconnectivity_vehicle.odometer.enabled and \
|
|
129
|
+
self.vehicle.carconnectivity_vehicle.odometer.value is not None:
|
|
130
|
+
self.trip.destination_odometer = self.vehicle.carconnectivity_vehicle.odometer.value
|
|
131
|
+
LOG.debug('Set destination odometer %.2f for trip of vehicle %s', self.trip.destination_odometer, self.vehicle.vin)
|
|
132
|
+
if self._update_trip_position(session=session, trip=self.trip, start=False):
|
|
133
|
+
self.trip = None
|
|
134
|
+
session.commit()
|
|
135
|
+
except DatabaseError as err:
|
|
136
|
+
session.rollback()
|
|
137
|
+
LOG.error('DatabaseError while ending trip for vehicle %s in database: %s', self.vehicle.vin, err)
|
|
138
|
+
self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
139
|
+
self.session_factory.remove()
|
|
140
|
+
self.last_carconnectivity_state = element.value
|
|
141
|
+
|
|
142
|
+
def _on_position_latitude_change(self, element: FloatAttribute, flags: Observable.ObserverEvent) -> None:
|
|
143
|
+
del flags
|
|
144
|
+
if element.enabled and element.value is not None:
|
|
145
|
+
self.last_parked_position_latitude = element.value
|
|
146
|
+
self.last_parked_position_time = element.last_changed
|
|
147
|
+
self._on_position_change()
|
|
148
|
+
|
|
149
|
+
def _on_position_longitude_change(self, element: FloatAttribute, flags: Observable.ObserverEvent) -> None:
|
|
150
|
+
del flags
|
|
151
|
+
if element.enabled and element.value is not None:
|
|
152
|
+
self.last_parked_position_longitude = element.value
|
|
153
|
+
self.last_parked_position_time = element.last_changed
|
|
154
|
+
self._on_position_change()
|
|
155
|
+
|
|
156
|
+
def _on_position_change(self) -> None:
|
|
157
|
+
# Check if there is a finished trip that lacks destination position. We allow 5min after destination_date to set the position.
|
|
158
|
+
if self.trip is not None:
|
|
159
|
+
with self.session_factory() as session:
|
|
160
|
+
with self.trip_lock:
|
|
161
|
+
self.trip = session.merge(self.trip)
|
|
162
|
+
session.refresh(self.trip)
|
|
163
|
+
if self.trip.destination_date is not None and self.trip.destination_position_latitude is None \
|
|
164
|
+
and self.last_parked_position_time is not None \
|
|
165
|
+
and self.last_parked_position_time < (self.trip.destination_date + timedelta(minutes=5)):
|
|
166
|
+
self._update_trip_position(session, self.trip, start=False,
|
|
167
|
+
latitude=self.last_parked_position_latitude,
|
|
168
|
+
longitude=self.last_parked_position_longitude)
|
|
169
|
+
self.session_factory.remove()
|
|
170
|
+
|
|
171
|
+
def _update_trip_position(self, session: Session, trip: Trip, start: bool,
|
|
172
|
+
latitude: Optional[float] = None, longitude: Optional[float] = None) -> bool:
|
|
173
|
+
if self.vehicle.carconnectivity_vehicle is None:
|
|
174
|
+
raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
|
|
175
|
+
if latitude or longitude is None:
|
|
176
|
+
if self.vehicle.carconnectivity_vehicle.position.enabled and self.vehicle.carconnectivity_vehicle.position.latitude.enabled \
|
|
177
|
+
and self.vehicle.carconnectivity_vehicle.position.longitude.enabled \
|
|
178
|
+
and self.vehicle.carconnectivity_vehicle.position.latitude.value is not None \
|
|
179
|
+
and self.vehicle.carconnectivity_vehicle.position.longitude.value is not None:
|
|
180
|
+
latitude = self.vehicle.carconnectivity_vehicle.position.latitude.value
|
|
181
|
+
longitude = self.vehicle.carconnectivity_vehicle.position.longitude.value
|
|
182
|
+
if latitude is not None and longitude is not None:
|
|
183
|
+
if start:
|
|
184
|
+
if trip.start_position_latitude is None and trip.start_position_longitude is None:
|
|
185
|
+
try:
|
|
186
|
+
trip.start_position_latitude = self.vehicle.carconnectivity_vehicle.position.latitude.value
|
|
187
|
+
trip.start_position_longitude = self.vehicle.carconnectivity_vehicle.position.longitude.value
|
|
188
|
+
session.commit()
|
|
189
|
+
except DatabaseError as err:
|
|
190
|
+
session.rollback()
|
|
191
|
+
LOG.error('DatabaseError while updating position for trip of vehicle %s in database: %s', self.vehicle.vin, err)
|
|
192
|
+
self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
193
|
+
if trip.start_location is None and self.vehicle.carconnectivity_vehicle.position.location.enabled:
|
|
194
|
+
location: Location = Location.from_carconnectivity_location(location=self.vehicle.carconnectivity_vehicle.position.location)
|
|
195
|
+
try:
|
|
196
|
+
location = session.merge(location)
|
|
197
|
+
trip.start_location = location
|
|
198
|
+
session.commit()
|
|
199
|
+
except DatabaseError as err:
|
|
200
|
+
session.rollback()
|
|
201
|
+
LOG.error('DatabaseError while merging location for trip of vehicle %s in database: %s', self.vehicle.vin, err)
|
|
202
|
+
self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
203
|
+
return True
|
|
204
|
+
else:
|
|
205
|
+
if trip.destination_position_latitude is None and trip.destination_position_longitude is None:
|
|
206
|
+
try:
|
|
207
|
+
trip.destination_position_latitude = self.vehicle.carconnectivity_vehicle.position.latitude.value
|
|
208
|
+
trip.destination_position_longitude = self.vehicle.carconnectivity_vehicle.position.longitude.value
|
|
209
|
+
session.commit()
|
|
210
|
+
except DatabaseError as err:
|
|
211
|
+
session.rollback()
|
|
212
|
+
LOG.error('DatabaseError while updating position for trip of vehicle %s in database: %s', self.vehicle.vin, err)
|
|
213
|
+
self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
214
|
+
if trip.destination_location is None and self.vehicle.carconnectivity_vehicle.position.location.enabled:
|
|
215
|
+
location: Location = Location.from_carconnectivity_location(location=self.vehicle.carconnectivity_vehicle.position.location)
|
|
216
|
+
try:
|
|
217
|
+
location = session.merge(location)
|
|
218
|
+
trip.destination_location = location
|
|
219
|
+
session.commit()
|
|
220
|
+
except DatabaseError as err:
|
|
221
|
+
session.rollback()
|
|
222
|
+
LOG.error('DatabaseError while merging location for trip of vehicle %s in database: %s', self.vehicle.vin, err)
|
|
223
|
+
self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
224
|
+
return True
|
|
225
|
+
return False
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
""" This module contains the database model for car connectivity plugins."""
|
|
2
|
+
|
|
3
|
+
from .charging_state import ChargingState # noqa: F401
|
|
4
|
+
from .charging_rate import ChargingRate # noqa: F401
|
|
5
|
+
from .charging_power import ChargingPower # noqa: F401
|
|
6
|
+
from .charging_session import ChargingSession # noqa: F401
|
|
7
|
+
from .charging_station import ChargingStation # noqa: F401
|
|
8
|
+
from .climatization_state import ClimatizationState # noqa: F401
|
|
9
|
+
from .connection_state import ConnectionState # noqa: F401
|
|
10
|
+
from .drive import Drive # noqa: F401
|
|
11
|
+
from .drive_level import DriveLevel # noqa: F401
|
|
12
|
+
from .drive_range import DriveRange # noqa: F401
|
|
13
|
+
from .drive_range_full import DriveRangeEstimatedFull # noqa: F401
|
|
14
|
+
from .location import Location # noqa: F401
|
|
15
|
+
from .outside_temperature import OutsideTemperature # noqa: F401
|
|
16
|
+
from .state import State # noqa: F401
|
|
17
|
+
from .tag import Tag # noqa: F401
|
|
18
|
+
from .trip import Trip # noqa: F401
|
|
19
|
+
from .vehicle import Vehicle # noqa: F401
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# A generic, single database configuration.
|
|
2
|
+
|
|
3
|
+
[alembic]
|
|
4
|
+
# path to migration scripts
|
|
5
|
+
script_location = carconnectivity_schema
|
|
6
|
+
|
|
7
|
+
# template used to generate migration files
|
|
8
|
+
# file_template = %%(rev)s_%%(slug)s
|
|
9
|
+
|
|
10
|
+
# sys.path path, will be prepended to sys.path if present.
|
|
11
|
+
# defaults to the current working directory.
|
|
12
|
+
prepend_sys_path = .
|
|
13
|
+
|
|
14
|
+
# timezone to use when rendering the date within the migration file
|
|
15
|
+
# as well as the filename.
|
|
16
|
+
# If specified, requires the python-dateutil library that can be
|
|
17
|
+
# installed by adding `alembic[tz]` to the pip requirements
|
|
18
|
+
# string value is passed to dateutil.tz.gettz()
|
|
19
|
+
# leave blank for localtime
|
|
20
|
+
# timezone =
|
|
21
|
+
|
|
22
|
+
# max length of characters to apply to the
|
|
23
|
+
# "slug" field
|
|
24
|
+
# truncate_slug_length = 40
|
|
25
|
+
|
|
26
|
+
# set to 'true' to run the environment during
|
|
27
|
+
# the 'revision' command, regardless of autogenerate
|
|
28
|
+
# revision_environment = false
|
|
29
|
+
|
|
30
|
+
# set to 'true' to allow .pyc and .pyo files without
|
|
31
|
+
# a source .py file to be detected as revisions in the
|
|
32
|
+
# versions/ directory
|
|
33
|
+
# sourceless = false
|
|
34
|
+
|
|
35
|
+
# version location specification; This defaults
|
|
36
|
+
# to vwsfriend-schema/versions. When using multiple version
|
|
37
|
+
# directories, initial revisions must be specified with --version-path.
|
|
38
|
+
# The path separator used here should be the separator specified by "version_path_separator"
|
|
39
|
+
# version_locations = %(here)s/bar:%(here)s/bat:vwsfriend-schema/versions
|
|
40
|
+
|
|
41
|
+
# version path separator; As mentioned above, this is the character used to split
|
|
42
|
+
# version_locations. Valid values are:
|
|
43
|
+
#
|
|
44
|
+
# version_path_separator = :
|
|
45
|
+
# version_path_separator = ;
|
|
46
|
+
# version_path_separator = space
|
|
47
|
+
version_path_separator = os # default: use os.pathsep
|
|
48
|
+
|
|
49
|
+
# the output encoding used when revision files
|
|
50
|
+
# are written from script.py.mako
|
|
51
|
+
# output_encoding = utf-8
|
|
52
|
+
|
|
53
|
+
sqlalchemy.url = sqlite:///carconnectivity-schema/reference.db
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
[post_write_hooks]
|
|
57
|
+
# post_write_hooks defines scripts or Python functions that are run
|
|
58
|
+
# on newly generated revision scripts. See the documentation for further
|
|
59
|
+
# detail and examples
|
|
60
|
+
|
|
61
|
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
|
62
|
+
# hooks = black
|
|
63
|
+
# black.type = console_scripts
|
|
64
|
+
# black.entrypoint = black
|
|
65
|
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
|
66
|
+
|
|
67
|
+
# Logging configuration
|
|
68
|
+
[loggers]
|
|
69
|
+
keys = root,sqlalchemy,alembic
|
|
70
|
+
|
|
71
|
+
[handlers]
|
|
72
|
+
keys = console
|
|
73
|
+
|
|
74
|
+
[formatters]
|
|
75
|
+
keys = generic
|
|
76
|
+
|
|
77
|
+
[logger_root]
|
|
78
|
+
level = WARN
|
|
79
|
+
handlers = console
|
|
80
|
+
qualname =
|
|
81
|
+
|
|
82
|
+
[logger_sqlalchemy]
|
|
83
|
+
level = WARN
|
|
84
|
+
handlers =
|
|
85
|
+
qualname = sqlalchemy.engine
|
|
86
|
+
|
|
87
|
+
[logger_alembic]
|
|
88
|
+
level = INFO
|
|
89
|
+
handlers =
|
|
90
|
+
qualname = alembic
|
|
91
|
+
|
|
92
|
+
[handler_console]
|
|
93
|
+
class = StreamHandler
|
|
94
|
+
args = (sys.stderr,)
|
|
95
|
+
level = NOTSET
|
|
96
|
+
formatter = generic
|
|
97
|
+
|
|
98
|
+
[formatter_generic]
|
|
99
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
100
|
+
datefmt = %H:%M:%S
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Generic single-database configuration.
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# pylint: skip-file
|
|
2
|
+
from logging.config import fileConfig
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import engine_from_config
|
|
5
|
+
from sqlalchemy import pool
|
|
6
|
+
|
|
7
|
+
from alembic import context
|
|
8
|
+
|
|
9
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
10
|
+
|
|
11
|
+
# this is the Alembic Config object, which provides
|
|
12
|
+
# access to the values within the .ini file in use.
|
|
13
|
+
config = context.config
|
|
14
|
+
|
|
15
|
+
# Interpret the config file for Python logging.
|
|
16
|
+
# This line sets up loggers basically.
|
|
17
|
+
if config.attributes.get('configure_logger', True):
|
|
18
|
+
fileConfig(config.config_file_name)
|
|
19
|
+
|
|
20
|
+
target_metadata = Base.metadata
|
|
21
|
+
|
|
22
|
+
# other values from the config, defined by the needs of env.py,
|
|
23
|
+
# can be acquired:
|
|
24
|
+
# my_important_option = config.get_main_option("my_important_option")
|
|
25
|
+
# ... etc.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run_migrations_offline():
|
|
29
|
+
"""Run migrations in 'offline' mode.
|
|
30
|
+
|
|
31
|
+
This configures the context with just a URL
|
|
32
|
+
and not an Engine, though an Engine is acceptable
|
|
33
|
+
here as well. By skipping the Engine creation
|
|
34
|
+
we don't even need a DBAPI to be available.
|
|
35
|
+
|
|
36
|
+
Calls to context.execute() here emit the given string to the
|
|
37
|
+
script output.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
url = config.get_main_option("sqlalchemy.url")
|
|
41
|
+
context.configure(
|
|
42
|
+
url=url,
|
|
43
|
+
target_metadata=target_metadata,
|
|
44
|
+
literal_binds=True,
|
|
45
|
+
dialect_opts={"paramstyle": "named"},
|
|
46
|
+
compare_type=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
with context.begin_transaction():
|
|
50
|
+
context.run_migrations()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def run_migrations_online():
|
|
54
|
+
"""Run migrations in 'online' mode.
|
|
55
|
+
|
|
56
|
+
In this scenario we need to create an Engine
|
|
57
|
+
and associate a connection with the context.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
connectable = engine_from_config(
|
|
61
|
+
config.get_section(config.config_ini_section),
|
|
62
|
+
prefix="sqlalchemy.",
|
|
63
|
+
poolclass=pool.NullPool,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
with connectable.connect() as connection:
|
|
67
|
+
context.configure(
|
|
68
|
+
connection=connection, target_metadata=target_metadata, compare_type=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
with context.begin_transaction():
|
|
72
|
+
context.run_migrations()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if context.is_offline_mode():
|
|
76
|
+
run_migrations_offline()
|
|
77
|
+
else:
|
|
78
|
+
run_migrations_online()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from alembic import op
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
${imports if imports else ""}
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = ${repr(up_revision)}
|
|
14
|
+
down_revision = ${repr(down_revision)}
|
|
15
|
+
branch_labels = ${repr(branch_labels)}
|
|
16
|
+
depends_on = ${repr(depends_on)}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade():
|
|
20
|
+
${upgrades if upgrades else "pass"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def downgrade():
|
|
24
|
+
${downgrades if downgrades else "pass"}
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
""" This module contains the Vehicle charging power database model"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import ForeignKey, UniqueConstraint
|
|
8
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
9
|
+
|
|
10
|
+
from sqlalchemy_utc import UtcDateTime
|
|
11
|
+
|
|
12
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from sqlalchemy import Constraint
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChargingPower(Base): # pylint: disable=too-few-public-methods
|
|
20
|
+
"""
|
|
21
|
+
SQLAlchemy model representing a vehicle's charging power information over a time period.
|
|
22
|
+
This table stores the power of charging (e.g., in kW) used by a vehicle within
|
|
23
|
+
a specific date range.
|
|
24
|
+
Attributes:
|
|
25
|
+
id (int): Primary key identifier for the charging type record.
|
|
26
|
+
vin (str): Foreign key reference to the vehicle's VIN in the vehicles table.
|
|
27
|
+
vehicle (Vehicle): Relationship to the Vehicle model.
|
|
28
|
+
first_date (datetime): The start date/time of this charging power period.
|
|
29
|
+
last_date (datetime): The end date/time of this charging power period.
|
|
30
|
+
power (float, optional): The charging power value, or None if not available.
|
|
31
|
+
Args:
|
|
32
|
+
vin (str): The vehicle identification number.
|
|
33
|
+
first_date (datetime): The start date/time for this charging power record.
|
|
34
|
+
last_date (datetime): The end date/time for this charging power record.
|
|
35
|
+
power (Optional[float]): The charging power value.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__tablename__: str = 'charging_powers'
|
|
39
|
+
__table_args__: tuple[Constraint] = (UniqueConstraint("vin", "first_date", name="charging_powers_vin_first_date"),)
|
|
40
|
+
|
|
41
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
42
|
+
vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
|
|
43
|
+
vehicle: Mapped["Vehicle"] = relationship("Vehicle")
|
|
44
|
+
first_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
45
|
+
last_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
46
|
+
power: Mapped[Optional[float]]
|
|
47
|
+
|
|
48
|
+
def __init__(self, vin: str, first_date: datetime, last_date: datetime, power: Optional[float]) -> None:
|
|
49
|
+
self.vin = vin
|
|
50
|
+
self.first_date = first_date
|
|
51
|
+
self.last_date = last_date
|
|
52
|
+
self.power = power
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
""" This module contains the Vehicle charging rates database model"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import ForeignKey, UniqueConstraint
|
|
8
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
9
|
+
|
|
10
|
+
from sqlalchemy_utc import UtcDateTime
|
|
11
|
+
|
|
12
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from sqlalchemy import Constraint
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChargingRate(Base): # pylint: disable=too-few-public-methods
|
|
20
|
+
"""
|
|
21
|
+
SQLAlchemy model representing a vehicle's charging type information over a time period.
|
|
22
|
+
This table stores the type of charging (e.g., AC, DC) used by a vehicle within
|
|
23
|
+
a specific date range.
|
|
24
|
+
Attributes:
|
|
25
|
+
id (int): Primary key identifier for the charging type record.
|
|
26
|
+
vin (str): Foreign key reference to the vehicle's VIN in the vehicles table.
|
|
27
|
+
vehicle (Vehicle): Relationship to the Vehicle model.
|
|
28
|
+
first_date (datetime): The start date/time of this charging type period.
|
|
29
|
+
last_date (datetime): The end date/time of this charging type period.
|
|
30
|
+
rate (float, optional): The charging rate value, or None if not available.
|
|
31
|
+
Args:
|
|
32
|
+
vin (str): The vehicle identification number.
|
|
33
|
+
first_date (datetime): The start date/time for this charging type record.
|
|
34
|
+
last_date (datetime): The end date/time for this charging type record.
|
|
35
|
+
rate (Optional[float]): The charging rate value.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__tablename__: str = 'charging_rates'
|
|
39
|
+
__table_args__: tuple[Constraint] = (UniqueConstraint("vin", "first_date", name="charging_rates_vin_first_date"),)
|
|
40
|
+
|
|
41
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
42
|
+
vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
|
|
43
|
+
vehicle: Mapped["Vehicle"] = relationship("Vehicle")
|
|
44
|
+
first_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
45
|
+
last_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
46
|
+
rate: Mapped[Optional[float]]
|
|
47
|
+
|
|
48
|
+
def __init__(self, vin: str, first_date: datetime, last_date: datetime, rate: Optional[float]) -> None:
|
|
49
|
+
self.vin = vin
|
|
50
|
+
self.first_date = first_date
|
|
51
|
+
self.last_date = last_date
|
|
52
|
+
self.rate = rate
|