carconnectivity-plugin-database 0.1__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.
Potentially problematic release.
This version of carconnectivity-plugin-database might be problematic. Click here for more details.
- carconnectivity_database/__init__.py +0 -0
- carconnectivity_database/carconnectivity_database_base.py +26 -0
- carconnectivity_plugin_database-0.1.dist-info/METADATA +74 -0
- carconnectivity_plugin_database-0.1.dist-info/RECORD +48 -0
- carconnectivity_plugin_database-0.1.dist-info/WHEEL +5 -0
- carconnectivity_plugin_database-0.1.dist-info/entry_points.txt +2 -0
- carconnectivity_plugin_database-0.1.dist-info/licenses/LICENSE +21 -0
- carconnectivity_plugin_database-0.1.dist-info/top_level.txt +2 -0
- carconnectivity_plugins/database/__init__.py +0 -0
- carconnectivity_plugins/database/_version.py +34 -0
- carconnectivity_plugins/database/agents/base_agent.py +2 -0
- carconnectivity_plugins/database/agents/charging_agent.py +610 -0
- carconnectivity_plugins/database/agents/climatization_agent.py +83 -0
- carconnectivity_plugins/database/agents/drive_state_agent.py +378 -0
- carconnectivity_plugins/database/agents/state_agent.py +166 -0
- carconnectivity_plugins/database/agents/trip_agent.py +227 -0
- carconnectivity_plugins/database/model/__init__.py +19 -0
- carconnectivity_plugins/database/model/alembic.ini +100 -0
- carconnectivity_plugins/database/model/base.py +4 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/README +1 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/__init__.py +0 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/env.py +78 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/script.py.mako +24 -0
- carconnectivity_plugins/database/model/carconnectivity_schema/versions/__init__.py +0 -0
- carconnectivity_plugins/database/model/charging_power.py +52 -0
- carconnectivity_plugins/database/model/charging_rate.py +52 -0
- carconnectivity_plugins/database/model/charging_session.py +144 -0
- carconnectivity_plugins/database/model/charging_state.py +57 -0
- carconnectivity_plugins/database/model/charging_station.py +66 -0
- carconnectivity_plugins/database/model/climatization_state.py +57 -0
- carconnectivity_plugins/database/model/connection_state.py +57 -0
- carconnectivity_plugins/database/model/datetime_decorator.py +44 -0
- carconnectivity_plugins/database/model/drive.py +86 -0
- carconnectivity_plugins/database/model/drive_consumption.py +48 -0
- carconnectivity_plugins/database/model/drive_level.py +50 -0
- carconnectivity_plugins/database/model/drive_range.py +53 -0
- carconnectivity_plugins/database/model/drive_range_full.py +53 -0
- carconnectivity_plugins/database/model/location.py +85 -0
- carconnectivity_plugins/database/model/migrations.py +24 -0
- carconnectivity_plugins/database/model/outside_temperature.py +52 -0
- carconnectivity_plugins/database/model/state.py +56 -0
- carconnectivity_plugins/database/model/tag.py +31 -0
- carconnectivity_plugins/database/model/timedelta_decorator.py +33 -0
- carconnectivity_plugins/database/model/trip.py +85 -0
- carconnectivity_plugins/database/model/vehicle.py +184 -0
- carconnectivity_plugins/database/plugin.py +161 -0
- carconnectivity_plugins/database/ui/plugin_ui.py +48 -0
- carconnectivity_plugins/database/ui/templates/database/status.html +8 -0
|
@@ -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,184 @@
|
|
|
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
|
+
agents: list[BaseAgent] = []
|
|
72
|
+
|
|
73
|
+
def __init__(self, vin) -> None:
|
|
74
|
+
self.vin = vin
|
|
75
|
+
|
|
76
|
+
def connect(self, database_plugin: Plugin, session_factory: scoped_session[Session], carconnectivity_vehicle: GenericVehicle) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Connect a CarConnectivity vehicle instance to this database vehicle model and set up observers.
|
|
79
|
+
This method establishes a connection between a CarConnectivity vehicle object and this database vehicle model.
|
|
80
|
+
It registers observers for various vehicle attributes (name, manufacturer, model, model_year, type, and license_plate)
|
|
81
|
+
to monitor changes and synchronize them with the database. If the attributes are enabled and have values that differ
|
|
82
|
+
from the current database values, they are immediately synchronized.
|
|
83
|
+
Args:
|
|
84
|
+
carconnectivity_vehicle (GenericVehicle): The CarConnectivity vehicle instance to connect and observe.
|
|
85
|
+
Returns:
|
|
86
|
+
None
|
|
87
|
+
Note:
|
|
88
|
+
- Observers are triggered on transaction end to batch updates
|
|
89
|
+
- Only enabled attributes are synchronized
|
|
90
|
+
- The type attribute is only synchronized if it's not None
|
|
91
|
+
"""
|
|
92
|
+
if self.agents:
|
|
93
|
+
raise ValueError("Can only connect once! Vehicle already connected with database model")
|
|
94
|
+
carconnectivity_vehicle.name.add_observer(self.__on_name_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
|
|
95
|
+
if carconnectivity_vehicle.name.enabled and self.name != carconnectivity_vehicle.name.value:
|
|
96
|
+
self.name = carconnectivity_vehicle.name.value
|
|
97
|
+
carconnectivity_vehicle.manufacturer.add_observer(self.__on_manufacturer_change, Observable.ObserverEvent.VALUE_CHANGED,
|
|
98
|
+
on_transaction_end=True)
|
|
99
|
+
if carconnectivity_vehicle.manufacturer.enabled and self.manufacturer != carconnectivity_vehicle.manufacturer.value:
|
|
100
|
+
self.manufacturer = carconnectivity_vehicle.manufacturer.value
|
|
101
|
+
carconnectivity_vehicle.model.add_observer(self.__on_model_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
|
|
102
|
+
if carconnectivity_vehicle.model.enabled and self.model != carconnectivity_vehicle.model.value:
|
|
103
|
+
self.model = carconnectivity_vehicle.model.value
|
|
104
|
+
carconnectivity_vehicle.model_year.add_observer(self.__on_model_year_change, Observable.ObserverEvent.VALUE_CHANGED,
|
|
105
|
+
on_transaction_end=True)
|
|
106
|
+
if carconnectivity_vehicle.model_year.enabled and self.model_year != carconnectivity_vehicle.model_year.value:
|
|
107
|
+
self.model_year = carconnectivity_vehicle.model_year.value
|
|
108
|
+
carconnectivity_vehicle.type.add_observer(self.__on_type_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
|
|
109
|
+
if carconnectivity_vehicle.type.enabled and carconnectivity_vehicle.type.value is not None \
|
|
110
|
+
and self.type != carconnectivity_vehicle.type.value:
|
|
111
|
+
self.type = carconnectivity_vehicle.type.value
|
|
112
|
+
carconnectivity_vehicle.license_plate.add_observer(self.__on_license_plate_change, Observable.ObserverEvent.VALUE_CHANGED,
|
|
113
|
+
on_transaction_end=True)
|
|
114
|
+
if carconnectivity_vehicle.license_plate.enabled and self.license_plate != carconnectivity_vehicle.license_plate.value:
|
|
115
|
+
self.license_plate = carconnectivity_vehicle.license_plate.value
|
|
116
|
+
|
|
117
|
+
with session_factory() as session:
|
|
118
|
+
for drive_id, drive in carconnectivity_vehicle.drives.drives.items():
|
|
119
|
+
drive_db: Optional[Drive] = session.query(Drive).filter(Drive.vin == self.vin, Drive.drive_id == drive_id).first()
|
|
120
|
+
if drive_db is None:
|
|
121
|
+
drive_db = Drive(vin=self.vin, drive_id=drive_id)
|
|
122
|
+
try:
|
|
123
|
+
session.add(drive_db)
|
|
124
|
+
session.commit()
|
|
125
|
+
LOG.debug('Added new drive %s for vehicle %s to database', drive_id, self.vin)
|
|
126
|
+
drive_db.connect(database_plugin, session_factory, drive)
|
|
127
|
+
except IntegrityError as err:
|
|
128
|
+
session.rollback()
|
|
129
|
+
LOG.error('IntegrityError while adding drive %s for vehicle %s to database, likely due to concurrent addition: %s', drive_id, self.vin,
|
|
130
|
+
err)
|
|
131
|
+
database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
132
|
+
except DatabaseError as err:
|
|
133
|
+
session.rollback()
|
|
134
|
+
LOG.error('DatabaseError while adding drive %s for vehicle %s to database: %s', drive_id, self.vin, err)
|
|
135
|
+
database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
136
|
+
else:
|
|
137
|
+
drive_db.connect(database_plugin, session_factory, drive)
|
|
138
|
+
LOG.debug('Connecting drive %s for vehicle %s', drive_id, self.vin)
|
|
139
|
+
|
|
140
|
+
state_agent: StateAgent = StateAgent(database_plugin, session_factory, self, carconnectivity_vehicle)
|
|
141
|
+
self.agents.append(state_agent)
|
|
142
|
+
LOG.debug("Adding StateAgent to vehicle %s", self.vin)
|
|
143
|
+
climazination_agent: ClimatizationAgent = ClimatizationAgent(database_plugin, session_factory, self, carconnectivity_vehicle)
|
|
144
|
+
self.agents.append(climazination_agent)
|
|
145
|
+
LOG.debug("Adding ClimatizationAgent to vehicle %s", self.vin)
|
|
146
|
+
trip_agent: TripAgent = TripAgent(database_plugin, session_factory, self, carconnectivity_vehicle)
|
|
147
|
+
self.agents.append(trip_agent)
|
|
148
|
+
LOG.debug("Adding TripAgent to vehicle %s", self.vin)
|
|
149
|
+
|
|
150
|
+
if isinstance(carconnectivity_vehicle, ElectricVehicle):
|
|
151
|
+
charging_agent: ChargingAgent = ChargingAgent(database_plugin, session_factory, self, carconnectivity_vehicle)
|
|
152
|
+
self.agents.append(charging_agent)
|
|
153
|
+
LOG.debug("Adding ChargingAgent to vehicle %s", self.vin)
|
|
154
|
+
session_factory.remove()
|
|
155
|
+
|
|
156
|
+
def __on_name_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
|
|
157
|
+
del flags
|
|
158
|
+
if self.name != element.value:
|
|
159
|
+
self.name = element.value
|
|
160
|
+
|
|
161
|
+
def __on_manufacturer_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
|
|
162
|
+
del flags
|
|
163
|
+
if self.manufacturer != element.value:
|
|
164
|
+
self.manufacturer = element.value
|
|
165
|
+
|
|
166
|
+
def __on_model_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
|
|
167
|
+
del flags
|
|
168
|
+
if self.model != element.value:
|
|
169
|
+
self.model = element.value
|
|
170
|
+
|
|
171
|
+
def __on_model_year_change(self, element: IntegerAttribute, flags: Observable.ObserverEvent) -> None:
|
|
172
|
+
del flags
|
|
173
|
+
if self.model_year != element.value:
|
|
174
|
+
self.model_year = element.value
|
|
175
|
+
|
|
176
|
+
def __on_type_change(self, element: EnumAttribute[GenericVehicle.Type], flags: Observable.ObserverEvent) -> None:
|
|
177
|
+
del flags
|
|
178
|
+
if element.value is not None and self.type != element.value:
|
|
179
|
+
self.type = element.value
|
|
180
|
+
|
|
181
|
+
def __on_license_plate_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
|
|
182
|
+
del flags
|
|
183
|
+
if self.license_plate != element.value:
|
|
184
|
+
self.license_plate = element.value
|