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,53 @@
1
+ """ This module contains the Drive range 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 DriveRange(Base): # pylint: disable=too-few-public-methods
19
+ """
20
+ Represents a drive range record in the database.
21
+ This class models the range information for a specific drive, tracking the first and last
22
+ date of the range measurement along with the range value itself.
23
+ Attributes:
24
+ id (int): Primary key identifier for the drive range record.
25
+ drive_id (int): Foreign key reference to the associated drive record.
26
+ drive (Drive): Relationship to the Drive model.
27
+ first_date (datetime): The timestamp when the range measurement started.
28
+ last_date (datetime): The timestamp when the range measurement ended.
29
+ range (Optional[float]): The measured range value in appropriate units (e.g., kilometers or miles).
30
+ Can be None if the range data is not available.
31
+ Args:
32
+ drive_id (int): The identifier of the associated drive.
33
+ first_date (datetime): The start timestamp of the range measurement.
34
+ last_date (datetime): The end timestamp of the range measurement.
35
+ range (Optional[float]): The range value, or None if unavailable.
36
+ """
37
+
38
+ __tablename__: str = 'drive_ranges'
39
+ __table_args__: tuple[Constraint] = (UniqueConstraint("drive_id", "first_date", name="drive_ranges_drive_id_first_date"),)
40
+
41
+ id: Mapped[int] = mapped_column(primary_key=True)
42
+ drive_id: Mapped[int] = mapped_column(ForeignKey("drives.id"))
43
+ drive: Mapped["Drive"] = relationship("Drive")
44
+ first_date: Mapped[datetime] = mapped_column(UtcDateTime)
45
+ last_date: Mapped[datetime] = mapped_column(UtcDateTime)
46
+ range: Mapped[Optional[float]]
47
+
48
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
49
+ def __init__(self, drive_id: Mapped[int], first_date: datetime, last_date: datetime, range: Optional[float]) -> None:
50
+ self.drive_id = drive_id
51
+ self.first_date = first_date
52
+ self.last_date = last_date
53
+ self.range = range
@@ -0,0 +1,53 @@
1
+ """ This module contains the Drive range estimated full 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 DriveRangeEstimatedFull(Base): # pylint: disable=too-few-public-methods
19
+ """
20
+ Represents a drive range estimated full record in the database.
21
+ This class models the range information for a specific drive, tracking the first and last
22
+ date of the range measurement along with the range value itself.
23
+ Attributes:
24
+ id (int): Primary key identifier for the drive range record.
25
+ drive_id (int): Foreign key reference to the associated drive record.
26
+ drive (Drive): Relationship to the Drive model.
27
+ first_date (datetime): The timestamp when the range measurement started.
28
+ last_date (datetime): The timestamp when the range measurement ended.
29
+ range (Optional[float]): The measured range value in appropriate units (e.g., kilometers or miles).
30
+ Can be None if the range data is not available.
31
+ Args:
32
+ drive_id (int): The identifier of the associated drive.
33
+ first_date (datetime): The start timestamp of the range measurement.
34
+ last_date (datetime): The end timestamp of the range measurement.
35
+ range (Optional[float]): The range value, or None if unavailable.
36
+ """
37
+
38
+ __tablename__: str = 'drive_ranges_estimated_full'
39
+ __table_args__: tuple[Constraint] = (UniqueConstraint("drive_id", "first_date", name="drive_ranges_estimated_full_drive_id_first_date"),)
40
+
41
+ id: Mapped[int] = mapped_column(primary_key=True)
42
+ drive_id: Mapped[int] = mapped_column(ForeignKey("drives.id"))
43
+ drive: Mapped["Drive"] = relationship("Drive")
44
+ first_date: Mapped[datetime] = mapped_column(UtcDateTime)
45
+ last_date: Mapped[datetime] = mapped_column(UtcDateTime)
46
+ range_estimated_full: Mapped[Optional[float]]
47
+
48
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
49
+ def __init__(self, drive_id: Mapped[int], first_date: datetime, last_date: datetime, range_estimated_full: Optional[float]) -> None:
50
+ self.drive_id = drive_id
51
+ self.first_date = first_date
52
+ self.last_date = last_date
53
+ self.range_estimated_full = range_estimated_full
@@ -0,0 +1,85 @@
1
+ """ This module contains the location 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.location import Location as CarConnectivityLocation
11
+
12
+
13
+ class Location(Base): # pylint: disable=too-few-public-methods,too-many-instance-attributes
14
+ """
15
+ SQLAlchemy model representing a geographical location.
16
+ This class maps to the 'locations' table in the database and stores
17
+ geographical and address information for a specific location.
18
+ Attributes:
19
+ uid (str): Unique identifier for the location (primary key).
20
+ source (Optional[str]): The data source providing the location information.
21
+ latitude (Optional[float]): Latitude coordinate of the location.
22
+ longitude (Optional[float]): Longitude coordinate of the location.
23
+ display_name (Optional[str]): Human-readable display name of the location.
24
+ name (Optional[str]): Name of the location.
25
+ amenity (Optional[str]): Type of amenity at the location.
26
+ house_number (Optional[str]): House or building number.
27
+ road (Optional[str]): Street or road name.
28
+ neighbourhood (Optional[str]): Neighbourhood or area name.
29
+ city (Optional[str]): City name.
30
+ postcode (Optional[str]): Postal code.
31
+ county (Optional[str]): County name.
32
+ country (Optional[str]): Country name.
33
+ state (Optional[str]): State or province name.
34
+ state_district (Optional[str]): State district name.
35
+ raw (Optional[str]): Raw location data as received from the source.
36
+ Methods:
37
+ from_carconnectivity_location: Class method to create a Location instance
38
+ from a CarConnectivity Location object.
39
+ """
40
+
41
+ __tablename__: str = 'locations'
42
+
43
+ uid: Mapped[str] = mapped_column(primary_key=True)
44
+ source: Mapped[Optional[str]]
45
+ latitude: Mapped[Optional[float]]
46
+ longitude: Mapped[Optional[float]]
47
+ display_name: Mapped[Optional[str]]
48
+ name: Mapped[Optional[str]]
49
+ amenity: Mapped[Optional[str]]
50
+ house_number: Mapped[Optional[str]]
51
+ road: Mapped[Optional[str]]
52
+ neighbourhood: Mapped[Optional[str]]
53
+ city: Mapped[Optional[str]]
54
+ postcode: Mapped[Optional[str]]
55
+ county: Mapped[Optional[str]]
56
+ country: Mapped[Optional[str]]
57
+ state: Mapped[Optional[str]]
58
+ state_district: Mapped[Optional[str]]
59
+ raw: Mapped[Optional[str]]
60
+
61
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
62
+ def __init__(self, uid: str) -> None:
63
+ self.uid = uid
64
+
65
+ @classmethod
66
+ def from_carconnectivity_location(cls, location: CarConnectivityLocation) -> Location:
67
+ """Create a Location instance from a carconnectivity Location object."""
68
+ loc = cls(uid=location.uid.value)
69
+ loc.source = location.source.value
70
+ loc.latitude = location.latitude.value
71
+ loc.longitude = location.longitude.value
72
+ loc.display_name = location.display_name.value
73
+ loc.name = location.name.value
74
+ loc.amenity = location.amenity.value
75
+ loc.house_number = location.house_number.value
76
+ loc.road = location.road.value
77
+ loc.neighbourhood = location.neighbourhood.value
78
+ loc.city = location.city.value
79
+ loc.postcode = location.postcode.value
80
+ loc.county = location.county.value
81
+ loc.country = location.country.value
82
+ loc.state = location.state.value
83
+ loc.state_district = location.state_district.value
84
+ loc.raw = location.raw.value
85
+ return loc
@@ -0,0 +1,24 @@
1
+ """ Module for running database migrations using Alembic."""
2
+ import os
3
+
4
+ from alembic.command import upgrade, stamp
5
+ from alembic.config import Config
6
+
7
+
8
+ def run_database_migrations(dsn: str, stamp_only: bool = False):
9
+ """Run database migrations using Alembic."""
10
+ # retrieves the directory that *this* file is in
11
+ migrations_dir = os.path.dirname(os.path.realpath(__file__))
12
+ # this assumes the alembic.ini is also contained in this same directory
13
+ config_file = os.path.join(migrations_dir, "alembic.ini")
14
+
15
+ config = Config(file_=config_file)
16
+ config.attributes['configure_logger'] = False
17
+ config.set_main_option("script_location", migrations_dir + '/carconnectivity_schema')
18
+ config.set_main_option('sqlalchemy.url', dsn)
19
+
20
+ if stamp_only:
21
+ stamp(config, "head")
22
+ else:
23
+ # upgrade the database to the latest revision
24
+ upgrade(config, "head")
@@ -0,0 +1,52 @@
1
+ """ This module contains the Vehicle outside temperature 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 OutsideTemperature(Base): # pylint: disable=too-few-public-methods
19
+ """
20
+ SQLAlchemy model representing outside temperature measurements for vehicles.
21
+ This class stores outside temperature data associated with a vehicle, including
22
+ the time period when the measurement was recorded.
23
+ Attributes:
24
+ id (int): Primary key identifier for the temperature record.
25
+ vin (str): Vehicle Identification Number, foreign key referencing vehicles table.
26
+ vehicle (Vehicle): Relationship to the Vehicle model.
27
+ first_date (datetime): The timestamp when this temperature measurement was first recorded.
28
+ last_date (datetime): The timestamp when this temperature measurement was last updated.
29
+ outside_temperature (float, optional): The outside temperature value in degrees (unit depends on system configuration).
30
+ Can be None if the temperature is not available.
31
+ Args:
32
+ vin (str): Vehicle Identification Number for the associated vehicle.
33
+ first_date (datetime): Initial timestamp for the temperature measurement.
34
+ last_date (datetime): Last update timestamp for the temperature measurement.
35
+ outside_temperature (float, optional): The measured outside temperature value.
36
+ """
37
+ __tablename__: str = 'outside_temperatures'
38
+ __table_args__: tuple[Constraint] = (UniqueConstraint("vin", "first_date", name="outside_temperatures_vin_first_date"),)
39
+
40
+ id: Mapped[int] = mapped_column(primary_key=True)
41
+ vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
42
+ vehicle: Mapped["Vehicle"] = relationship("Vehicle")
43
+ first_date: Mapped[datetime] = mapped_column(UtcDateTime)
44
+ last_date: Mapped[datetime] = mapped_column(UtcDateTime)
45
+ outside_temperature: Mapped[Optional[float]]
46
+
47
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
48
+ def __init__(self, vin: str, first_date: datetime, last_date: datetime, outside_temperature: Optional[float]) -> None:
49
+ self.vin = vin
50
+ self.first_date = first_date
51
+ self.last_date = last_date
52
+ self.outside_temperature = outside_temperature
@@ -0,0 +1,56 @@
1
+ """ This module contains the Vehicle 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.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 State(Base): # pylint: disable=too-few-public-methods
21
+ """
22
+ Represents a vehicle state record in the database.
23
+ This class models the states table which tracks the historical state of vehicles over time periods.
24
+ Attributes:
25
+ id (int): Primary key for the state record.
26
+ vin (str): Foreign key reference to the vehicle identification number in the parent table.
27
+ vehicle (Vehicle): Relationship to the Vehicle model.
28
+ first_date (datetime): The timestamp when this state period began (UTC).
29
+ last_date (datetime, optional): The timestamp when this state period ended (UTC).
30
+ None indicates the state is still active.
31
+ state (GenericVehicle.State, optional): The vehicle's operational state during this period.
32
+ connection_state (GenericVehicle.ConnectionState, optional): The vehicle's connection state
33
+ during this period.
34
+ Args:
35
+ vin (str): The vehicle identification number.
36
+ first_date (datetime): The start timestamp of the state period.
37
+ last_date (datetime): The end timestamp of the state period.
38
+ state (GenericVehicle.State, optional): The vehicle's operational state.
39
+ connection_state (GenericVehicle.ConnectionState, optional): The vehicle's connection state.
40
+ """
41
+ __tablename__: str = 'states'
42
+ __table_args__: tuple[Constraint] = (UniqueConstraint("vin", "first_date", name="states_vin_first_date"),)
43
+
44
+ id: Mapped[int] = mapped_column(primary_key=True)
45
+ vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
46
+ vehicle: Mapped["Vehicle"] = relationship("Vehicle")
47
+ first_date: Mapped[datetime] = mapped_column(UtcDateTime)
48
+ last_date: Mapped[datetime] = mapped_column(UtcDateTime)
49
+ state: Mapped[Optional[GenericVehicle.State]]
50
+
51
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
52
+ def __init__(self, vin: str, first_date: datetime, last_date: datetime, state: Optional[GenericVehicle.State]) -> None:
53
+ self.vin = vin
54
+ self.first_date = first_date
55
+ self.last_date = last_date
56
+ self.state = state
@@ -0,0 +1,31 @@
1
+ """ This module contains the Tag database model"""
2
+ from __future__ import annotations
3
+ from typing import Optional
4
+
5
+ from sqlalchemy.orm import Mapped, mapped_column
6
+
7
+ from carconnectivity_plugins.database.model.base import Base
8
+
9
+
10
+ class Tag(Base): # pylint: disable=too-few-public-methods
11
+ """
12
+ Represents a tag in the database.
13
+ A tag is a label that can be associated with various entities in the system.
14
+ Each tag has a unique name and an optional description.
15
+ Attributes:
16
+ name (str): The unique name of the tag. Acts as the primary key.
17
+ description (str, optional): An optional description providing more context about the tag.
18
+ Args:
19
+ name (str): The unique name for the tag.
20
+ description (str, optional): An optional description for the tag. Defaults to None.
21
+ """
22
+
23
+ __tablename__: str = 'tags'
24
+
25
+ name: Mapped[str] = mapped_column(primary_key=True)
26
+ description: Mapped[Optional[str]]
27
+
28
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
29
+ def __init__(self, name: str, description: Optional[str] = None) -> None:
30
+ self.name = name
31
+ self.description = description
@@ -0,0 +1,33 @@
1
+ """Decorator for handling timedelta objects in SQLAlchemy models.
2
+ This decorator ensures that duration values are stored in microsecond format"""
3
+ from datetime import timedelta
4
+ import sqlalchemy
5
+
6
+
7
+ # pylint: disable=too-many-ancestors
8
+ class TimedeltaDecorator(sqlalchemy.types.TypeDecorator):
9
+ """Decorator for handling datetime objects in SQLAlchemy models."""
10
+ impl = sqlalchemy.types.Float
11
+ cache_ok = True
12
+
13
+ @property
14
+ def python_type(self):
15
+ """Return the Python type handled by this decorator."""
16
+ return timedelta
17
+
18
+ def process_bind_param(self, value, dialect):
19
+ if value is None:
20
+ return value
21
+
22
+ return value.total_seconds()
23
+
24
+ def process_result_value(self, value, dialect):
25
+ if value is None:
26
+ return value
27
+
28
+ return timedelta(seconds=value)
29
+
30
+ def process_literal_param(self, value, dialect):
31
+ if value is None:
32
+ return value
33
+ return value.total_seconds()
@@ -0,0 +1,85 @@
1
+ """ This module contains the Vehicle trip 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_plugins.database.model.base import Base
13
+
14
+ if TYPE_CHECKING:
15
+ from sqlalchemy import Constraint
16
+
17
+
18
+ trip_tag_association_table = Table('trips_tags', Base.metadata,
19
+ Column('trips_id', ForeignKey('trips.id')),
20
+ Column('tags_name', ForeignKey('tags.name'))
21
+ )
22
+
23
+
24
+ class Trip(Base): # pylint: disable=too-few-public-methods
25
+ """
26
+ Represents a vehicle trip in the database.
27
+
28
+ A Trip records information about a journey made by a vehicle, including start and
29
+ destination details such as timestamps, positions, and mileage readings. Trips can
30
+ be associated with tags for categorization and filtering.
31
+
32
+ Attributes:
33
+ id (int): Primary key identifier for the trip.
34
+ vin (str): Vehicle Identification Number, foreign key to vehicles table.
35
+ vehicle (Vehicle): Relationship to the associated Vehicle object.
36
+ start_date (datetime): UTC timestamp when the trip started.
37
+ destination_date (datetime, optional): UTC timestamp when the trip ended.
38
+ start_position_latitude (float, optional): Latitude coordinate of trip start location.
39
+ start_position_longitude (float, optional): Longitude coordinate of trip start location.
40
+ destination_position_latitude (float, optional): Latitude coordinate of trip destination.
41
+ destination_position_longitude (float, optional): Longitude coordinate of trip destination.
42
+ start_odometer (float, optional): Vehicle mileage in kilometers at trip start.
43
+ destination_odometer (float, optional): Vehicle mileage in kilometers at trip end.
44
+ tags (list[Tag]): List of tags associated with this trip for categorization.
45
+
46
+ Args:
47
+ vin (str): Vehicle Identification Number for the trip.
48
+ start_date (datetime): The starting timestamp of the trip.
49
+ start_position_latitude (float, optional): Latitude coordinate of trip start location.
50
+ start_position_longitude (float, optional): Longitude coordinate of trip start location.
51
+ start_odometer (float, optional): Vehicle mileage in kilometers at trip start.
52
+ """
53
+ __tablename__: str = 'trips'
54
+ __table_args__: tuple[Constraint] = (UniqueConstraint("vin", "start_date", name="vin_start_date"),)
55
+
56
+ id: Mapped[int] = mapped_column(primary_key=True)
57
+ vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
58
+ vehicle: Mapped["Vehicle"] = relationship("Vehicle")
59
+ start_date: Mapped[datetime] = mapped_column(UtcDateTime)
60
+ destination_date: Mapped[Optional[datetime]] = mapped_column(UtcDateTime)
61
+ start_position_latitude: Mapped[Optional[float]]
62
+ start_position_longitude: Mapped[Optional[float]]
63
+ start_location_uid: Mapped[Optional[str]] = mapped_column(ForeignKey("locations.uid"))
64
+ start_location: Mapped[Optional["Location"]] = relationship("Location", foreign_keys=[start_location_uid])
65
+ destination_position_latitude: Mapped[Optional[float]]
66
+ destination_position_longitude: Mapped[Optional[float]]
67
+ destination_location_uid: Mapped[Optional[str]] = mapped_column(ForeignKey("locations.uid"))
68
+ destination_location: Mapped[Optional["Location"]] = relationship("Location", foreign_keys=[destination_location_uid])
69
+ start_odometer: Mapped[Optional[float]]
70
+ destination_odometer: Mapped[Optional[float]]
71
+
72
+ tags: Mapped[list["Tag"]] = relationship("Tag", secondary=trip_tag_association_table, backref=backref("trips"))
73
+
74
+ # pylint: disable-next=too-many-arguments, too-many-positional-arguments
75
+ def __init__(self, vin: str, start_date: datetime, start_position_latitude: Optional[float] = None,
76
+ start_position_longitude: Optional[float] = None, start_odometer: Optional[float] = None) -> None:
77
+ self.vin = vin
78
+ self.start_date = start_date
79
+ self.start_position_latitude = start_position_latitude
80
+ self.start_position_longitude = start_position_longitude
81
+ self.start_odometer = start_odometer
82
+
83
+ def is_completed(self) -> bool:
84
+ """Returns True if the trip has been completed (i.e., has a destination date)."""
85
+ return self.destination_date is not None
@@ -0,0 +1,186 @@
1
+ """ This module contains the Vehicle database model"""
2
+ from __future__ import annotations
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ import logging
6
+
7
+ from sqlalchemy.exc import DatabaseError, IntegrityError
8
+ from sqlalchemy.orm import Mapped, mapped_column
9
+
10
+ from carconnectivity.vehicle import GenericVehicle, ElectricVehicle
11
+ from carconnectivity.observable import Observable
12
+ from carconnectivity.attributes import StringAttribute, IntegerAttribute, EnumAttribute
13
+
14
+ from carconnectivity_plugins.database.agents.base_agent import BaseAgent
15
+ from carconnectivity_plugins.database.agents.state_agent import StateAgent
16
+ from carconnectivity_plugins.database.agents.charging_agent import ChargingAgent
17
+ from carconnectivity_plugins.database.agents.climatization_agent import ClimatizationAgent
18
+ from carconnectivity_plugins.database.agents.trip_agent import TripAgent
19
+ from carconnectivity_plugins.database.model.base import Base
20
+ from carconnectivity_plugins.database.model.drive import Drive
21
+
22
+ if TYPE_CHECKING:
23
+ from sqlalchemy.orm import scoped_session
24
+ from sqlalchemy.orm.session import Session
25
+
26
+ from carconnectivity_plugins.database.plugin import Plugin
27
+
28
+ LOG: logging.Logger = logging.getLogger("carconnectivity.plugins.database.model.vehicle")
29
+
30
+
31
+ class Vehicle(Base):
32
+ """
33
+ SQLAlchemy model representing a vehicle in the database.
34
+
35
+ This class maps vehicle data from the CarConnectivity framework to database records,
36
+ maintaining synchronization between the CarConnectivity vehicle objects and their
37
+ corresponding database entries.
38
+
39
+ Attributes:
40
+ vin (str): Vehicle Identification Number, used as the primary key.
41
+ name (Optional[str]): The name or nickname of the vehicle.
42
+ manufacturer (Optional[str]): The manufacturer of the vehicle.
43
+ model (Optional[str]): The model name of the vehicle.
44
+ model_year (Optional[int]): The model year of the vehicle.
45
+ type (Optional[GenericVehicle.Type]): The type of vehicle (e.g., electric, hybrid).
46
+ license_plate (Optional[str]): The license plate number of the vehicle.
47
+ carconnectivity_vehicle (Optional[GenericVehicle]): Reference to the associated
48
+ CarConnectivity GenericVehicle object (not persisted in database).
49
+
50
+ Methods:
51
+ __init__(vin): Initialize a new Vehicle instance with the given VIN.
52
+ connect(carconnectivity_vehicle): Connect this database model to a CarConnectivity
53
+ vehicle object and set up observers to sync changes.
54
+
55
+ Notes:
56
+ The class uses SQLAlchemy's mapped_column and Mapped types for database mapping.
57
+ Observer callbacks are registered to automatically update database fields when
58
+ corresponding values change in the CarConnectivity vehicle object.
59
+ """
60
+ __tablename__ = 'vehicles'
61
+ __allow_unmapped__ = True
62
+
63
+ vin: Mapped[str] = mapped_column(primary_key=True)
64
+ name: Mapped[Optional[str]]
65
+ manufacturer: Mapped[Optional[str]]
66
+ model: Mapped[Optional[str]]
67
+ model_year: Mapped[Optional[int]]
68
+ type: Mapped[Optional[GenericVehicle.Type]]
69
+ license_plate: Mapped[Optional[str]]
70
+
71
+ carconnectivity_vehicle: Optional[GenericVehicle] = None
72
+ agents: list[BaseAgent] = []
73
+
74
+ def __init__(self, vin) -> None:
75
+ self.vin = vin
76
+
77
+ def connect(self, database_plugin: Plugin, session_factory: scoped_session[Session], carconnectivity_vehicle: GenericVehicle) -> None:
78
+ """
79
+ Connect a CarConnectivity vehicle instance to this database vehicle model and set up observers.
80
+ This method establishes a connection between a CarConnectivity vehicle object and this database vehicle model.
81
+ It registers observers for various vehicle attributes (name, manufacturer, model, model_year, type, and license_plate)
82
+ to monitor changes and synchronize them with the database. If the attributes are enabled and have values that differ
83
+ from the current database values, they are immediately synchronized.
84
+ Args:
85
+ carconnectivity_vehicle (GenericVehicle): The CarConnectivity vehicle instance to connect and observe.
86
+ Returns:
87
+ None
88
+ Note:
89
+ - Observers are triggered on transaction end to batch updates
90
+ - Only enabled attributes are synchronized
91
+ - The type attribute is only synchronized if it's not None
92
+ """
93
+ if self.carconnectivity_vehicle is not None:
94
+ raise ValueError("Can only connect once! Vehicle already connected with database model")
95
+ self.carconnectivity_vehicle = carconnectivity_vehicle
96
+ self.carconnectivity_vehicle.name.add_observer(self.__on_name_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
97
+ if self.carconnectivity_vehicle.name.enabled and self.name != self.carconnectivity_vehicle.name.value:
98
+ self.name = self.carconnectivity_vehicle.name.value
99
+ self.carconnectivity_vehicle.manufacturer.add_observer(self.__on_manufacturer_change, Observable.ObserverEvent.VALUE_CHANGED,
100
+ on_transaction_end=True)
101
+ if self.carconnectivity_vehicle.manufacturer.enabled and self.manufacturer != self.carconnectivity_vehicle.manufacturer.value:
102
+ self.manufacturer = self.carconnectivity_vehicle.manufacturer.value
103
+ self.carconnectivity_vehicle.model.add_observer(self.__on_model_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
104
+ if self.carconnectivity_vehicle.model.enabled and self.model != self.carconnectivity_vehicle.model.value:
105
+ self.model = self.carconnectivity_vehicle.model.value
106
+ self.carconnectivity_vehicle.model_year.add_observer(self.__on_model_year_change, Observable.ObserverEvent.VALUE_CHANGED,
107
+ on_transaction_end=True)
108
+ if self.carconnectivity_vehicle.model_year.enabled and self.model_year != self.carconnectivity_vehicle.model_year.value:
109
+ self.model_year = self.carconnectivity_vehicle.model_year.value
110
+ self.carconnectivity_vehicle.type.add_observer(self.__on_type_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
111
+ if self.carconnectivity_vehicle.type.enabled and self.carconnectivity_vehicle.type.value is not None \
112
+ and self.type != self.carconnectivity_vehicle.type.value:
113
+ self.type = self.carconnectivity_vehicle.type.value
114
+ self.carconnectivity_vehicle.license_plate.add_observer(self.__on_license_plate_change, Observable.ObserverEvent.VALUE_CHANGED,
115
+ on_transaction_end=True)
116
+ if self.carconnectivity_vehicle.license_plate.enabled and self.license_plate != self.carconnectivity_vehicle.license_plate.value:
117
+ self.license_plate = self.carconnectivity_vehicle.license_plate.value
118
+
119
+ with session_factory() as session:
120
+ for drive_id, drive in self.carconnectivity_vehicle.drives.drives.items():
121
+ drive_db: Optional[Drive] = session.query(Drive).filter(Drive.vin == self.vin, Drive.drive_id == drive_id).first()
122
+ if drive_db is None:
123
+ drive_db = Drive(vin=self.vin, drive_id=drive_id)
124
+ try:
125
+ session.add(drive_db)
126
+ session.commit()
127
+ LOG.debug('Added new drive %s for vehicle %s to database', drive_id, self.vin)
128
+ drive_db.connect(database_plugin, session_factory, drive)
129
+ except IntegrityError as err:
130
+ session.rollback()
131
+ LOG.error('IntegrityError while adding drive %s for vehicle %s to database, likely due to concurrent addition: %s', drive_id, self.vin,
132
+ err)
133
+ database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
134
+ except DatabaseError as err:
135
+ session.rollback()
136
+ LOG.error('DatabaseError while adding drive %s for vehicle %s to database: %s', drive_id, self.vin, err)
137
+ database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
138
+ else:
139
+ drive_db.connect(database_plugin, session_factory, drive)
140
+ LOG.debug('Connecting drive %s for vehicle %s', drive_id, self.vin)
141
+
142
+ state_agent: StateAgent = StateAgent(database_plugin, session_factory, self)
143
+ self.agents.append(state_agent)
144
+ LOG.debug("Adding StateAgent to vehicle %s", self.vin)
145
+ climazination_agent: ClimatizationAgent = ClimatizationAgent(database_plugin, session_factory, self)
146
+ self.agents.append(climazination_agent)
147
+ LOG.debug("Adding ClimatizationAgent to vehicle %s", self.vin)
148
+ trip_agent: TripAgent = TripAgent(database_plugin, session_factory, self)
149
+ self.agents.append(trip_agent)
150
+ LOG.debug("Adding TripAgent to vehicle %s", self.vin)
151
+
152
+ if isinstance(self.carconnectivity_vehicle, ElectricVehicle):
153
+ charging_agent: ChargingAgent = ChargingAgent(database_plugin, session_factory, self)
154
+ self.agents.append(charging_agent)
155
+ LOG.debug("Adding ChargingAgent to vehicle %s", self.vin)
156
+ session_factory.remove()
157
+
158
+ def __on_name_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
159
+ del flags
160
+ if self.name != element.value:
161
+ self.name = element.value
162
+
163
+ def __on_manufacturer_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
164
+ del flags
165
+ if self.manufacturer != element.value:
166
+ self.manufacturer = element.value
167
+
168
+ def __on_model_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
169
+ del flags
170
+ if self.model != element.value:
171
+ self.model = element.value
172
+
173
+ def __on_model_year_change(self, element: IntegerAttribute, flags: Observable.ObserverEvent) -> None:
174
+ del flags
175
+ if self.model_year != element.value:
176
+ self.model_year = element.value
177
+
178
+ def __on_type_change(self, element: EnumAttribute[GenericVehicle.Type], flags: Observable.ObserverEvent) -> None:
179
+ del flags
180
+ if element.value is not None and self.type != element.value:
181
+ self.type = element.value
182
+
183
+ def __on_license_plate_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
184
+ del flags
185
+ if self.license_plate != element.value:
186
+ self.license_plate = element.value