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,144 @@
1
+ """ This module contains the Vehicle charging sessions 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, Table, Column, UniqueConstraint
8
+ from sqlalchemy.orm import Mapped, mapped_column, relationship, backref
9
+
10
+ from sqlalchemy_utc import UtcDateTime
11
+
12
+ from carconnectivity.charging import Charging
13
+
14
+ from carconnectivity_plugins.database.model.base import Base
15
+
16
+
17
+ if TYPE_CHECKING:
18
+ from sqlalchemy import Constraint
19
+
20
+
21
+ charging_tag_association_table = Table('charging_sessions_tags', Base.metadata,
22
+ Column('charging_sessions_id', ForeignKey('charging_sessions.id')),
23
+ Column('tags_name', ForeignKey('tags.name'))
24
+ )
25
+
26
+
27
+ class ChargingSession(Base): # pylint: disable=too-few-public-methods
28
+ """
29
+ Represents a charging session for an electric vehicle.
30
+ This class models a complete charging session lifecycle, tracking various timestamps
31
+ and metrics from when the vehicle is plugged in until it is disconnected. It captures
32
+ connection events (plug, lock, charge start/end, unlock, unplug) along with session
33
+ metadata such as battery levels, charge type, location, and odometer readings.
34
+ Attributes:
35
+ id (int): Primary key identifier for the charging session.
36
+ vin (str): Vehicle Identification Number, foreign key to the vehicles table.
37
+ vehicle (Vehicle): Relationship to the associated Vehicle object.
38
+ plug_connected_date (datetime, optional): Timestamp when the plug was connected.
39
+ plug_locked_date (datetime, optional): Timestamp when the plug was locked.
40
+ plug_unlocked_date (datetime, optional): Timestamp when the plug was unlocked.
41
+ session_start_date (datetime, optional): Timestamp when charging started.
42
+ session_end_date (datetime, optional): Timestamp when charging ended.
43
+ plug_disconnected_date (datetime, optional): Timestamp when the plug was disconnected.
44
+ start_level (float, optional): Battery level percentage at session start.
45
+ end_level (float, optional): Battery level percentage at session end.
46
+ session_charge_type (Charging.ChargingType, optional): Type of charging (e.g., AC, DC).
47
+ session_position_latitude (float, optional): Latitude coordinate of charging location.
48
+ session_position_longitude (float, optional): Longitude coordinate of charging location.
49
+ session_odometer (float, optional): Odometer reading at session start.
50
+ charging_type (Charging.ChargingType, optional): General charging type used during session.
51
+ tags (list[Tag]): Associated tags for categorizing or labeling the session.
52
+ The class provides various helper methods to query the current and historical state
53
+ of the charging session, such as whether the vehicle is currently connected, locked,
54
+ or actively charging.
55
+ """
56
+
57
+ __tablename__: str = 'charging_sessions'
58
+ __table_args__: tuple[Constraint] = (UniqueConstraint("vin", "session_start_date", name="vin_session_start_date"),)
59
+
60
+ id: Mapped[int] = mapped_column(primary_key=True)
61
+ vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
62
+ vehicle: Mapped["Vehicle"] = relationship("Vehicle")
63
+ plug_connected_date: Mapped[Optional[datetime]] = mapped_column(UtcDateTime)
64
+ plug_locked_date: Mapped[Optional[datetime]] = mapped_column(UtcDateTime)
65
+ plug_unlocked_date: Mapped[Optional[datetime]] = mapped_column(UtcDateTime)
66
+ session_start_date: Mapped[Optional[datetime]] = mapped_column(UtcDateTime)
67
+ session_end_date: Mapped[Optional[datetime]] = mapped_column(UtcDateTime)
68
+ plug_disconnected_date: Mapped[Optional[datetime]] = mapped_column(UtcDateTime)
69
+ start_level: Mapped[Optional[float]]
70
+ end_level: Mapped[Optional[float]]
71
+ session_charge_type: Mapped[Optional[Charging.ChargingType]]
72
+ session_position_latitude: Mapped[Optional[float]]
73
+ session_position_longitude: Mapped[Optional[float]]
74
+ session_odometer: Mapped[Optional[float]]
75
+ charging_type: Mapped[Optional[Charging.ChargingType]]
76
+ location_uid: Mapped[Optional[str]] = mapped_column(ForeignKey("locations.uid"))
77
+ location: Mapped[Optional["Location"]] = relationship("Location")
78
+ charging_station_uid: Mapped[Optional[str]] = mapped_column(ForeignKey("charging_stations.uid"))
79
+ charging_station: Mapped[Optional["ChargingStation"]] = relationship("ChargingStation")
80
+ meter_start_kwh: Mapped[Optional[float]]
81
+ meter_end_kwh: Mapped[Optional[float]]
82
+ price_per_kwh: Mapped[Optional[float]]
83
+ price_per_min: Mapped[Optional[float]]
84
+ price_per_session: Mapped[Optional[float]]
85
+ real_charged: Mapped[Optional[float]]
86
+ real_cost: Mapped[Optional[float]]
87
+ tags: Mapped[list["Tag"]] = relationship("Tag", secondary=charging_tag_association_table, backref=backref("charging_sessions"))
88
+
89
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
90
+ def __init__(self, vin: str, plug_connected_date: Optional[datetime] = None, plug_locked_date: Optional[datetime] = None,
91
+ session_start_date: Optional[datetime] = None, start_level: Optional[float] = None,
92
+ session_charge_type: Optional[Charging.ChargingType] = None, session_position_latitude: Optional[float] = None,
93
+ session_position_longitude: Optional[float] = None, session_odometer: Optional[float] = None,
94
+ charging_type: Optional[Charging.ChargingType] = None) -> None:
95
+ self.vin = vin
96
+ self.plug_connected_date = plug_connected_date
97
+ self.plug_locked_date = plug_locked_date
98
+ self.session_start_date = session_start_date
99
+ self.start_level = start_level
100
+ self.session_charge_type = session_charge_type
101
+ self.session_position_latitude = session_position_latitude
102
+ self.session_position_longitude = session_position_longitude
103
+ self.session_odometer = session_odometer
104
+ self.charging_type = charging_type
105
+
106
+ def is_connected(self) -> bool:
107
+ """Returns True if the vehicle is currently connected to a charger."""
108
+ return self.plug_connected_date is not None and self.plug_disconnected_date is None
109
+
110
+ def is_locked(self) -> bool:
111
+ """Returns True if the vehicle is currently locked to a charger."""
112
+ return self.plug_locked_date is not None and self.plug_unlocked_date is None
113
+
114
+ def is_charging(self) -> bool:
115
+ """Returns True if the vehicle is currently in a charging session."""
116
+ return self.session_start_date is not None and self.session_end_date is None
117
+
118
+ def is_closed(self) -> bool:
119
+ """Returns True if the charging session has ended."""
120
+ return self.session_end_date is not None or self.plug_unlocked_date is not None or self.plug_disconnected_date is not None
121
+
122
+ def was_started(self) -> bool:
123
+ """Returns True if the charging session was started but may also have been already ended."""
124
+ return self.session_start_date is not None
125
+
126
+ def was_connected(self) -> bool:
127
+ """Returns True if the vehicle was connected to a charger during this session."""
128
+ return self.plug_connected_date is not None
129
+
130
+ def was_locked(self) -> bool:
131
+ """Returns True if the vehicle was locked to a charger during this session."""
132
+ return self.plug_locked_date is not None
133
+
134
+ def was_ended(self) -> bool:
135
+ """Returns True if the charging session was ended."""
136
+ return self.session_end_date is not None
137
+
138
+ def was_disconnected(self) -> bool:
139
+ """Returns True if the vehicle was disconnected from a charger during this session."""
140
+ return self.plug_disconnected_date is not None
141
+
142
+ def was_unlocked(self) -> bool:
143
+ """Returns True if the vehicle was unlocked from a charger during this session."""
144
+ return self.plug_unlocked_date is not None
@@ -0,0 +1,57 @@
1
+ """ This module contains the Vehicle charging state 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.charging import Charging
13
+
14
+ from carconnectivity_plugins.database.model.base import Base
15
+
16
+ if TYPE_CHECKING:
17
+ from sqlalchemy import Constraint
18
+
19
+
20
+ class ChargingState(Base): # pylint: disable=too-few-public-methods
21
+ """
22
+ SQLAlchemy model representing a vehicle's charging state over a time period.
23
+
24
+ This model stores charging state information for vehicles, tracking the state
25
+ between a first and last date. It maintains a foreign key relationship with
26
+ the Vehicle model through the VIN (Vehicle Identification Number).
27
+
28
+ Attributes:
29
+ id (int): Primary key for the charging state record.
30
+ vin (str): Foreign key reference to the vehicle's VIN in the vehicles table.
31
+ vehicle (Vehicle): Relationship to the associated Vehicle model.
32
+ first_date (datetime): The start datetime of this charging state period (UTC).
33
+ last_date (datetime): The end datetime of this charging state period (UTC).
34
+ state (Optional[Charging.ChargingState]): The charging state during this period,
35
+ or None if no state is available.
36
+
37
+ Args:
38
+ vin (str): The vehicle identification number.
39
+ first_date (datetime): The starting datetime for this charging state.
40
+ last_date (datetime): The ending datetime for this charging state.
41
+ state (Optional[Charging.ChargingState]): The charging state value.
42
+ """
43
+ __tablename__: str = 'charging_states'
44
+ __table_args__: tuple[Constraint] = (UniqueConstraint("vin", "first_date", name="charging_states_vin_first_date"),)
45
+
46
+ id: Mapped[int] = mapped_column(primary_key=True)
47
+ vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
48
+ vehicle: Mapped["Vehicle"] = relationship("Vehicle")
49
+ first_date: Mapped[datetime] = mapped_column(UtcDateTime)
50
+ last_date: Mapped[datetime] = mapped_column(UtcDateTime)
51
+ state: Mapped[Optional[Charging.ChargingState]]
52
+
53
+ def __init__(self, vin: str, first_date: datetime, last_date: datetime, state: Optional[Charging.ChargingState]) -> None:
54
+ self.vin = vin
55
+ self.first_date = first_date
56
+ self.last_date = last_date
57
+ self.state = state
@@ -0,0 +1,66 @@
1
+ """ This module contains the charging stations database model"""
2
+ from __future__ import annotations
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from sqlalchemy.orm import Mapped, mapped_column
6
+
7
+ from carconnectivity_plugins.database.model.base import Base
8
+
9
+ if TYPE_CHECKING:
10
+ from carconnectivity.charging_station import ChargingStation as CarConnectivityChargingStation
11
+
12
+
13
+ class ChargingStation(Base): # pylint: disable=too-few-public-methods,too-many-instance-attributes
14
+ """
15
+ SQLAlchemy model representing a charging station in the database.
16
+ This class maps charging station data to the 'charging_stations' database table,
17
+ storing information about electric vehicle charging locations and their properties.
18
+ Attributes:
19
+ uid (str): Unique identifier for the charging station (primary key).
20
+ source (Optional[str]): Source or provider of the charging station data.
21
+ name (Optional[str]): Name or title of the charging station.
22
+ latitude (Optional[float]): Geographic latitude coordinate of the station.
23
+ longitude (Optional[float]): Geographic longitude coordinate of the station.
24
+ address (Optional[str]): Physical address of the charging station.
25
+ max_power (Optional[float]): Maximum power output in kilowatts (kW).
26
+ num_spots (Optional[int]): Number of available charging spots.
27
+ operator_id (Optional[str]): Identifier of the charging station operator.
28
+ operator_name (Optional[str]): Name of the charging station operator.
29
+ raw (Optional[str]): Raw data from the original source in string format.
30
+ Methods:
31
+ from_carconnectivity_charging_station: Class method to create an instance from a
32
+ CarConnectivity ChargingStation object.
33
+ """
34
+ __tablename__: str = 'charging_stations'
35
+
36
+ uid: Mapped[str] = mapped_column(primary_key=True)
37
+ source: Mapped[Optional[str]]
38
+ name: Mapped[Optional[str]]
39
+ latitude: Mapped[Optional[float]]
40
+ longitude: Mapped[Optional[float]]
41
+ address: Mapped[Optional[str]]
42
+ max_power: Mapped[Optional[float]]
43
+ num_spots: Mapped[Optional[int]]
44
+ operator_id: Mapped[Optional[str]]
45
+ operator_name: Mapped[Optional[str]]
46
+ raw: Mapped[Optional[str]]
47
+
48
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
49
+ def __init__(self, uid: str) -> None:
50
+ self.uid = uid
51
+
52
+ @classmethod
53
+ def from_carconnectivity_charging_station(cls, charging_station: CarConnectivityChargingStation) -> ChargingStation:
54
+ """Create a ChargingStation instance from a carconnectivity ChargingStation object."""
55
+ cs = cls(uid=charging_station.uid.value)
56
+ cs.source = charging_station.source.value
57
+ cs.name = charging_station.name.value
58
+ cs.latitude = charging_station.latitude.value
59
+ cs.longitude = charging_station.longitude.value
60
+ cs.address = charging_station.address.value
61
+ cs.max_power = charging_station.max_power.value
62
+ cs.num_spots = charging_station.num_spots.value
63
+ cs.operator_id = charging_station.operator_id.value
64
+ cs.operator_name = charging_station.operator_name.value
65
+ cs.raw = charging_station.raw.value
66
+ return cs
@@ -0,0 +1,57 @@
1
+ """ This module contains the Vehicle climatization state 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.climatization import Climatization
13
+
14
+ from carconnectivity_plugins.database.model.base import Base
15
+
16
+ if TYPE_CHECKING:
17
+ from sqlalchemy import Constraint
18
+
19
+
20
+ class ClimatizationState(Base): # pylint: disable=too-few-public-methods
21
+ """
22
+ SQLAlchemy model representing a vehicle's climatization state over a time period.
23
+
24
+ This model stores climatization state information for vehicles, tracking the state
25
+ between a first and last date. It maintains a foreign key relationship with
26
+ the Vehicle model through the VIN (Vehicle Identification Number).
27
+
28
+ Attributes:
29
+ id (int): Primary key for the climatization state record.
30
+ vin (str): Foreign key reference to the vehicle's VIN in the vehicles table.
31
+ vehicle (Vehicle): Relationship to the associated Vehicle model.
32
+ first_date (datetime): The start datetime of this climatization state period (UTC).
33
+ last_date (datetime): The end datetime of this climatization state period (UTC).
34
+ state (Optional[Climatization.ClimatizationState]): The climatization state during this period,
35
+ or None if no state is available.
36
+
37
+ Args:
38
+ vin (str): The vehicle identification number.
39
+ first_date (datetime): The starting datetime for this climatization state.
40
+ last_date (datetime): The ending datetime for this climatization state.
41
+ state (Optional[Climatization.ClimatizationState]): The climatization state value.
42
+ """
43
+ __tablename__: str = 'climatization_states'
44
+ __table_args__: tuple[Constraint] = (UniqueConstraint("vin", "first_date", name="climatization_states_vin_first_date"),)
45
+
46
+ id: Mapped[int] = mapped_column(primary_key=True)
47
+ vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
48
+ vehicle: Mapped["Vehicle"] = relationship("Vehicle")
49
+ first_date: Mapped[datetime] = mapped_column(UtcDateTime)
50
+ last_date: Mapped[datetime] = mapped_column(UtcDateTime)
51
+ state: Mapped[Optional[Climatization.ClimatizationState]]
52
+
53
+ def __init__(self, vin: str, first_date: datetime, last_date: datetime, state: Optional[Climatization.ClimatizationState]) -> None:
54
+ self.vin = vin
55
+ self.first_date = first_date
56
+ self.last_date = last_date
57
+ self.state = state
@@ -0,0 +1,57 @@
1
+ """ This module contains the Vehicle connectionstate 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.vehicle import GenericVehicle
13
+
14
+ from carconnectivity_plugins.database.model.base import Base
15
+
16
+ if TYPE_CHECKING:
17
+ from sqlalchemy import Constraint
18
+
19
+
20
+ class ConnectionState(Base): # pylint: disable=too-few-public-methods
21
+ """
22
+ Represents a vehicle connection state record in the database.
23
+ This class models the states table which tracks the historical connection
24
+ state of vehicles over time periods.
25
+ Attributes:
26
+ id (int): Primary key for the state record.
27
+ vin (str): Foreign key reference to the vehicle identification number in the parent table.
28
+ vehicle (Vehicle): Relationship to the Vehicle model.
29
+ first_date (datetime): The timestamp when this state period began (UTC).
30
+ last_date (datetime, optional): The timestamp when this state period ended (UTC).
31
+ None indicates the state is still active.
32
+ state (GenericVehicle.State, optional): The vehicle's operational state during this period.
33
+ connection_state (GenericVehicle.ConnectionState, optional): The vehicle's connection state
34
+ during this period.
35
+ Args:
36
+ vin (str): The vehicle identification number.
37
+ first_date (datetime): The start timestamp of the state period.
38
+ last_date (datetime): The end timestamp of the state period.
39
+ state (GenericVehicle.State, optional): The vehicle's operational state.
40
+ connection_state (GenericVehicle.ConnectionState, optional): The vehicle's connection state.
41
+ """
42
+ __tablename__: str = 'connection_states'
43
+ __table_args__: tuple[Constraint] = (UniqueConstraint("vin", "first_date", name="connection_states_vin_first_date"),)
44
+
45
+ id: Mapped[int] = mapped_column(primary_key=True)
46
+ vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
47
+ vehicle: Mapped["Vehicle"] = relationship("Vehicle")
48
+ first_date: Mapped[datetime] = mapped_column(UtcDateTime)
49
+ last_date: Mapped[datetime] = mapped_column(UtcDateTime)
50
+ connection_state: Mapped[Optional[GenericVehicle.ConnectionState]]
51
+
52
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
53
+ def __init__(self, vin: str, first_date: datetime, last_date: datetime, connection_state: Optional[GenericVehicle.ConnectionState]) -> None:
54
+ self.vin = vin
55
+ self.first_date = first_date
56
+ self.last_date = last_date
57
+ self.connection_state = connection_state
@@ -0,0 +1,44 @@
1
+ """Decorator for handling datetime objects in SQLAlchemy models.
2
+ This decorator ensures that datetime values are stored in UTC and retrieved in UTC,
3
+ while also converting naive datetime objects to the local timezone before storing.
4
+ It is designed to work with SQLAlchemy's DateTime type."""
5
+ from datetime import datetime, timezone
6
+ import sqlalchemy
7
+
8
+
9
+ # pylint: disable=too-many-ancestors
10
+ class DatetimeDecorator(sqlalchemy.types.TypeDecorator):
11
+ """Decorator for handling datetime objects in SQLAlchemy models."""
12
+ impl = sqlalchemy.types.DateTime
13
+ cache_ok = True
14
+
15
+ def process_literal_param(self, value, dialect):
16
+ """Process literal parameter for SQLAlchemy."""
17
+ if value is None:
18
+ return None
19
+ return f"'{value.isoformat()}'"
20
+
21
+ @property
22
+ def python_type(self):
23
+ """Return the Python type handled by this decorator."""
24
+ return datetime
25
+
26
+ LOCAL_TIMEZONE = datetime.utcnow().astimezone().tzinfo
27
+
28
+ def process_bind_param(self, value, dialect):
29
+ if value is None:
30
+ return value
31
+
32
+ if value.tzinfo is None:
33
+ value = value.astimezone(self.LOCAL_TIMEZONE)
34
+
35
+ return value.astimezone(timezone.utc)
36
+
37
+ def process_result_value(self, value, dialect):
38
+ if value is None:
39
+ return value
40
+
41
+ if value.tzinfo is None:
42
+ return value.replace(tzinfo=timezone.utc)
43
+
44
+ return value.astimezone(timezone.utc)
@@ -0,0 +1,89 @@
1
+ """ This module contains the Drive database model"""
2
+ from __future__ import annotations
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ import logging
6
+
7
+ from sqlalchemy import ForeignKey, UniqueConstraint
8
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
9
+
10
+ from carconnectivity.drive import GenericDrive
11
+
12
+ from carconnectivity_plugins.database.agents.base_agent import BaseAgent
13
+ from carconnectivity_plugins.database.agents.drive_state_agent import DriveStateAgent
14
+ from carconnectivity_plugins.database.model.base import Base
15
+
16
+ if TYPE_CHECKING:
17
+ from sqlalchemy.orm import scoped_session
18
+ from sqlalchemy.orm.session import Session
19
+ from sqlalchemy import Constraint
20
+
21
+ from carconnectivity_plugins.database.plugin import Plugin
22
+
23
+ LOG: logging.Logger = logging.getLogger("carconnectivity.plugins.database.model.drive")
24
+
25
+
26
+ class Drive(Base):
27
+ """
28
+ Database model representing a vehicle drive/trip.
29
+ This class maps to the 'drives' table and stores information about individual
30
+ vehicle drives, including their relationship to vehicles and various drive
31
+ attributes tracked through CarConnectivity.
32
+ Attributes:
33
+ id (int): Primary key for the drive record.
34
+ vin (str): Vehicle Identification Number, foreign key to vehicles table.
35
+ vehicle (Vehicle): SQLAlchemy relationship to the associated Vehicle.
36
+ drive_id (Optional[str]): Optional identifier for the drive.
37
+ type (Optional[GenericDrive.Type]): Type/category of the drive.
38
+ carconnectivity_drive (Optional[GenericDrive]): Reference to the CarConnectivity
39
+ drive object (not persisted to database).
40
+ agents (list[BaseAgent]): List of agents monitoring this drive (not persisted
41
+ to database).
42
+ Args:
43
+ vin (str): Vehicle Identification Number to associate this drive with.
44
+ Methods:
45
+ connect: Establishes connection between database model and CarConnectivity
46
+ drive object, sets up observers, and initializes agents.
47
+ """
48
+ __tablename__: str = 'drives'
49
+ __allow_unmapped__: bool = True
50
+ __table_args__: tuple[Constraint] = (UniqueConstraint("vin", "drive_id", name="vin_drive_id"),)
51
+
52
+ id: Mapped[int] = mapped_column(primary_key=True)
53
+ vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
54
+ vehicle: Mapped["Vehicle"] = relationship("Vehicle")
55
+ drive_id: Mapped[Optional[str]]
56
+ type: Mapped[Optional[GenericDrive.Type]]
57
+ capacity: Mapped[Optional[float]]
58
+ capacity_total: Mapped[Optional[float]]
59
+ wltp_range: Mapped[Optional[float]]
60
+
61
+ carconnectivity_drive: Optional[GenericDrive] = None
62
+ agents: list[BaseAgent] = []
63
+
64
+ def __init__(self, vin, drive_id: Optional[str] = None) -> None:
65
+ self.vin = vin
66
+ self.drive_id = drive_id
67
+
68
+ def connect(self, database_plugin: Plugin, session_factory: scoped_session[Session], carconnectivity_drive: GenericDrive) -> None:
69
+ """
70
+ Connect a CarConnectivity drive object to this database model instance.
71
+ This method establishes a connection between the database drive model and a CarConnectivity drive object,
72
+ sets up observers for type changes, synchronizes the drive type if available, and initializes the drive state agent.
73
+ Args:
74
+ session (Session): The database session to use for operations.
75
+ carconnectivity_drive (GenericDrive): The CarConnectivity drive object to connect to this database model.
76
+ Returns:
77
+ None
78
+ Note:
79
+ - Adds an observer to monitor type changes in the vehicle
80
+ - Automatically syncs the drive type if enabled and has a value
81
+ - Creates and registers a DriveStateAgent for managing drive state
82
+ """
83
+ if self.carconnectivity_drive is not None:
84
+ raise ValueError("Can only connect once! Drive already connected with database model")
85
+ self.carconnectivity_drive = carconnectivity_drive
86
+ LOG.debug("Adding DriveStateAgent to drive %s of vehicle %s", self.drive_id, self.vin)
87
+ drive_state_agent: DriveStateAgent = DriveStateAgent(database_plugin, session_factory, self) # type: ignore[assignment]
88
+ self.agents.append(drive_state_agent)
89
+
@@ -0,0 +1,48 @@
1
+ """ This module contains the Drive consumption 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
+ if TYPE_CHECKING:
15
+ from sqlalchemy import Constraint
16
+
17
+
18
+ class DriveConsumption(Base): # pylint: disable=too-few-public-methods
19
+ """
20
+ SQLAlchemy model representing consumption data for a specific drive .
21
+ This model stores consumption measurements for a drive
22
+ tracking energy or fuel usage between two timestamps.
23
+ Attributes:
24
+ id (int): Primary key identifier for the drive consumption record.
25
+ drive_id (int): Foreign key reference to the associated Drive.
26
+ drive (Drive): Relationship to the parent Drive object.
27
+ first_date (datetime): Start timestamp of the consumption measurement period.
28
+ last_date (datetime): End timestamp of the consumption measurement period.
29
+ consumption (Optional[float]): Measured consumption value for the period (e.g., kWh, liters).
30
+ Table Constraints:
31
+ - Unique constraint on (drive_id, first_date) to prevent duplicate entries.
32
+ """
33
+ __tablename__: str = 'drive_consumptions'
34
+ __table_args__: tuple[Constraint] = (UniqueConstraint("drive_id", "first_date", name="drive_consumptions_drive_id_first_date"),)
35
+
36
+ id: Mapped[int] = mapped_column(primary_key=True)
37
+ drive_id: Mapped[int] = mapped_column(ForeignKey("drives.id"))
38
+ drive: Mapped["Drive"] = relationship("Drive")
39
+ first_date: Mapped[datetime] = mapped_column(UtcDateTime)
40
+ last_date: Mapped[datetime] = mapped_column(UtcDateTime)
41
+ consumption: Mapped[Optional[float]]
42
+
43
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
44
+ def __init__(self, drive_id: Mapped[int], first_date: datetime, last_date: datetime, consumption: Optional[float]) -> None:
45
+ self.drive_id = drive_id
46
+ self.first_date = first_date
47
+ self.last_date = last_date
48
+ self.consumption = consumption
@@ -0,0 +1,50 @@
1
+ """ This module contains the Drive level 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
+ if TYPE_CHECKING:
15
+ from sqlalchemy import Constraint
16
+
17
+
18
+ class DriveLevel(Base): # pylint: disable=too-few-public-methods
19
+ """
20
+ SQLAlchemy model representing energy levels for a drive.
21
+ This class maps to the 'drive_levels' database table and tracks the level.
22
+ Attributes:
23
+ id (int): Primary key identifier for the drive level record.
24
+ drive_id (str): Foreign key referencing the associated drive
25
+ drive (Drive): Relationship to the Drive model.
26
+ first_date (datetime): Timestamp when this level measurement started.
27
+ last_date (datetime): Timestamp when this level measurement ended.
28
+ level (Optional[float]): Battery level value (percentage or absolute value).
29
+ Args:
30
+ drive_id (str): The identifier of the associated drive.
31
+ first_date (datetime): Start timestamp for this level measurement.
32
+ last_date (datetime): End timestamp for this level measurement.
33
+ level (Optional[float]): The battery level value, can be None if unavailable.
34
+ """
35
+ __tablename__: str = 'drive_levels'
36
+ __table_args__: tuple[Constraint] = (UniqueConstraint("drive_id", "first_date", name="drive_levels_drive_id_first_date"),)
37
+
38
+ id: Mapped[int] = mapped_column(primary_key=True)
39
+ drive_id: Mapped[int] = mapped_column(ForeignKey("drives.id"))
40
+ drive: Mapped["Drive"] = relationship("Drive")
41
+ first_date: Mapped[datetime] = mapped_column(UtcDateTime)
42
+ last_date: Mapped[datetime] = mapped_column(UtcDateTime)
43
+ level: Mapped[Optional[float]]
44
+
45
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
46
+ def __init__(self, drive_id: Mapped[int], first_date: datetime, last_date: datetime, level: Optional[float]) -> None:
47
+ self.drive_id = drive_id
48
+ self.first_date = first_date
49
+ self.last_date = last_date
50
+ self.level = level