carconnectivity-plugin-database 0.1a19__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. carconnectivity_database/__init__.py +0 -0
  2. carconnectivity_database/carconnectivity_database_base.py +26 -0
  3. carconnectivity_plugin_database-0.1a19.dist-info/METADATA +74 -0
  4. carconnectivity_plugin_database-0.1a19.dist-info/RECORD +48 -0
  5. carconnectivity_plugin_database-0.1a19.dist-info/WHEEL +5 -0
  6. carconnectivity_plugin_database-0.1a19.dist-info/entry_points.txt +2 -0
  7. carconnectivity_plugin_database-0.1a19.dist-info/licenses/LICENSE +21 -0
  8. carconnectivity_plugin_database-0.1a19.dist-info/top_level.txt +2 -0
  9. carconnectivity_plugins/database/__init__.py +0 -0
  10. carconnectivity_plugins/database/_version.py +34 -0
  11. carconnectivity_plugins/database/agents/base_agent.py +2 -0
  12. carconnectivity_plugins/database/agents/charging_agent.py +523 -0
  13. carconnectivity_plugins/database/agents/climatization_agent.py +80 -0
  14. carconnectivity_plugins/database/agents/drive_state_agent.py +368 -0
  15. carconnectivity_plugins/database/agents/state_agent.py +162 -0
  16. carconnectivity_plugins/database/agents/trip_agent.py +225 -0
  17. carconnectivity_plugins/database/model/__init__.py +19 -0
  18. carconnectivity_plugins/database/model/alembic.ini +100 -0
  19. carconnectivity_plugins/database/model/base.py +4 -0
  20. carconnectivity_plugins/database/model/carconnectivity_schema/README +1 -0
  21. carconnectivity_plugins/database/model/carconnectivity_schema/__init__.py +0 -0
  22. carconnectivity_plugins/database/model/carconnectivity_schema/env.py +78 -0
  23. carconnectivity_plugins/database/model/carconnectivity_schema/script.py.mako +24 -0
  24. carconnectivity_plugins/database/model/carconnectivity_schema/versions/__init__.py +0 -0
  25. carconnectivity_plugins/database/model/charging_power.py +52 -0
  26. carconnectivity_plugins/database/model/charging_rate.py +52 -0
  27. carconnectivity_plugins/database/model/charging_session.py +144 -0
  28. carconnectivity_plugins/database/model/charging_state.py +57 -0
  29. carconnectivity_plugins/database/model/charging_station.py +66 -0
  30. carconnectivity_plugins/database/model/climatization_state.py +57 -0
  31. carconnectivity_plugins/database/model/connection_state.py +57 -0
  32. carconnectivity_plugins/database/model/datetime_decorator.py +44 -0
  33. carconnectivity_plugins/database/model/drive.py +89 -0
  34. carconnectivity_plugins/database/model/drive_consumption.py +48 -0
  35. carconnectivity_plugins/database/model/drive_level.py +50 -0
  36. carconnectivity_plugins/database/model/drive_range.py +53 -0
  37. carconnectivity_plugins/database/model/drive_range_full.py +53 -0
  38. carconnectivity_plugins/database/model/location.py +85 -0
  39. carconnectivity_plugins/database/model/migrations.py +24 -0
  40. carconnectivity_plugins/database/model/outside_temperature.py +52 -0
  41. carconnectivity_plugins/database/model/state.py +56 -0
  42. carconnectivity_plugins/database/model/tag.py +31 -0
  43. carconnectivity_plugins/database/model/timedelta_decorator.py +33 -0
  44. carconnectivity_plugins/database/model/trip.py +85 -0
  45. carconnectivity_plugins/database/model/vehicle.py +186 -0
  46. carconnectivity_plugins/database/plugin.py +167 -0
  47. carconnectivity_plugins/database/ui/plugin_ui.py +48 -0
  48. carconnectivity_plugins/database/ui/templates/database/status.html +8 -0
