carconnectivity-plugin-database 0.1a3__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.
- carconnectivity_database/__init__.py +0 -0
- carconnectivity_database/carconnectivity_database_base.py +26 -0
- carconnectivity_plugin_database-0.1a3.dist-info/METADATA +74 -0
- carconnectivity_plugin_database-0.1a3.dist-info/RECORD +34 -0
- carconnectivity_plugin_database-0.1a3.dist-info/WHEEL +5 -0
- carconnectivity_plugin_database-0.1a3.dist-info/entry_points.txt +2 -0
- carconnectivity_plugin_database-0.1a3.dist-info/licenses/LICENSE +21 -0
- carconnectivity_plugin_database-0.1a3.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/drive_state_agent.py +71 -0
- carconnectivity_plugins/database/agents/state_agent.py +94 -0
- carconnectivity_plugins/database/model/__init__.py +10 -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/connection_state.py +53 -0
- carconnectivity_plugins/database/model/datetime_decorator.py +44 -0
- carconnectivity_plugins/database/model/drive.py +87 -0
- carconnectivity_plugins/database/model/drive_level.py +46 -0
- carconnectivity_plugins/database/model/drive_range.py +49 -0
- carconnectivity_plugins/database/model/migrations.py +24 -0
- carconnectivity_plugins/database/model/outside_temperature.py +48 -0
- carconnectivity_plugins/database/model/state.py +52 -0
- carconnectivity_plugins/database/model/timedelta_decorator.py +33 -0
- carconnectivity_plugins/database/model/vehicle.py +148 -0
- carconnectivity_plugins/database/plugin.py +150 -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 Vehicle connectionstate database model"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import ForeignKey
|
|
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
|
+
|
|
17
|
+
class ConnectionState(Base): # pylint: disable=too-few-public-methods
|
|
18
|
+
"""
|
|
19
|
+
Represents a vehicle connection state record in the database.
|
|
20
|
+
This class models the states table which tracks the historical connection
|
|
21
|
+
state of vehicles over time periods.
|
|
22
|
+
Attributes:
|
|
23
|
+
id (int): Primary key for the state record.
|
|
24
|
+
vin (str): Foreign key reference to the vehicle identification number in the parent table.
|
|
25
|
+
vehicle (Vehicle): Relationship to the Vehicle model.
|
|
26
|
+
first_date (datetime): The timestamp when this state period began (UTC).
|
|
27
|
+
last_date (datetime, optional): The timestamp when this state period ended (UTC).
|
|
28
|
+
None indicates the state is still active.
|
|
29
|
+
state (GenericVehicle.State, optional): The vehicle's operational state during this period.
|
|
30
|
+
connection_state (GenericVehicle.ConnectionState, optional): The vehicle's connection state
|
|
31
|
+
during this period.
|
|
32
|
+
Args:
|
|
33
|
+
vin (str): The vehicle identification number.
|
|
34
|
+
first_date (datetime): The start timestamp of the state period.
|
|
35
|
+
last_date (datetime): The end timestamp of the state period.
|
|
36
|
+
state (GenericVehicle.State, optional): The vehicle's operational state.
|
|
37
|
+
connection_state (GenericVehicle.ConnectionState, optional): The vehicle's connection state.
|
|
38
|
+
"""
|
|
39
|
+
__tablename__: str = 'connection_states'
|
|
40
|
+
|
|
41
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
42
|
+
vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
|
|
43
|
+
vehicle: Mapped["Vehicle"] = relationship("Vehicle")
|
|
44
|
+
first_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
45
|
+
last_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
46
|
+
connection_state: Mapped[Optional[GenericVehicle.ConnectionState]]
|
|
47
|
+
|
|
48
|
+
# pylint: disable-next=too-many-arguments, too-many-positional-arguments
|
|
49
|
+
def __init__(self, vin: str, first_date: datetime, last_date: datetime, connection_state: Optional[GenericVehicle.ConnectionState]) -> None:
|
|
50
|
+
self.vin = vin
|
|
51
|
+
self.first_date = first_date
|
|
52
|
+
self.last_date = last_date
|
|
53
|
+
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,87 @@
|
|
|
1
|
+
""" This module contains the Drive database model"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING, Optional
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import ForeignKey, UniqueConstraint
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
7
|
+
|
|
8
|
+
from carconnectivity.drive import GenericDrive
|
|
9
|
+
from carconnectivity.observable import Observable
|
|
10
|
+
from carconnectivity.attributes import EnumAttribute
|
|
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.session import Session
|
|
18
|
+
from sqlalchemy import Constraint
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Drive(Base):
|
|
22
|
+
"""
|
|
23
|
+
Database model representing a vehicle drive/trip.
|
|
24
|
+
This class maps to the 'drives' table and stores information about individual
|
|
25
|
+
vehicle drives, including their relationship to vehicles and various drive
|
|
26
|
+
attributes tracked through CarConnectivity.
|
|
27
|
+
Attributes:
|
|
28
|
+
id (int): Primary key for the drive record.
|
|
29
|
+
vin (str): Vehicle Identification Number, foreign key to vehicles table.
|
|
30
|
+
vehicle (Vehicle): SQLAlchemy relationship to the associated Vehicle.
|
|
31
|
+
drive_id (Optional[str]): Optional identifier for the drive.
|
|
32
|
+
type (Optional[GenericDrive.Type]): Type/category of the drive.
|
|
33
|
+
carconnectivity_drive (Optional[GenericDrive]): Reference to the CarConnectivity
|
|
34
|
+
drive object (not persisted to database).
|
|
35
|
+
agents (list[BaseAgent]): List of agents monitoring this drive (not persisted
|
|
36
|
+
to database).
|
|
37
|
+
Args:
|
|
38
|
+
vin (str): Vehicle Identification Number to associate this drive with.
|
|
39
|
+
Methods:
|
|
40
|
+
connect: Establishes connection between database model and CarConnectivity
|
|
41
|
+
drive object, sets up observers, and initializes agents.
|
|
42
|
+
"""
|
|
43
|
+
__tablename__: str = 'drives'
|
|
44
|
+
__allow_unmapped__: bool = True
|
|
45
|
+
__table_args__: tuple[Constraint] = (UniqueConstraint("vin", "drive_id", name="vin_drive_id"),)
|
|
46
|
+
|
|
47
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
48
|
+
vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
|
|
49
|
+
vehicle: Mapped["Vehicle"] = relationship("Vehicle")
|
|
50
|
+
drive_id: Mapped[Optional[str]]
|
|
51
|
+
type: Mapped[Optional[GenericDrive.Type]]
|
|
52
|
+
|
|
53
|
+
carconnectivity_drive: Optional[GenericDrive] = None
|
|
54
|
+
agents: list[BaseAgent] = []
|
|
55
|
+
|
|
56
|
+
def __init__(self, vin, drive_id: Optional[str] = None) -> None:
|
|
57
|
+
self.vin = vin
|
|
58
|
+
self.drive_id = drive_id
|
|
59
|
+
|
|
60
|
+
def connect(self, session: Session, carconnectivity_drive: GenericDrive) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Connect a CarConnectivity drive object to this database model instance.
|
|
63
|
+
This method establishes a connection between the database drive model and a CarConnectivity drive object,
|
|
64
|
+
sets up observers for type changes, synchronizes the drive type if available, and initializes the drive state agent.
|
|
65
|
+
Args:
|
|
66
|
+
session (Session): The database session to use for operations.
|
|
67
|
+
carconnectivity_drive (GenericDrive): The CarConnectivity drive object to connect to this database model.
|
|
68
|
+
Returns:
|
|
69
|
+
None
|
|
70
|
+
Note:
|
|
71
|
+
- Adds an observer to monitor type changes in the vehicle
|
|
72
|
+
- Automatically syncs the drive type if enabled and has a value
|
|
73
|
+
- Creates and registers a DriveStateAgent for managing drive state
|
|
74
|
+
"""
|
|
75
|
+
self.carconnectivity_drive = carconnectivity_drive
|
|
76
|
+
self.carconnectivity_drive.type.add_observer(self.__on_type_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
|
|
77
|
+
if self.carconnectivity_drive.type.enabled and self.carconnectivity_drive.type.value is not None \
|
|
78
|
+
and self.type != self.carconnectivity_drive.type.value:
|
|
79
|
+
self.type = self.carconnectivity_drive.type.value
|
|
80
|
+
|
|
81
|
+
drive_state_agent: DriveStateAgent = DriveStateAgent(session, self) # type: ignore[assignment]
|
|
82
|
+
self.agents.append(drive_state_agent)
|
|
83
|
+
|
|
84
|
+
def __on_type_change(self, element: EnumAttribute[GenericDrive.Type], flags: Observable.ObserverEvent) -> None:
|
|
85
|
+
del flags
|
|
86
|
+
if element.value is not None and self.type != element.value:
|
|
87
|
+
self.type = element.value
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
""" This module contains the Drive level database model"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import ForeignKey
|
|
8
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
9
|
+
|
|
10
|
+
from sqlalchemy_utc import UtcDateTime
|
|
11
|
+
|
|
12
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DriveLevel(Base): # pylint: disable=too-few-public-methods
|
|
16
|
+
"""
|
|
17
|
+
SQLAlchemy model representing energy levels for a drive.
|
|
18
|
+
This class maps to the 'drive_levels' database table and tracks the level.
|
|
19
|
+
Attributes:
|
|
20
|
+
id (int): Primary key identifier for the drive level record.
|
|
21
|
+
drive_id (str): Foreign key referencing the associated drive
|
|
22
|
+
drive (Drive): Relationship to the Drive model.
|
|
23
|
+
first_date (datetime): Timestamp when this level measurement started.
|
|
24
|
+
last_date (datetime): Timestamp when this level measurement ended.
|
|
25
|
+
level (Optional[float]): Battery level value (percentage or absolute value).
|
|
26
|
+
Args:
|
|
27
|
+
drive_id (str): The identifier of the associated drive.
|
|
28
|
+
first_date (datetime): Start timestamp for this level measurement.
|
|
29
|
+
last_date (datetime): End timestamp for this level measurement.
|
|
30
|
+
level (Optional[float]): The battery level value, can be None if unavailable.
|
|
31
|
+
"""
|
|
32
|
+
__tablename__: str = 'drive_levels'
|
|
33
|
+
|
|
34
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
35
|
+
drive_id: Mapped[int] = mapped_column(ForeignKey("drives.id"))
|
|
36
|
+
drive: Mapped["Drive"] = relationship("Drive")
|
|
37
|
+
first_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
38
|
+
last_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
39
|
+
level: Mapped[Optional[float]]
|
|
40
|
+
|
|
41
|
+
# pylint: disable-next=too-many-arguments, too-many-positional-arguments
|
|
42
|
+
def __init__(self, drive_id: Mapped[int], first_date: datetime, last_date: datetime, level: Optional[float]) -> None:
|
|
43
|
+
self.drive_id = drive_id
|
|
44
|
+
self.first_date = first_date
|
|
45
|
+
self.last_date = last_date
|
|
46
|
+
self.level = level
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
""" This module contains the Drive range database model"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import ForeignKey
|
|
8
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
9
|
+
|
|
10
|
+
from sqlalchemy_utc import UtcDateTime
|
|
11
|
+
|
|
12
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DriveRange(Base): # pylint: disable=too-few-public-methods
|
|
16
|
+
"""
|
|
17
|
+
Represents a drive range record in the database.
|
|
18
|
+
This class models the range information for a specific drive, tracking the first and last
|
|
19
|
+
date of the range measurement along with the range value itself.
|
|
20
|
+
Attributes:
|
|
21
|
+
id (int): Primary key identifier for the drive range record.
|
|
22
|
+
drive_id (int): Foreign key reference to the associated drive record.
|
|
23
|
+
drive (Drive): Relationship to the Drive model.
|
|
24
|
+
first_date (datetime): The timestamp when the range measurement started.
|
|
25
|
+
last_date (datetime): The timestamp when the range measurement ended.
|
|
26
|
+
range (Optional[float]): The measured range value in appropriate units (e.g., kilometers or miles).
|
|
27
|
+
Can be None if the range data is not available.
|
|
28
|
+
Args:
|
|
29
|
+
drive_id (int): The identifier of the associated drive.
|
|
30
|
+
first_date (datetime): The start timestamp of the range measurement.
|
|
31
|
+
last_date (datetime): The end timestamp of the range measurement.
|
|
32
|
+
range (Optional[float]): The range value, or None if unavailable.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__tablename__: str = 'drive_ranges'
|
|
36
|
+
|
|
37
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
38
|
+
drive_id: Mapped[int] = mapped_column(ForeignKey("drives.id"))
|
|
39
|
+
drive: Mapped["Drive"] = relationship("Drive")
|
|
40
|
+
first_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
41
|
+
last_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
42
|
+
range: Mapped[Optional[float]]
|
|
43
|
+
|
|
44
|
+
# pylint: disable-next=too-many-arguments, too-many-positional-arguments
|
|
45
|
+
def __init__(self, drive_id: Mapped[int], first_date: datetime, last_date: datetime, range: Optional[float]) -> None:
|
|
46
|
+
self.drive_id = drive_id
|
|
47
|
+
self.first_date = first_date
|
|
48
|
+
self.last_date = last_date
|
|
49
|
+
self.range = range
|
|
@@ -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,48 @@
|
|
|
1
|
+
""" This module contains the Vehicle outside temperature database model"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import ForeignKey
|
|
8
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
9
|
+
|
|
10
|
+
from sqlalchemy_utc import UtcDateTime
|
|
11
|
+
|
|
12
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OutsideTemperature(Base): # pylint: disable=too-few-public-methods
|
|
16
|
+
"""
|
|
17
|
+
SQLAlchemy model representing outside temperature measurements for vehicles.
|
|
18
|
+
This class stores outside temperature data associated with a vehicle, including
|
|
19
|
+
the time period when the measurement was recorded.
|
|
20
|
+
Attributes:
|
|
21
|
+
id (int): Primary key identifier for the temperature record.
|
|
22
|
+
vin (str): Vehicle Identification Number, foreign key referencing vehicles table.
|
|
23
|
+
vehicle (Vehicle): Relationship to the Vehicle model.
|
|
24
|
+
first_date (datetime): The timestamp when this temperature measurement was first recorded.
|
|
25
|
+
last_date (datetime): The timestamp when this temperature measurement was last updated.
|
|
26
|
+
outside_temperature (float, optional): The outside temperature value in degrees (unit depends on system configuration).
|
|
27
|
+
Can be None if the temperature is not available.
|
|
28
|
+
Args:
|
|
29
|
+
vin (str): Vehicle Identification Number for the associated vehicle.
|
|
30
|
+
first_date (datetime): Initial timestamp for the temperature measurement.
|
|
31
|
+
last_date (datetime): Last update timestamp for the temperature measurement.
|
|
32
|
+
outside_temperature (float, optional): The measured outside temperature value.
|
|
33
|
+
"""
|
|
34
|
+
__tablename__: str = 'outside_temperatures'
|
|
35
|
+
|
|
36
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
37
|
+
vin: Mapped[str] = mapped_column(ForeignKey("vehicles.vin"))
|
|
38
|
+
vehicle: Mapped["Vehicle"] = relationship("Vehicle")
|
|
39
|
+
first_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
40
|
+
last_date: Mapped[datetime] = mapped_column(UtcDateTime)
|
|
41
|
+
outside_temperature: Mapped[Optional[float]]
|
|
42
|
+
|
|
43
|
+
# pylint: disable-next=too-many-arguments, too-many-positional-arguments
|
|
44
|
+
def __init__(self, vin: str, first_date: datetime, last_date: datetime, outside_temperature: Optional[float]) -> None:
|
|
45
|
+
self.vin = vin
|
|
46
|
+
self.first_date = first_date
|
|
47
|
+
self.last_date = last_date
|
|
48
|
+
self.outside_temperature = outside_temperature
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
""" This module contains the Vehicle state database model"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import ForeignKey
|
|
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
|
+
|
|
17
|
+
class State(Base): # pylint: disable=too-few-public-methods
|
|
18
|
+
"""
|
|
19
|
+
Represents a vehicle state record in the database.
|
|
20
|
+
This class models the states table which tracks the historical state of vehicles over time periods.
|
|
21
|
+
Attributes:
|
|
22
|
+
id (int): Primary key for the state record.
|
|
23
|
+
vin (str): Foreign key reference to the vehicle identification number in the parent table.
|
|
24
|
+
vehicle (Vehicle): Relationship to the Vehicle model.
|
|
25
|
+
first_date (datetime): The timestamp when this state period began (UTC).
|
|
26
|
+
last_date (datetime, optional): The timestamp when this state period ended (UTC).
|
|
27
|
+
None indicates the state is still active.
|
|
28
|
+
state (GenericVehicle.State, optional): The vehicle's operational state during this period.
|
|
29
|
+
connection_state (GenericVehicle.ConnectionState, optional): The vehicle's connection state
|
|
30
|
+
during this period.
|
|
31
|
+
Args:
|
|
32
|
+
vin (str): The vehicle identification number.
|
|
33
|
+
first_date (datetime): The start timestamp of the state period.
|
|
34
|
+
last_date (datetime): The end timestamp of the state period.
|
|
35
|
+
state (GenericVehicle.State, optional): The vehicle's operational state.
|
|
36
|
+
connection_state (GenericVehicle.ConnectionState, optional): The vehicle's connection state.
|
|
37
|
+
"""
|
|
38
|
+
__tablename__: str = 'states'
|
|
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
|
+
state: Mapped[Optional[GenericVehicle.State]]
|
|
46
|
+
|
|
47
|
+
# pylint: disable-next=too-many-arguments, too-many-positional-arguments
|
|
48
|
+
def __init__(self, vin: str, first_date: datetime, last_date: datetime, state: Optional[GenericVehicle.State]) -> None:
|
|
49
|
+
self.vin = vin
|
|
50
|
+
self.first_date = first_date
|
|
51
|
+
self.last_date = last_date
|
|
52
|
+
self.state = state
|
|
@@ -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,148 @@
|
|
|
1
|
+
""" This module contains the Vehicle 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.vehicle import GenericVehicle
|
|
8
|
+
from carconnectivity.observable import Observable
|
|
9
|
+
from carconnectivity.attributes import StringAttribute, IntegerAttribute, EnumAttribute
|
|
10
|
+
|
|
11
|
+
from carconnectivity_plugins.database.agents.base_agent import BaseAgent
|
|
12
|
+
from carconnectivity_plugins.database.agents.state_agent import StateAgent
|
|
13
|
+
from carconnectivity_plugins.database.model.base import Base
|
|
14
|
+
from carconnectivity_plugins.database.model.drive import Drive
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sqlalchemy.orm.session import Session
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Vehicle(Base):
|
|
21
|
+
"""
|
|
22
|
+
SQLAlchemy model representing a vehicle in the database.
|
|
23
|
+
|
|
24
|
+
This class maps vehicle data from the CarConnectivity framework to database records,
|
|
25
|
+
maintaining synchronization between the CarConnectivity vehicle objects and their
|
|
26
|
+
corresponding database entries.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
vin (str): Vehicle Identification Number, used as the primary key.
|
|
30
|
+
name (Optional[str]): The name or nickname of the vehicle.
|
|
31
|
+
manufacturer (Optional[str]): The manufacturer of the vehicle.
|
|
32
|
+
model (Optional[str]): The model name of the vehicle.
|
|
33
|
+
model_year (Optional[int]): The model year of the vehicle.
|
|
34
|
+
type (Optional[GenericVehicle.Type]): The type of vehicle (e.g., electric, hybrid).
|
|
35
|
+
license_plate (Optional[str]): The license plate number of the vehicle.
|
|
36
|
+
carconnectivity_vehicle (Optional[GenericVehicle]): Reference to the associated
|
|
37
|
+
CarConnectivity GenericVehicle object (not persisted in database).
|
|
38
|
+
|
|
39
|
+
Methods:
|
|
40
|
+
__init__(vin): Initialize a new Vehicle instance with the given VIN.
|
|
41
|
+
connect(carconnectivity_vehicle): Connect this database model to a CarConnectivity
|
|
42
|
+
vehicle object and set up observers to sync changes.
|
|
43
|
+
|
|
44
|
+
Notes:
|
|
45
|
+
The class uses SQLAlchemy's mapped_column and Mapped types for database mapping.
|
|
46
|
+
Observer callbacks are registered to automatically update database fields when
|
|
47
|
+
corresponding values change in the CarConnectivity vehicle object.
|
|
48
|
+
"""
|
|
49
|
+
__tablename__ = 'vehicles'
|
|
50
|
+
__allow_unmapped__ = True
|
|
51
|
+
|
|
52
|
+
vin: Mapped[str] = mapped_column(primary_key=True)
|
|
53
|
+
name: Mapped[Optional[str]]
|
|
54
|
+
manufacturer: Mapped[Optional[str]]
|
|
55
|
+
model: Mapped[Optional[str]]
|
|
56
|
+
model_year: Mapped[Optional[int]]
|
|
57
|
+
type: Mapped[Optional[GenericVehicle.Type]]
|
|
58
|
+
license_plate: Mapped[Optional[str]]
|
|
59
|
+
|
|
60
|
+
carconnectivity_vehicle: Optional[GenericVehicle] = None
|
|
61
|
+
agents: list[BaseAgent] = []
|
|
62
|
+
|
|
63
|
+
def __init__(self, vin) -> None:
|
|
64
|
+
self.vin = vin
|
|
65
|
+
|
|
66
|
+
def connect(self, session: Session, carconnectivity_vehicle: GenericVehicle) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Connect a CarConnectivity vehicle instance to this database vehicle model and set up observers.
|
|
69
|
+
This method establishes a connection between a CarConnectivity vehicle object and this database vehicle model.
|
|
70
|
+
It registers observers for various vehicle attributes (name, manufacturer, model, model_year, type, and license_plate)
|
|
71
|
+
to monitor changes and synchronize them with the database. If the attributes are enabled and have values that differ
|
|
72
|
+
from the current database values, they are immediately synchronized.
|
|
73
|
+
Args:
|
|
74
|
+
carconnectivity_vehicle (GenericVehicle): The CarConnectivity vehicle instance to connect and observe.
|
|
75
|
+
Returns:
|
|
76
|
+
None
|
|
77
|
+
Note:
|
|
78
|
+
- Observers are triggered on transaction end to batch updates
|
|
79
|
+
- Only enabled attributes are synchronized
|
|
80
|
+
- The type attribute is only synchronized if it's not None
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
self.carconnectivity_vehicle = carconnectivity_vehicle
|
|
84
|
+
self.carconnectivity_vehicle.name.add_observer(self.__on_name_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
|
|
85
|
+
if self.carconnectivity_vehicle.name.enabled and self.name != self.carconnectivity_vehicle.name.value:
|
|
86
|
+
self.name = self.carconnectivity_vehicle.name.value
|
|
87
|
+
self.carconnectivity_vehicle.manufacturer.add_observer(self.__on_manufacturer_change, Observable.ObserverEvent.VALUE_CHANGED,
|
|
88
|
+
on_transaction_end=True)
|
|
89
|
+
if self.carconnectivity_vehicle.manufacturer.enabled and self.manufacturer != self.carconnectivity_vehicle.manufacturer.value:
|
|
90
|
+
self.manufacturer = self.carconnectivity_vehicle.manufacturer.value
|
|
91
|
+
self.carconnectivity_vehicle.model.add_observer(self.__on_model_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
|
|
92
|
+
if self.carconnectivity_vehicle.model.enabled and self.model != self.carconnectivity_vehicle.model.value:
|
|
93
|
+
self.model = self.carconnectivity_vehicle.model.value
|
|
94
|
+
self.carconnectivity_vehicle.model_year.add_observer(self.__on_model_year_change, Observable.ObserverEvent.VALUE_CHANGED,
|
|
95
|
+
on_transaction_end=True)
|
|
96
|
+
if self.carconnectivity_vehicle.model_year.enabled and self.model_year != self.carconnectivity_vehicle.model_year.value:
|
|
97
|
+
self.model_year = self.carconnectivity_vehicle.model_year.value
|
|
98
|
+
self.carconnectivity_vehicle.type.add_observer(self.__on_type_change, Observable.ObserverEvent.VALUE_CHANGED, on_transaction_end=True)
|
|
99
|
+
if self.carconnectivity_vehicle.type.enabled and self.carconnectivity_vehicle.type.value is not None \
|
|
100
|
+
and self.type != self.carconnectivity_vehicle.type.value:
|
|
101
|
+
self.type = self.carconnectivity_vehicle.type.value
|
|
102
|
+
self.carconnectivity_vehicle.license_plate.add_observer(self.__on_license_plate_change, Observable.ObserverEvent.VALUE_CHANGED,
|
|
103
|
+
on_transaction_end=True)
|
|
104
|
+
if self.carconnectivity_vehicle.license_plate.enabled and self.license_plate != self.carconnectivity_vehicle.license_plate.value:
|
|
105
|
+
self.license_plate = self.carconnectivity_vehicle.license_plate.value
|
|
106
|
+
|
|
107
|
+
for drive_id, drive in self.carconnectivity_vehicle.drives.drives.items():
|
|
108
|
+
drive_db: Optional[Drive] = session.query(Drive).filter(Drive.vin == self.vin, Drive.drive_id == drive_id).first()
|
|
109
|
+
if drive_db is None:
|
|
110
|
+
drive_db = Drive(vin=self.vin, drive_id=drive_id)
|
|
111
|
+
with session.begin_nested():
|
|
112
|
+
session.add(drive_db)
|
|
113
|
+
session.commit()
|
|
114
|
+
LOG.debug('Added new drive %s for vehicle %s to database', drive_id, self.vin)
|
|
115
|
+
drive_db.connect(session, drive)
|
|
116
|
+
|
|
117
|
+
state_agent: StateAgent = StateAgent(session, self) # type: ignore[assignment]
|
|
118
|
+
self.agents.append(state_agent)
|
|
119
|
+
|
|
120
|
+
def __on_name_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
|
|
121
|
+
del flags
|
|
122
|
+
if self.name != element.value:
|
|
123
|
+
self.name = element.value
|
|
124
|
+
|
|
125
|
+
def __on_manufacturer_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
|
|
126
|
+
del flags
|
|
127
|
+
if self.manufacturer != element.value:
|
|
128
|
+
self.manufacturer = element.value
|
|
129
|
+
|
|
130
|
+
def __on_model_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
|
|
131
|
+
del flags
|
|
132
|
+
if self.model != element.value:
|
|
133
|
+
self.model = element.value
|
|
134
|
+
|
|
135
|
+
def __on_model_year_change(self, element: IntegerAttribute, flags: Observable.ObserverEvent) -> None:
|
|
136
|
+
del flags
|
|
137
|
+
if self.model_year != element.value:
|
|
138
|
+
self.model_year = element.value
|
|
139
|
+
|
|
140
|
+
def __on_type_change(self, element: EnumAttribute[GenericVehicle.Type], flags: Observable.ObserverEvent) -> None:
|
|
141
|
+
del flags
|
|
142
|
+
if element.value is not None and self.type != element.value:
|
|
143
|
+
self.type = element.value
|
|
144
|
+
|
|
145
|
+
def __on_license_plate_change(self, element: StringAttribute, flags: Observable.ObserverEvent) -> None:
|
|
146
|
+
del flags
|
|
147
|
+
if self.license_plate != element.value:
|
|
148
|
+
self.license_plate = element.value
|