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,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,4 @@
1
+ """Module just storing the Base class for SQLAlchemy models."""
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+
4
+ Base = declarative_base()
@@ -0,0 +1 @@
1
+ Generic single-database configuration.
@@ -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"}
@@ -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