@@ -0,0 +1,523 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ import threading
5
+
6
+ import logging
7
+ from datetime import timedelta
8
+
9
+ from sqlalchemy.exc import DatabaseError
10
+
11
+ from carconnectivity.observable import Observable
12
+ from carconnectivity.vehicle import ElectricVehicle
13
+ from carconnectivity.charging import Charging
14
+ from carconnectivity.charging_connector import ChargingConnector
15
+
16
+ from carconnectivity_plugins.database.agents.base_agent import BaseAgent
17
+
18
+ from carconnectivity_plugins.database.model.charging_state import ChargingState
19
+ from carconnectivity_plugins.database.model.charging_rate import ChargingRate
20
+ from carconnectivity_plugins.database.model.charging_power import ChargingPower
21
+ from carconnectivity_plugins.database.model.charging_session import ChargingSession
22
+ from carconnectivity_plugins.database.model.location import Location
23
+ from carconnectivity_plugins.database.model.charging_station import ChargingStation
24
+
25
+ if TYPE_CHECKING:
26
+ from typing import Optional
27
+ from sqlalchemy.orm import scoped_session
28
+ from sqlalchemy.orm.session import Session
29
+
30
+ from carconnectivity.attributes import EnumAttribute, SpeedAttribute, PowerAttribute
31
+
32
+ from carconnectivity_plugins.database.plugin import Plugin
33
+ from carconnectivity_plugins.database.model.vehicle import Vehicle
34
+ from carconnectivity.drive import ElectricDrive
35
+
36
+ LOG: logging.Logger = logging.getLogger("carconnectivity.plugins.database.agents.charging_agent")
37
+
38
+
39
+ class ChargingAgent(BaseAgent):
40
+
41
+ def __init__(self, database_plugin: Plugin, session_factory: scoped_session[Session], vehicle: Vehicle) -> None:
42
+ if vehicle is None or vehicle.carconnectivity_vehicle is None:
43
+ raise ValueError("Vehicle or its carconnectivity_vehicle attribute is None")
44
+ if not isinstance(vehicle.carconnectivity_vehicle, ElectricVehicle):
45
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is not an ElectricVehicle")
46
+ self.database_plugin: Plugin = database_plugin
47
+ self.session_factory: scoped_session[Session] = session_factory
48
+ self.vehicle: Vehicle = vehicle
49
+
50
+ with self.session_factory() as session:
51
+ self.last_charging_session: Optional[ChargingSession] = session.query(ChargingSession).filter(ChargingSession.vehicle == vehicle) \
52
+ .order_by(ChargingSession.session_start_date.desc().nulls_first(),
53
+ ChargingSession.plug_locked_date.desc().nulls_first(),
54
+ ChargingSession.plug_connected_date.desc().nulls_first()).first()
55
+
56
+ self.last_charging_session_lock: threading.RLock = threading.RLock()
57
+ self.carconnectivity_last_charging_state: Optional[Charging.ChargingState] = vehicle.carconnectivity_vehicle.charging.state.value
58
+ self.carconnectivity_last_connector_state: Optional[ChargingConnector.ChargingConnectorConnectionState] = vehicle.carconnectivity_vehicle.charging\
59
+ .connector.connection_state.value
60
+ self.carconnectivity_last_connector_lock_state: Optional[ChargingConnector.ChargingConnectorLockState] = vehicle.carconnectivity_vehicle.charging\
61
+ .connector.lock_state.value
62
+ if self.last_charging_session is not None and not self.last_charging_session.is_closed():
63
+ if vehicle.carconnectivity_vehicle.charging.state.value in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION) \
64
+ or (vehicle.carconnectivity_vehicle.charging.connector.connection_state.enabled
65
+ and vehicle.carconnectivity_vehicle.charging.connector.connection_state.value ==
66
+ ChargingConnector.ChargingConnectorConnectionState.CONNECTED) \
67
+ or (vehicle.carconnectivity_vehicle.charging.connector.lock_state.enabled
68
+ and vehicle.carconnectivity_vehicle.charging.connector.lock_state.value == ChargingConnector.ChargingConnectorLockState.LOCKED):
69
+ LOG.info("Last charging session for vehicle %s is still open during startup, will continue this session", vehicle.vin)
70
+ else:
71
+ LOG.info("Last charging session for vehicle %s is still open during startup, but we are not charging, ignoring it", vehicle.vin)
72
+ self.last_charging_session = None
73
+ else:
74
+ self.last_charging_session = None
75
+
76
+ self.last_charging_state: Optional[ChargingState] = session.query(ChargingState).filter(ChargingState.vehicle == vehicle)\
77
+ .order_by(ChargingState.first_date.desc()).first()
78
+ self.last_charging_state_lock: threading.RLock = threading.RLock()
79
+
80
+ self.last_charging_rate: Optional[ChargingRate] = session.query(ChargingRate).filter(ChargingRate.vehicle == vehicle)\
81
+ .order_by(ChargingRate.first_date.desc()).first()
82
+ self.last_charging_rate_lock: threading.RLock = threading.RLock()
83
+
84
+ self.last_charging_power: Optional[ChargingPower] = session.query(ChargingPower).filter(ChargingPower.vehicle == vehicle)\
85
+ .order_by(ChargingPower.first_date.desc()).first()
86
+ self.last_charging_power_lock: threading.RLock = threading.RLock()
87
+
88
+ vehicle.carconnectivity_vehicle.charging.connector.connection_state.add_observer(self.__on_connector_state_change, Observable.ObserverEvent.UPDATED)
89
+ if vehicle.carconnectivity_vehicle.charging.connector.connection_state.enabled:
90
+ self.__on_connector_state_change(vehicle.carconnectivity_vehicle.charging.connector.connection_state, Observable.ObserverEvent.UPDATED)
91
+
92
+ vehicle.carconnectivity_vehicle.charging.connector.lock_state.add_observer(self.__on_connector_lock_state_change, Observable.ObserverEvent.UPDATED)
93
+ if vehicle.carconnectivity_vehicle.charging.connector.lock_state.enabled:
94
+ self.__on_connector_lock_state_change(vehicle.carconnectivity_vehicle.charging.connector.lock_state, Observable.ObserverEvent.UPDATED)
95
+
96
+ vehicle.carconnectivity_vehicle.charging.state.add_observer(self.__on_charging_state_change, Observable.ObserverEvent.UPDATED)
97
+ if vehicle.carconnectivity_vehicle.charging.state.enabled:
98
+ self.__on_charging_state_change(vehicle.carconnectivity_vehicle.charging.state, Observable.ObserverEvent.UPDATED)
99
+
100
+ vehicle.carconnectivity_vehicle.charging.rate.add_observer(self.__on_charging_rate_change, Observable.ObserverEvent.UPDATED)
101
+ if vehicle.carconnectivity_vehicle.charging.rate.enabled:
102
+ self.__on_charging_rate_change(vehicle.carconnectivity_vehicle.charging.rate, Observable.ObserverEvent.UPDATED)
103
+
104
+ vehicle.carconnectivity_vehicle.charging.power.add_observer(self.__on_charging_power_change, Observable.ObserverEvent.UPDATED)
105
+ if vehicle.carconnectivity_vehicle.charging.power.enabled:
106
+ self.__on_charging_power_change(vehicle.carconnectivity_vehicle.charging.power, Observable.ObserverEvent.UPDATED)
107
+ self.session_factory.remove()
108
+
109
+ def __on_charging_state_change(self, element: EnumAttribute[Charging.ChargingState], flags: Observable.ObserverEvent) -> None:
110
+ del flags
111
+ if self.vehicle.carconnectivity_vehicle is None:
112
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
113
+
114
+ if element.enabled:
115
+ with self.session_factory() as session:
116
+ with self.last_charging_state_lock:
117
+ if self.last_charging_state is not None:
118
+ self.last_charging_state = session.merge(self.last_charging_state)
119
+ session.refresh(self.last_charging_state)
120
+ if (self.last_charging_state is None or self.last_charging_state.state != element.value) \
121
+ and element.last_updated is not None:
122
+ new_charging_state: ChargingState = ChargingState(vin=self.vehicle.vin, first_date=element.last_updated,
123
+ last_date=element.last_updated, state=element.value)
124
+ try:
125
+ session.add(new_charging_state)
126
+ session.commit()
127
+ LOG.debug('Added new charging state %s for vehicle %s to database', element.value, self.vehicle.vin)
128
+ self.last_charging_state = new_charging_state
129
+ except DatabaseError as err:
130
+ session.rollback()
131
+ LOG.error('DatabaseError while adding charging state for vehicle %s to database: %s', self.vehicle.vin, err)
132
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
133
+
134
+ elif self.last_charging_state is not None and self.last_charging_state.state == element.value and element.last_updated is not None:
135
+ if self.last_charging_state.last_date is None or element.last_updated > self.last_charging_state.last_date:
136
+ try:
137
+ self.last_charging_state.last_date = element.last_updated
138
+ session.commit()
139
+ LOG.debug('Updated charging state %s for vehicle %s in database', element.value, self.vehicle.vin)
140
+ except DatabaseError as err:
141
+ session.rollback()
142
+ LOG.error('DatabaseError while updating charging state for vehicle %s in database: %s', self.vehicle.vin, err)
143
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
144
+
145
+ with self.last_charging_session_lock:
146
+ if self.last_charging_session is not None:
147
+ self.last_charging_session = session.merge(self.last_charging_session)
148
+ session.refresh(self.last_charging_session)
149
+
150
+ if element.value in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION) \
151
+ and self.carconnectivity_last_charging_state not in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION):
152
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
153
+ # check that we are not resuming an old session
154
+ allowed_interrupt: timedelta = timedelta(hours=24)
155
+ # we allow longer CONSERVATION within the session
156
+ if element.value == Charging.ChargingState.CONSERVATION:
157
+ allowed_interrupt = timedelta(hours=300)
158
+ # We can reuse the session if the vehicle was connected and not disconnected in the meantime
159
+ # And the session end date was not set or is within the allowed interrupt time
160
+ if self.last_charging_session is not None \
161
+ and self.last_charging_session.was_connected() and not self.last_charging_session.was_disconnected() \
162
+ and (self.last_charging_session.session_end_date is None or element.last_changed is None
163
+ or self.last_charging_session.session_end_date > (element.last_changed - allowed_interrupt)):
164
+ LOG.debug("Continuing existing charging session for vehicle %s", self.vehicle.vin)
165
+ try:
166
+ self.last_charging_session.session_end_date = None
167
+ self.last_charging_session.end_level = None
168
+ session.commit()
169
+ except DatabaseError as err:
170
+ session.rollback()
171
+ LOG.error('DatabaseError while updating charging session for vehicle %s in database: %s', self.vehicle.vin, err)
172
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
173
+ else:
174
+ LOG.info("Starting new charging session for vehicle %s", self.vehicle.vin)
175
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, session_start_date=element.last_changed)
176
+ try:
177
+ session.add(new_session)
178
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
179
+ self._update_session_odometer(session, new_session)
180
+ self._update_session_position(session, new_session)
181
+ self._update_session_charging_type(session, new_session)
182
+ self.last_charging_session = new_session
183
+ session.commit()
184
+ except DatabaseError as err:
185
+ session.rollback()
186
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
187
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
188
+ else:
189
+ if self.last_charging_session.was_started():
190
+ LOG.debug("Continuing existing charging session for vehicle %s", self.vehicle.vin)
191
+ else:
192
+ LOG.debug("Starting charging in existing charging session for vehicle %s", self.vehicle.vin)
193
+ try:
194
+ self.last_charging_session.session_start_date = element.last_changed
195
+ self._update_session_odometer(session, self.last_charging_session)
196
+ self._update_session_position(session, self.last_charging_session)
197
+ self._update_session_charging_type(session, self.last_charging_session)
198
+ session.commit()
199
+ except DatabaseError as err:
200
+ session.rollback()
201
+ LOG.error('DatabaseError while starting charging session for vehicle %s in database: %s', self.vehicle.vin, err)
202
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
203
+ # Update startlevel at beginning of charging
204
+ if self.last_charging_session is not None and isinstance(self.vehicle.carconnectivity_vehicle, ElectricVehicle):
205
+ electric_drive: Optional[ElectricDrive] = self.vehicle.carconnectivity_vehicle.get_electric_drive()
206
+ if electric_drive is not None and electric_drive.level.enabled and electric_drive.level.value is not None:
207
+ try:
208
+ self.last_charging_session.start_level = electric_drive.level.value
209
+ session.commit()
210
+ except DatabaseError as err:
211
+ session.rollback()
212
+ LOG.error('DatabaseError while setting start level for vehicle %s in database: %s', self.vehicle.vin, err)
213
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
214
+ elif element.value not in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION) \
215
+ and self.carconnectivity_last_charging_state in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION):
216
+ if self.last_charging_session is not None and not self.last_charging_session.was_ended():
217
+ LOG.info("Ending charging session for vehicle %s", self.vehicle.vin)
218
+ try:
219
+ self.last_charging_session.session_end_date = element.last_changed
220
+ session.commit()
221
+ except DatabaseError as err:
222
+ session.rollback()
223
+ LOG.error('DatabaseError while ending charging session for vehicle %s in database: %s', self.vehicle.vin, err)
224
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
225
+ if isinstance(self.vehicle.carconnectivity_vehicle, ElectricVehicle):
226
+ electric_drive: Optional[ElectricDrive] = self.vehicle.carconnectivity_vehicle.get_electric_drive()
227
+ if electric_drive is not None and electric_drive.level.enabled and electric_drive.level.value is not None:
228
+ try:
229
+ self.last_charging_session.end_level = electric_drive.level.value
230
+ session.commit()
231
+ except DatabaseError as err:
232
+ session.rollback()
233
+ LOG.error('DatabaseError while setting start level for vehicle %s in database: %s', self.vehicle.vin, err)
234
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
235
+ self.carconnectivity_last_charging_state = element.value
236
+ self.session_factory.remove()
237
+
238
+ def __on_charging_rate_change(self, element: SpeedAttribute, flags: Observable.ObserverEvent) -> None:
239
+ del flags
240
+ if self.vehicle.carconnectivity_vehicle is None:
241
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
242
+ if element.enabled:
243
+ with self.last_charging_rate_lock:
244
+ with self.session_factory() as session:
245
+ if self.last_charging_rate is not None:
246
+ self.last_charging_rate = session.merge(self.last_charging_rate)
247
+ session.refresh(self.last_charging_rate)
248
+ if (self.last_charging_rate is None or self.last_charging_rate.rate != element.value) \
249
+ and element.last_updated is not None:
250
+ new_charging_rate: ChargingRate = ChargingRate(vin=self.vehicle.vin, first_date=element.last_updated,
251
+ last_date=element.last_updated, rate=element.value)
252
+ try:
253
+ session.add(new_charging_rate)
254
+ session.commit()
255
+ LOG.debug('Added new charging rate %s for vehicle %s to database', element.value, self.vehicle.vin)
256
+ self.last_charging_rate = new_charging_rate
257
+ except DatabaseError as err:
258
+ session.rollback()
259
+ LOG.error('DatabaseError while adding charging rate for vehicle %s to database: %s', self.vehicle.vin, err)
260
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
261
+ elif self.last_charging_rate is not None and self.last_charging_rate.rate == element.value and element.last_updated is not None:
262
+ if self.last_charging_rate.last_date is None or element.last_updated > self.last_charging_rate.last_date:
263
+ try:
264
+ self.last_charging_rate.last_date = element.last_updated
265
+ session.commit()
266
+ LOG.debug('Updated charging rate %s for vehicle %s in database', element.value, self.vehicle.vin)
267
+ except DatabaseError as err:
268
+ session.rollback()
269
+ LOG.error('DatabaseError while updating charging rate for vehicle %s in database: %s', self.vehicle.vin, err)
270
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
271
+ self.session_factory.remove()
272
+
273
+ def __on_charging_power_change(self, element: PowerAttribute, flags: Observable.ObserverEvent) -> None:
274
+ del flags
275
+ if self.vehicle.carconnectivity_vehicle is None:
276
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
277
+ if element.enabled:
278
+ with self.last_charging_power_lock:
279
+ with self.session_factory() as session:
280
+ if self.last_charging_power is not None:
281
+ self.last_charging_power = session.merge(self.last_charging_power)
282
+ session.refresh(self.last_charging_power)
283
+ if (self.last_charging_power is None or self.last_charging_power.power != element.value) \
284
+ and element.last_updated is not None:
285
+ new_charging_power: ChargingPower = ChargingPower(vin=self.vehicle.vin, first_date=element.last_updated,
286
+ last_date=element.last_updated, power=element.value)
287
+ try:
288
+ session.add(new_charging_power)
289
+ session.commit()
290
+ LOG.debug('Added new charging power %s for vehicle %s to database', element.value, self.vehicle.vin)
291
+ self.last_charging_power = new_charging_power
292
+ except DatabaseError as err:
293
+ session.rollback()
294
+ LOG.error('DatabaseError while adding charging power for vehicle %s to database: %s', self.vehicle.vin, err)
295
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
296
+ elif self.last_charging_power is not None and self.last_charging_power.power == element.value and element.last_updated is not None:
297
+ if self.last_charging_power.last_date is None or element.last_updated > self.last_charging_power.last_date:
298
+ try:
299
+ self.last_charging_power.last_date = element.last_updated
300
+ LOG.debug('Updated charging power %s for vehicle %s in database', element.value, self.vehicle.vin)
301
+ except DatabaseError as err:
302
+ session.rollback()
303
+ LOG.error('DatabaseError while updating charging power for vehicle %s in database: %s', self.vehicle.vin, err)
304
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
305
+ self.session_factory.remove()
306
+
307
+ def __on_connector_state_change(self, element: EnumAttribute[ChargingConnector.ChargingConnectorConnectionState], flags: Observable.ObserverEvent) -> None:
308
+ del flags
309
+ if self.vehicle.carconnectivity_vehicle is None:
310
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
311
+
312
+ with self.session_factory() as session:
313
+ with self.last_charging_session_lock:
314
+ if self.last_charging_session is not None:
315
+ self.last_charging_session = session.merge(self.last_charging_session)
316
+ session.refresh(self.last_charging_session)
317
+
318
+ if element.value == ChargingConnector.ChargingConnectorConnectionState.CONNECTED \
319
+ and self.carconnectivity_last_connector_state != ChargingConnector.ChargingConnectorConnectionState.CONNECTED:
320
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
321
+ LOG.info("Starting new charging session for vehicle %s due to connector connected state", self.vehicle.vin)
322
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, plug_connected_date=element.last_changed)
323
+ try:
324
+ session.add(new_session)
325
+ self._update_session_odometer(session, new_session)
326
+ self._update_session_position(session, new_session)
327
+ session.commit()
328
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
329
+ self.last_charging_session = new_session
330
+ except DatabaseError as err:
331
+ session.rollback()
332
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
333
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
334
+ elif not self.last_charging_session.was_connected():
335
+ LOG.debug("Continuing existing charging session for vehicle %s, writing connected date", self.vehicle.vin)
336
+ try:
337
+ self.last_charging_session.plug_connected_date = element.last_changed
338
+ self._update_session_odometer(session, self.last_charging_session)
339
+ self._update_session_position(session, self.last_charging_session)
340
+ session.commit()
341
+ except DatabaseError as err:
342
+ session.rollback()
343
+ LOG.error('DatabaseError while starting charging session for vehicle %s in database: %s', self.vehicle.vin, err)
344
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
345
+ elif element.value != ChargingConnector.ChargingConnectorConnectionState.CONNECTED \
346
+ and self.carconnectivity_last_connector_state == ChargingConnector.ChargingConnectorConnectionState.CONNECTED:
347
+ if self.last_charging_session is not None and not self.last_charging_session.was_disconnected():
348
+ LOG.info("Writing plug disconnected date for charging session of vehicle %s", self.vehicle.vin)
349
+ try:
350
+ self.last_charging_session.plug_disconnected_date = element.last_changed
351
+ session.commit()
352
+ except DatabaseError as err:
353
+ session.rollback()
354
+ LOG.error('DatabaseError while ending charging session for vehicle %s in database: %s', self.vehicle.vin, err)
355
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
356
+ # Create charging session when connected at startup
357
+ elif element.value == ChargingConnector.ChargingConnectorConnectionState.CONNECTED \
358
+ and self.carconnectivity_last_connector_state == ChargingConnector.ChargingConnectorConnectionState.CONNECTED:
359
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
360
+ LOG.info("Starting new charging session for vehicle %s due to connector connected state", self.vehicle.vin)
361
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, plug_connected_date=element.last_changed)
362
+ try:
363
+ session.add(new_session)
364
+ self._update_session_odometer(session, new_session)
365
+ self._update_session_position(session, new_session)
366
+ session.commit()
367
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
368
+ self.last_charging_session = new_session
369
+ except DatabaseError as err:
370
+ session.rollback()
371
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
372
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
373
+ elif self.last_charging_session is not None and not self.last_charging_session.was_connected():
374
+ try:
375
+ self.last_charging_session.plug_connected_date = element.last_changed
376
+ session.commit()
377
+ LOG.info("Writing plug connected date for charging session of vehicle %s", self.vehicle.vin)
378
+ except DatabaseError as err:
379
+ session.rollback()
380
+ LOG.error('DatabaseError while changing charging session for vehicle %s to database: %s', self.vehicle.vin, err)
381
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
382
+ self.carconnectivity_last_connector_state = element.value
383
+ self.session_factory.remove()
384
+
385
+ def __on_connector_lock_state_change(self, element: EnumAttribute[ChargingConnector.ChargingConnectorLockState], flags: Observable.ObserverEvent) -> None:
386
+ del flags
387
+ if self.vehicle.carconnectivity_vehicle is None:
388
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
389
+
390
+ with self.session_factory() as session:
391
+ with self.last_charging_session_lock:
392
+ if self.last_charging_session is not None:
393
+ self.last_charging_session = session.merge(self.last_charging_session)
394
+ session.refresh(self.last_charging_session)
395
+
396
+ if element.value == ChargingConnector.ChargingConnectorLockState.LOCKED \
397
+ and self.carconnectivity_last_connector_lock_state != ChargingConnector.ChargingConnectorLockState.LOCKED:
398
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
399
+ LOG.info("Starting new charging session for vehicle %s due to connector locked state", self.vehicle.vin)
400
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, plug_locked_date=element.last_changed)
401
+ try:
402
+ session.add(new_session)
403
+ self._update_session_odometer(session, new_session)
404
+ self._update_session_position(session, new_session)
405
+ session.commit()
406
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
407
+ self.last_charging_session = new_session
408
+ except DatabaseError as err:
409
+ session.rollback()
410
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
411
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
412
+ elif not self.last_charging_session.was_locked():
413
+ LOG.debug("Continuing existing charging session for vehicle %s, writing locked date", self.vehicle.vin)
414
+ try:
415
+ self.last_charging_session.plug_locked_date = element.last_changed
416
+ self._update_session_odometer(session, self.last_charging_session)
417
+ self._update_session_position(session, self.last_charging_session)
418
+ session.commit()
419
+ except DatabaseError as err:
420
+ session.rollback()
421
+ LOG.error('DatabaseError while starting charging session for vehicle %s in database: %s', self.vehicle.vin, err)
422
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
423
+ elif element.value != ChargingConnector.ChargingConnectorLockState.LOCKED \
424
+ and self.carconnectivity_last_connector_lock_state == ChargingConnector.ChargingConnectorLockState.LOCKED:
425
+ if self.last_charging_session is not None and not self.last_charging_session.was_unlocked():
426
+ LOG.info("Writing plug unlocked date for charging session of vehicle %s", self.vehicle.vin)
427
+ try:
428
+ self.last_charging_session.plug_unlocked_date = element.last_changed
429
+ session.commit()
430
+ except DatabaseError as err:
431
+ session.rollback()
432
+ LOG.error('DatabaseError while ending charging session for vehicle %s in database: %s', self.vehicle.vin, err)
433
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
434
+ # Create charging session when locked at startup
435
+ elif element.value == ChargingConnector.ChargingConnectorLockState.LOCKED \
436
+ and self.carconnectivity_last_connector_lock_state == ChargingConnector.ChargingConnectorLockState.LOCKED:
437
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
438
+ LOG.info("Starting new charging session for vehicle %s due to connector locked state", self.vehicle.vin)
439
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, plug_locked_date=element.last_changed)
440
+ try:
441
+ session.add(new_session)
442
+ self._update_session_odometer(session, new_session)
443
+ self._update_session_position(session, new_session)
444
+ session.commit()
445
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
446
+ self.last_charging_session = new_session
447
+ except DatabaseError as err:
448
+ session.rollback()
449
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
450
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
451
+ elif self.last_charging_session is not None and not self.last_charging_session.was_locked():
452
+ try:
453
+ self.last_charging_session.plug_locked_date = element.last_changed
454
+ session.commit()
455
+ LOG.info("Writing plug locked date for charging session of vehicle %s", self.vehicle.vin)
456
+ except DatabaseError as err:
457
+ session.rollback()
458
+ LOG.error('DatabaseError while changing charging session for vehicle %s to database: %s', self.vehicle.vin, err)
459
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
460
+ self.carconnectivity_last_connector_lock_state = element.value
461
+ self.session_factory.remove()
462
+
463
+ def _update_session_odometer(self, session: Session, charging_session: ChargingSession) -> None:
464
+ if self.vehicle.carconnectivity_vehicle is None:
465
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
466
+ if self.vehicle.carconnectivity_vehicle.odometer.enabled:
467
+ if charging_session.session_odometer is None:
468
+ try:
469
+ charging_session.session_odometer = self.vehicle.carconnectivity_vehicle.odometer.value
470
+ except DatabaseError as err:
471
+ session.rollback()
472
+ LOG.error('DatabaseError while updating odometer for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
473
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
474
+
475
+ def _update_session_charging_type(self, session: Session, charging_session: ChargingSession) -> None:
476
+ if self.vehicle.carconnectivity_vehicle is None:
477
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
478
+ if isinstance(self.vehicle.carconnectivity_vehicle, ElectricVehicle) and self.vehicle.carconnectivity_vehicle.charging.type.enabled \
479
+ and self.vehicle.carconnectivity_vehicle.charging.type.value is not None:
480
+ if charging_session.charging_type is None:
481
+ try:
482
+ charging_session.charging_type = self.vehicle.carconnectivity_vehicle.charging.type.value
483
+ except DatabaseError as err:
484
+ session.rollback()
485
+ LOG.error('DatabaseError while updating charging type for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
486
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
487
+
488
+ def _update_session_position(self, session: Session, charging_session: ChargingSession) -> None:
489
+ if self.vehicle.carconnectivity_vehicle is None:
490
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
491
+ if self.vehicle.carconnectivity_vehicle.position.enabled and self.vehicle.carconnectivity_vehicle.position.latitude.enabled \
492
+ and self.vehicle.carconnectivity_vehicle.position.longitude.enabled \
493
+ and self.vehicle.carconnectivity_vehicle.position.latitude.value is not None \
494
+ and self.vehicle.carconnectivity_vehicle.position.longitude.value is not None:
495
+ if charging_session.session_position_latitude is None and charging_session.session_position_longitude is None:
496
+ try:
497
+ charging_session.session_position_latitude = self.vehicle.carconnectivity_vehicle.position.latitude.value
498
+ charging_session.session_position_longitude = self.vehicle.carconnectivity_vehicle.position.longitude.value
499
+ except DatabaseError as err:
500
+ session.rollback()
501
+ LOG.error('DatabaseError while updating position for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
502
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
503
+ if charging_session.location is None and self.vehicle.carconnectivity_vehicle.position.location.enabled:
504
+ location: Location = Location.from_carconnectivity_location(location=self.vehicle.carconnectivity_vehicle.position.location)
505
+ try:
506
+ location = session.merge(location)
507
+ charging_session.location = location
508
+ except DatabaseError as err:
509
+ session.rollback()
510
+ LOG.error('DatabaseError while merging location for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
511
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
512
+ if charging_session.charging_station is None \
513
+ and isinstance(self.vehicle.carconnectivity_vehicle, ElectricVehicle) and self.vehicle.carconnectivity_vehicle.charging is not None \
514
+ and self.vehicle.carconnectivity_vehicle.charging.enabled and self.vehicle.carconnectivity_vehicle.charging.charging_station.enabled:
515
+ charging_station: ChargingStation = ChargingStation.from_carconnectivity_charging_station(
516
+ charging_station=self.vehicle.carconnectivity_vehicle.charging.charging_station)
517
+ try:
518
+ charging_station = session.merge(charging_station)
519
+ charging_session.charging_station = charging_station
520
+ except DatabaseError as err:
521
+ session.rollback()
522
+ LOG.error('DatabaseError while merging charging station for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
523
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ import threading
5
+
6
+ import logging
7
+
8
+ from sqlalchemy.exc import DatabaseError
9
+
10
+ from carconnectivity.observable import Observable
11
+
12
+ from carconnectivity_plugins.database.agents.base_agent import BaseAgent
13
+ from carconnectivity_plugins.database.model.climatization_state import ClimatizationState
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Optional
17
+ from sqlalchemy.orm import scoped_session
18
+ from sqlalchemy.orm.session import Session
19
+
20
+ from carconnectivity.attributes import EnumAttribute
21
+
22
+ from carconnectivity.climatization import Climatization
23
+
24
+ from carconnectivity_plugins.database.plugin import Plugin
25
+ from carconnectivity_plugins.database.model.vehicle import Vehicle
26
+
27
+
28
+ LOG: logging.Logger = logging.getLogger("carconnectivity.plugins.database.agents.climatization_agent")
29
+
30
+
31
+ class ClimatizationAgent(BaseAgent):
32
+ def __init__(self, database_plugin: Plugin, session_factory: scoped_session[Session], vehicle: Vehicle) -> None:
33
+ if vehicle is None or vehicle.carconnectivity_vehicle is None:
34
+ raise ValueError("Vehicle or its carconnectivity_vehicle attribute is None")
35
+ self.database_plugin: Plugin = database_plugin
36
+ self.session_factory: scoped_session[Session] = session_factory
37
+ self.vehicle: Vehicle = vehicle
38
+
39
+ with self.session_factory() as session:
40
+ self.last_state: Optional[ClimatizationState] = session.query(ClimatizationState).filter(ClimatizationState.vehicle == vehicle)\
41
+ .order_by(ClimatizationState.first_date.desc()).first()
42
+ self.last_state_lock: threading.RLock = threading.RLock()
43
+
44
+ vehicle.carconnectivity_vehicle.climatization.state.add_observer(self.__on_state_change, Observable.ObserverEvent.UPDATED)
45
+ self.__on_state_change(vehicle.carconnectivity_vehicle.climatization.state, Observable.ObserverEvent.UPDATED)
46
+ self.session_factory.remove()
47
+
48
+ def __on_state_change(self, element: EnumAttribute[Climatization.ClimatizationState], flags: Observable.ObserverEvent) -> None:
49
+ del flags
50
+ if element.enabled:
51
+ with self.last_state_lock:
52
+ with self.session_factory() as session:
53
+ if self.last_state is not None:
54
+ self.last_state = session.merge(self.last_state)
55
+ session.refresh(self.last_state)
56
+ if (self.last_state is None or self.last_state.state != element.value) \
57
+ and element.last_updated is not None:
58
+ new_state: ClimatizationState = ClimatizationState(vin=self.vehicle.vin, first_date=element.last_updated,
59
+ last_date=element.last_updated, state=element.value)
60
+ try:
61
+ session.add(new_state)
62
+ session.commit()
63
+ LOG.debug('Added new climatization state %s for vehicle %s to database', element.value, self.vehicle.vin)
64
+ self.last_state = new_state
65
+ except DatabaseError as err:
66
+ session.rollback()
67
+ LOG.error('DatabaseError while adding climatizationstate for vehicle %s to database: %s', self.vehicle.vin, err)
68
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
69
+
70
+ elif self.last_state is not None and self.last_state.state == element.value and element.last_updated is not None:
71
+ if self.last_state.last_date is None or element.last_updated > self.last_state.last_date:
72
+ try:
73
+ self.last_state.last_date = element.last_updated
74
+ session.commit()
75
+ LOG.debug('Updated climatizationstate %s for vehicle %s in database', element.value, self.vehicle.vin)
76
+ except DatabaseError as err:
77
+ session.rollback()
78
+ LOG.error('DatabaseError while updating climatizationstate for vehicle %s in database: %s', self.vehicle.vin, err)
79
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
80
+ self.session_factory.remove()