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,167 @@
|
|
|
1
|
+
"""Module implements the plugin to connect with Database"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Engine, create_engine, text, inspect
|
|
9
|
+
from sqlalchemy.orm import sessionmaker, scoped_session
|
|
10
|
+
from sqlalchemy.exc import DatabaseError, OperationalError
|
|
11
|
+
from sqlalchemy.orm.session import Session
|
|
12
|
+
|
|
13
|
+
from carconnectivity.errors import ConfigurationError
|
|
14
|
+
from carconnectivity.util import config_remove_credentials
|
|
15
|
+
from carconnectivity.observable import Observable
|
|
16
|
+
from carconnectivity.vehicle import GenericVehicle
|
|
17
|
+
|
|
18
|
+
from carconnectivity_plugins.base.plugin import BasePlugin
|
|
19
|
+
|
|
20
|
+
from carconnectivity_plugins.database._version import __version__
|
|
21
|
+
from carconnectivity_plugins.database.model.migrations import run_database_migrations
|
|
22
|
+
from carconnectivity_plugins.database.model.vehicle import Vehicle
|
|
23
|
+
|
|
24
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from typing import Dict, Optional
|
|
29
|
+
from carconnectivity.carconnectivity import CarConnectivity
|
|
30
|
+
|
|
31
|
+
LOG: logging.Logger = logging.getLogger("carconnectivity.plugins.database")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Plugin(BasePlugin): # pylint: disable=too-many-instance-attributes
|
|
35
|
+
"""
|
|
36
|
+
Plugin class for Database connectivity.
|
|
37
|
+
Args:
|
|
38
|
+
car_connectivity (CarConnectivity): An instance of CarConnectivity.
|
|
39
|
+
config (Dict): Configuration dictionary containing connection details.
|
|
40
|
+
"""
|
|
41
|
+
def __init__(self, plugin_id: str, car_connectivity: CarConnectivity, config: Dict) -> None:
|
|
42
|
+
BasePlugin.__init__(self, plugin_id=plugin_id, car_connectivity=car_connectivity, config=config, log=LOG)
|
|
43
|
+
|
|
44
|
+
self._background_thread: Optional[threading.Thread] = None
|
|
45
|
+
self._stop_event = threading.Event()
|
|
46
|
+
|
|
47
|
+
LOG.info("Loading database plugin with config %s", config_remove_credentials(config))
|
|
48
|
+
|
|
49
|
+
if 'db_url' in config:
|
|
50
|
+
self.active_config['db_url'] = config['db_url']
|
|
51
|
+
else:
|
|
52
|
+
raise ConfigurationError('db_url must be configured in the plugin config')
|
|
53
|
+
|
|
54
|
+
connect_args = {}
|
|
55
|
+
if 'postgresql' in self.active_config['db_url']:
|
|
56
|
+
connect_args['options'] = '-c timezone=utc'
|
|
57
|
+
self.engine: Engine = create_engine(self.active_config['db_url'], pool_pre_ping=True, connect_args=connect_args)
|
|
58
|
+
session_factory: sessionmaker[Session] = sessionmaker(bind=self.engine, autoflush=True)
|
|
59
|
+
self.scoped_session_factory: scoped_session[Session] = scoped_session(session_factory)
|
|
60
|
+
|
|
61
|
+
self.vehicles: Dict[str, Vehicle] = {}
|
|
62
|
+
self.vehicles_lock: threading.RLock = threading.RLock()
|
|
63
|
+
|
|
64
|
+
def startup(self) -> None:
|
|
65
|
+
LOG.info("Starting database plugin")
|
|
66
|
+
self._background_thread = threading.Thread(target=self._background_loop, daemon=False)
|
|
67
|
+
self._background_thread.name = 'carconnectivity.plugins.database-background'
|
|
68
|
+
self._background_thread.start()
|
|
69
|
+
self.healthy._set_value(value=True) # pylint: disable=protected-access
|
|
70
|
+
LOG.debug("Starting Database plugin done")
|
|
71
|
+
return super().startup()
|
|
72
|
+
|
|
73
|
+
def _background_loop(self) -> None:
|
|
74
|
+
self._stop_event.clear()
|
|
75
|
+
first_run: bool = True
|
|
76
|
+
with self.scoped_session_factory() as session:
|
|
77
|
+
while not self._stop_event.is_set():
|
|
78
|
+
try:
|
|
79
|
+
session.execute(text('SELECT 1')).all()
|
|
80
|
+
self.healthy._set_value(value=True) # pylint: disable=protected-access
|
|
81
|
+
if first_run:
|
|
82
|
+
LOG.info('Database connection established successfully')
|
|
83
|
+
first_run = False
|
|
84
|
+
if not inspect(self.engine).has_table("alembic_version"):
|
|
85
|
+
LOG.info('It looks like you have an empty database will create all tables')
|
|
86
|
+
Base.metadata.create_all(self.engine)
|
|
87
|
+
run_database_migrations(dsn=self.active_config['db_url'], stamp_only=True)
|
|
88
|
+
else:
|
|
89
|
+
LOG.info('It looks like you have an existing database will check if an upgrade is necessary')
|
|
90
|
+
Base.metadata.create_all(self.engine) # TODO: remove after some time
|
|
91
|
+
run_database_migrations(dsn=self.active_config['db_url'])
|
|
92
|
+
LOG.info('Database upgrade done')
|
|
93
|
+
session.commit()
|
|
94
|
+
self.car_connectivity.garage.add_observer(self.__on_add_vehicle, flag=Observable.ObserverEvent.ENABLED, on_transaction_end=True)
|
|
95
|
+
with self.vehicles_lock:
|
|
96
|
+
self.vehicles = {vehicle.vin: vehicle for vehicle in session.query(Vehicle).all()}
|
|
97
|
+
for vehicle in self.vehicles.values():
|
|
98
|
+
car_connectivity_vehicle: Optional[GenericVehicle] = self.car_connectivity.garage.get_vehicle(vehicle.vin)
|
|
99
|
+
if car_connectivity_vehicle is not None:
|
|
100
|
+
vehicle.connect(self, self.scoped_session_factory, car_connectivity_vehicle)
|
|
101
|
+
for garage_vehicle in self.car_connectivity.garage.list_vehicles():
|
|
102
|
+
if garage_vehicle.vin.value is not None and garage_vehicle.vin.value not in self.vehicles:
|
|
103
|
+
LOG.debug('New vehicle found in garage during startup: %s', garage_vehicle.vin.value)
|
|
104
|
+
new_vehicle: Vehicle = session.get(Vehicle, garage_vehicle.vin.value)
|
|
105
|
+
if new_vehicle is None:
|
|
106
|
+
new_vehicle: Vehicle = Vehicle(vin=garage_vehicle.vin.value)
|
|
107
|
+
try:
|
|
108
|
+
session.add(new_vehicle)
|
|
109
|
+
session.commit()
|
|
110
|
+
LOG.debug('Added new vehicle %s to database', garage_vehicle.vin.value)
|
|
111
|
+
new_vehicle.connect(self, self.scoped_session_factory, garage_vehicle)
|
|
112
|
+
self.vehicles[garage_vehicle.vin.value] = new_vehicle
|
|
113
|
+
except DatabaseError as err:
|
|
114
|
+
session.rollback()
|
|
115
|
+
LOG.error('DatabaseError while adding vehicle %s to database: %s', garage_vehicle.vin.value, err)
|
|
116
|
+
self.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
117
|
+
else:
|
|
118
|
+
new_vehicle.connect(self, self.scoped_session_factory, garage_vehicle)
|
|
119
|
+
self.vehicles[garage_vehicle.vin.value] = new_vehicle
|
|
120
|
+
|
|
121
|
+
except OperationalError as err:
|
|
122
|
+
LOG.error('Could not establish a connection to database, will try again after 10 seconds: %s', err)
|
|
123
|
+
self.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
124
|
+
self._stop_event.wait(10)
|
|
125
|
+
self.scoped_session_factory.remove()
|
|
126
|
+
|
|
127
|
+
def shutdown(self) -> None:
|
|
128
|
+
self._stop_event.set()
|
|
129
|
+
if self._background_thread is not None:
|
|
130
|
+
self._background_thread.join()
|
|
131
|
+
return super().shutdown()
|
|
132
|
+
|
|
133
|
+
def get_version(self) -> str:
|
|
134
|
+
return __version__
|
|
135
|
+
|
|
136
|
+
def get_type(self) -> str:
|
|
137
|
+
return "carconnectivity-plugin-database"
|
|
138
|
+
|
|
139
|
+
def get_name(self) -> str:
|
|
140
|
+
return "Database Plugin"
|
|
141
|
+
|
|
142
|
+
def __on_add_vehicle(self, element, flags) -> None:
|
|
143
|
+
del flags
|
|
144
|
+
with self.vehicles_lock:
|
|
145
|
+
if isinstance(element, GenericVehicle) and element.vin not in self.vehicles:
|
|
146
|
+
LOG.debug('New vehicle added to garage: %s', element.vin)
|
|
147
|
+
if element.vin.value is not None:
|
|
148
|
+
with self.scoped_session_factory() as session:
|
|
149
|
+
if element.vin.value in self.vehicles:
|
|
150
|
+
vehicle: Vehicle = self.vehicles[element.vin.value]
|
|
151
|
+
vehicle.connect(self, self.scoped_session_factory, element)
|
|
152
|
+
else:
|
|
153
|
+
vehicle: Vehicle = session.get(Vehicle, element.vin.value)
|
|
154
|
+
if vehicle is None:
|
|
155
|
+
vehicle = Vehicle(vin=element.vin.value)
|
|
156
|
+
try:
|
|
157
|
+
session.add(vehicle)
|
|
158
|
+
session.commit()
|
|
159
|
+
vehicle.connect(self, self.scoped_session_factory, element)
|
|
160
|
+
except DatabaseError as err:
|
|
161
|
+
session.rollback()
|
|
162
|
+
LOG.error('DatabaseError while adding vehicle %s to database: %s', element.vin.value, err)
|
|
163
|
+
self.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
164
|
+
else:
|
|
165
|
+
vehicle.connect(self, self.scoped_session_factory, element)
|
|
166
|
+
self.vehicles[element.vin.value] = vehicle
|
|
167
|
+
self.scoped_session_factory.remove()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
""" User interface for the Database plugin in the Car Connectivity application. """
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import flask
|
|
8
|
+
import flask_login
|
|
9
|
+
|
|
10
|
+
from carconnectivity_plugins.base.plugin import BasePlugin
|
|
11
|
+
from carconnectivity_plugins.base.ui.plugin_ui import BasePluginUI
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from typing import Optional, List, Dict, Union, Literal
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PluginUI(BasePluginUI):
|
|
18
|
+
"""
|
|
19
|
+
A user interface class for the Database plugin in the Car Connectivity application.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, plugin: BasePlugin):
|
|
22
|
+
blueprint: Optional[flask.Blueprint] = flask.Blueprint(name=plugin.id, import_name='carconnectivity-plugin-database', url_prefix=f'/{plugin.id}',
|
|
23
|
+
template_folder=os.path.dirname(__file__) + '/templates')
|
|
24
|
+
super().__init__(plugin, blueprint=blueprint)
|
|
25
|
+
|
|
26
|
+
@self.blueprint.route('/', methods=['GET'])
|
|
27
|
+
def root():
|
|
28
|
+
return flask.redirect(flask.url_for('plugins.database.status'))
|
|
29
|
+
|
|
30
|
+
@self.blueprint.route('/status', methods=['GET'])
|
|
31
|
+
@flask_login.login_required
|
|
32
|
+
def status():
|
|
33
|
+
return flask.render_template('database/status.html', current_app=flask.current_app, plugin=self.plugin)
|
|
34
|
+
|
|
35
|
+
def get_nav_items(self) -> List[Dict[Literal['text', 'url', 'sublinks', 'divider'], Union[str, List]]]:
|
|
36
|
+
"""
|
|
37
|
+
Generates a list of navigation items for the Database plugin UI.
|
|
38
|
+
"""
|
|
39
|
+
return super().get_nav_items() + [{"text": "Status", "url": flask.url_for('plugins.database.status')}]
|
|
40
|
+
|
|
41
|
+
def get_title(self) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Returns the title of the plugin.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: The title of the plugin, which is "Database".
|
|
47
|
+
"""
|
|
48
|
+
return "Database"
|