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.
Files changed (48) hide show
  1. carconnectivity_database/__init__.py +0 -0
  2. carconnectivity_database/carconnectivity_database_base.py +26 -0
  3. carconnectivity_plugin_database-0.1a19.dist-info/METADATA +74 -0
  4. carconnectivity_plugin_database-0.1a19.dist-info/RECORD +48 -0
  5. carconnectivity_plugin_database-0.1a19.dist-info/WHEEL +5 -0
  6. carconnectivity_plugin_database-0.1a19.dist-info/entry_points.txt +2 -0
  7. carconnectivity_plugin_database-0.1a19.dist-info/licenses/LICENSE +21 -0
  8. carconnectivity_plugin_database-0.1a19.dist-info/top_level.txt +2 -0
  9. carconnectivity_plugins/database/__init__.py +0 -0
  10. carconnectivity_plugins/database/_version.py +34 -0
  11. carconnectivity_plugins/database/agents/base_agent.py +2 -0
  12. carconnectivity_plugins/database/agents/charging_agent.py +523 -0
  13. carconnectivity_plugins/database/agents/climatization_agent.py +80 -0
  14. carconnectivity_plugins/database/agents/drive_state_agent.py +368 -0
  15. carconnectivity_plugins/database/agents/state_agent.py +162 -0
  16. carconnectivity_plugins/database/agents/trip_agent.py +225 -0
  17. carconnectivity_plugins/database/model/__init__.py +19 -0
  18. carconnectivity_plugins/database/model/alembic.ini +100 -0
  19. carconnectivity_plugins/database/model/base.py +4 -0
  20. carconnectivity_plugins/database/model/carconnectivity_schema/README +1 -0
  21. carconnectivity_plugins/database/model/carconnectivity_schema/__init__.py +0 -0
  22. carconnectivity_plugins/database/model/carconnectivity_schema/env.py +78 -0
  23. carconnectivity_plugins/database/model/carconnectivity_schema/script.py.mako +24 -0
  24. carconnectivity_plugins/database/model/carconnectivity_schema/versions/__init__.py +0 -0
  25. carconnectivity_plugins/database/model/charging_power.py +52 -0
  26. carconnectivity_plugins/database/model/charging_rate.py +52 -0
  27. carconnectivity_plugins/database/model/charging_session.py +144 -0
  28. carconnectivity_plugins/database/model/charging_state.py +57 -0
  29. carconnectivity_plugins/database/model/charging_station.py +66 -0
  30. carconnectivity_plugins/database/model/climatization_state.py +57 -0
  31. carconnectivity_plugins/database/model/connection_state.py +57 -0
  32. carconnectivity_plugins/database/model/datetime_decorator.py +44 -0
  33. carconnectivity_plugins/database/model/drive.py +89 -0
  34. carconnectivity_plugins/database/model/drive_consumption.py +48 -0
  35. carconnectivity_plugins/database/model/drive_level.py +50 -0
  36. carconnectivity_plugins/database/model/drive_range.py +53 -0
  37. carconnectivity_plugins/database/model/drive_range_full.py +53 -0
  38. carconnectivity_plugins/database/model/location.py +85 -0
  39. carconnectivity_plugins/database/model/migrations.py +24 -0
  40. carconnectivity_plugins/database/model/outside_temperature.py +52 -0
  41. carconnectivity_plugins/database/model/state.py +56 -0
  42. carconnectivity_plugins/database/model/tag.py +31 -0
  43. carconnectivity_plugins/database/model/timedelta_decorator.py +33 -0
  44. carconnectivity_plugins/database/model/trip.py +85 -0
  45. carconnectivity_plugins/database/model/vehicle.py +186 -0
  46. carconnectivity_plugins/database/plugin.py +167 -0
  47. carconnectivity_plugins/database/ui/plugin_ui.py +48 -0
  48. 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"
@@ -0,0 +1,8 @@
1
+ {% extends 'base.html' %}
2
+
3
+ {% block header %}
4
+ <h1>{% block title %}Database Plugin Status {% endblock %}</h1>
5
+ {% endblock %}
6
+
7
+ {% block content %}
8
+ {% endblock %}