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.
Files changed (34) hide show
  1. carconnectivity_database/__init__.py +0 -0
  2. carconnectivity_database/carconnectivity_database_base.py +26 -0
  3. carconnectivity_plugin_database-0.1a3.dist-info/METADATA +74 -0
  4. carconnectivity_plugin_database-0.1a3.dist-info/RECORD +34 -0
  5. carconnectivity_plugin_database-0.1a3.dist-info/WHEEL +5 -0
  6. carconnectivity_plugin_database-0.1a3.dist-info/entry_points.txt +2 -0
  7. carconnectivity_plugin_database-0.1a3.dist-info/licenses/LICENSE +21 -0
  8. carconnectivity_plugin_database-0.1a3.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/drive_state_agent.py +71 -0
  13. carconnectivity_plugins/database/agents/state_agent.py +94 -0
  14. carconnectivity_plugins/database/model/__init__.py +10 -0
  15. carconnectivity_plugins/database/model/alembic.ini +100 -0
  16. carconnectivity_plugins/database/model/base.py +4 -0
  17. carconnectivity_plugins/database/model/carconnectivity_schema/README +1 -0
  18. carconnectivity_plugins/database/model/carconnectivity_schema/__init__.py +0 -0
  19. carconnectivity_plugins/database/model/carconnectivity_schema/env.py +78 -0
  20. carconnectivity_plugins/database/model/carconnectivity_schema/script.py.mako +24 -0
  21. carconnectivity_plugins/database/model/carconnectivity_schema/versions/__init__.py +0 -0
  22. carconnectivity_plugins/database/model/connection_state.py +53 -0
  23. carconnectivity_plugins/database/model/datetime_decorator.py +44 -0
  24. carconnectivity_plugins/database/model/drive.py +87 -0
  25. carconnectivity_plugins/database/model/drive_level.py +46 -0
  26. carconnectivity_plugins/database/model/drive_range.py +49 -0
  27. carconnectivity_plugins/database/model/migrations.py +24 -0
  28. carconnectivity_plugins/database/model/outside_temperature.py +48 -0
  29. carconnectivity_plugins/database/model/state.py +52 -0
  30. carconnectivity_plugins/database/model/timedelta_decorator.py +33 -0
  31. carconnectivity_plugins/database/model/vehicle.py +148 -0
  32. carconnectivity_plugins/database/plugin.py +150 -0
  33. carconnectivity_plugins/database/ui/plugin_ui.py +48 -0
  34. 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