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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of carconnectivity-plugin-database might be problematic. Click here for more details.

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.1.dist-info/METADATA +74 -0
  4. carconnectivity_plugin_database-0.1.dist-info/RECORD +48 -0
  5. carconnectivity_plugin_database-0.1.dist-info/WHEEL +5 -0
  6. carconnectivity_plugin_database-0.1.dist-info/entry_points.txt +2 -0
  7. carconnectivity_plugin_database-0.1.dist-info/licenses/LICENSE +21 -0
  8. carconnectivity_plugin_database-0.1.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 +610 -0
  13. carconnectivity_plugins/database/agents/climatization_agent.py +83 -0
  14. carconnectivity_plugins/database/agents/drive_state_agent.py +378 -0
  15. carconnectivity_plugins/database/agents/state_agent.py +166 -0
  16. carconnectivity_plugins/database/agents/trip_agent.py +227 -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 +86 -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 +184 -0
  46. carconnectivity_plugins/database/plugin.py +161 -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,610 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ import threading
5
+
6
+ import logging
7
+ from datetime import timedelta, datetime, timezone
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, FloatAttribute
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,
42
+ carconnectivity_vehicle: ElectricVehicle) -> None:
43
+ if vehicle is None or carconnectivity_vehicle is None:
44
+ raise ValueError("Vehicle or its carconnectivity_vehicle attribute is None")
45
+ if not isinstance(carconnectivity_vehicle, ElectricVehicle):
46
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is not an ElectricVehicle")
47
+ self.database_plugin: Plugin = database_plugin
48
+ self.session_factory: scoped_session[Session] = session_factory
49
+ self.vehicle: Vehicle = vehicle
50
+ self.carconnectivity_vehicle: ElectricVehicle = carconnectivity_vehicle
51
+
52
+ with self.session_factory() as session:
53
+ self.last_charging_session: Optional[ChargingSession] = session.query(ChargingSession).filter(ChargingSession.vehicle == vehicle) \
54
+ .order_by(ChargingSession.session_start_date.desc().nulls_first(),
55
+ ChargingSession.plug_locked_date.desc().nulls_first(),
56
+ ChargingSession.plug_connected_date.desc().nulls_first()).first()
57
+
58
+ self.last_charging_session_lock: threading.RLock = threading.RLock()
59
+ self.carconnectivity_last_charging_state: Optional[Charging.ChargingState] = self.carconnectivity_vehicle.charging.state.value
60
+ self.carconnectivity_last_connector_state: Optional[ChargingConnector.ChargingConnectorConnectionState] = self.carconnectivity_vehicle.charging\
61
+ .connector.connection_state.value
62
+ self.carconnectivity_last_connector_lock_state: Optional[ChargingConnector.ChargingConnectorLockState] = self.carconnectivity_vehicle.charging\
63
+ .connector.lock_state.value
64
+ if self.last_charging_session is not None and not self.last_charging_session.is_closed():
65
+ if self.carconnectivity_vehicle.charging.state.value in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION) \
66
+ or (self.carconnectivity_vehicle.charging.connector.connection_state.enabled
67
+ and self.carconnectivity_vehicle.charging.connector.connection_state.value ==
68
+ ChargingConnector.ChargingConnectorConnectionState.CONNECTED) \
69
+ or (self.carconnectivity_vehicle.charging.connector.lock_state.enabled
70
+ and self.carconnectivity_vehicle.charging.connector.lock_state.value == ChargingConnector.ChargingConnectorLockState.LOCKED):
71
+ LOG.info("Last charging session for vehicle %s is still open during startup, will continue this session", vehicle.vin)
72
+ else:
73
+ LOG.info("Last charging session for vehicle %s is still open during startup, but we are not charging, ignoring it", vehicle.vin)
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
+ self.carconnectivity_vehicle.charging.connector.connection_state.add_observer(self.__on_connector_state_change, Observable.ObserverEvent.UPDATED)
89
+ if self.carconnectivity_vehicle.charging.connector.connection_state.enabled:
90
+ self.__on_connector_state_change(self.carconnectivity_vehicle.charging.connector.connection_state, Observable.ObserverEvent.UPDATED)
91
+
92
+ self.carconnectivity_vehicle.charging.connector.lock_state.add_observer(self.__on_connector_lock_state_change, Observable.ObserverEvent.UPDATED)
93
+ if self.carconnectivity_vehicle.charging.connector.lock_state.enabled:
94
+ self.__on_connector_lock_state_change(self.carconnectivity_vehicle.charging.connector.lock_state, Observable.ObserverEvent.UPDATED)
95
+
96
+ self.carconnectivity_vehicle.charging.state.add_observer(self.__on_charging_state_change, Observable.ObserverEvent.UPDATED)
97
+ if self.carconnectivity_vehicle.charging.state.enabled:
98
+ self.__on_charging_state_change(self.carconnectivity_vehicle.charging.state, Observable.ObserverEvent.UPDATED)
99
+
100
+ self.carconnectivity_vehicle.charging.rate.add_observer(self.__on_charging_rate_change, Observable.ObserverEvent.UPDATED)
101
+ if self.carconnectivity_vehicle.charging.rate.enabled:
102
+ self.__on_charging_rate_change(self.carconnectivity_vehicle.charging.rate, Observable.ObserverEvent.UPDATED)
103
+
104
+ self.carconnectivity_vehicle.charging.power.add_observer(self.__on_charging_power_change, Observable.ObserverEvent.UPDATED)
105
+ if self.carconnectivity_vehicle.charging.power.enabled:
106
+ self.__on_charging_power_change(self.carconnectivity_vehicle.charging.power, Observable.ObserverEvent.UPDATED)
107
+
108
+ self.carconnectivity_vehicle.charging.type.add_observer(self._on_charging_type_change, Observable.ObserverEvent.VALUE_CHANGED)
109
+
110
+ electric_drive: Optional[ElectricDrive] = self.carconnectivity_vehicle.get_electric_drive()
111
+ if electric_drive is not None:
112
+ electric_drive.level.add_observer(self._on_battery_level_change, Observable.ObserverEvent.VALUE_CHANGED)
113
+ self.session_factory.remove()
114
+
115
+ def __on_charging_state_change(self, element: EnumAttribute[Charging.ChargingState], flags: Observable.ObserverEvent) -> None:
116
+ del flags
117
+ if self.carconnectivity_vehicle is None:
118
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
119
+
120
+ if element.enabled:
121
+ with self.session_factory() as session:
122
+ with self.last_charging_state_lock:
123
+ if self.last_charging_state is not None:
124
+ self.last_charging_state = session.merge(self.last_charging_state)
125
+ session.refresh(self.last_charging_state)
126
+ if element.last_updated is not None \
127
+ and (self.last_charging_state is None or (self.last_charging_state.state != element.value
128
+ and element.last_updated > self.last_charging_state.last_date)):
129
+ new_charging_state: ChargingState = ChargingState(vin=self.vehicle.vin, first_date=element.last_updated,
130
+ last_date=element.last_updated, state=element.value)
131
+ try:
132
+ session.add(new_charging_state)
133
+ session.commit()
134
+ LOG.debug('Added new charging state %s for vehicle %s to database', element.value, self.vehicle.vin)
135
+ self.last_charging_state = new_charging_state
136
+ except DatabaseError as err:
137
+ session.rollback()
138
+ LOG.error('DatabaseError while adding charging state for vehicle %s to database: %s', self.vehicle.vin, err)
139
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
140
+
141
+ elif self.last_charging_state is not None and self.last_charging_state.state == element.value and element.last_updated is not None:
142
+ if self.last_charging_state.last_date is None or element.last_updated > self.last_charging_state.last_date:
143
+ try:
144
+ self.last_charging_state.last_date = element.last_updated
145
+ session.commit()
146
+ LOG.debug('Updated charging state %s for vehicle %s in database', element.value, self.vehicle.vin)
147
+ except DatabaseError as err:
148
+ session.rollback()
149
+ LOG.error('DatabaseError while updating charging state for vehicle %s in database: %s', self.vehicle.vin, err)
150
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
151
+
152
+ with self.last_charging_session_lock:
153
+ if self.last_charging_session is not None:
154
+ self.last_charging_session = session.merge(self.last_charging_session)
155
+ session.refresh(self.last_charging_session)
156
+
157
+ if element.value in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION) \
158
+ and self.carconnectivity_last_charging_state not in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION):
159
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
160
+ # check that we are not resuming an old session
161
+ allowed_interrupt: timedelta = timedelta(hours=24)
162
+ # we allow longer CONSERVATION within the session
163
+ if element.value == Charging.ChargingState.CONSERVATION:
164
+ allowed_interrupt = timedelta(hours=300)
165
+ # We can reuse the session if the vehicle was connected and not disconnected in the meantime
166
+ # And the session end date was not set or is within the allowed interrupt time
167
+ if self.last_charging_session is not None \
168
+ and self.last_charging_session.was_connected() and not self.last_charging_session.was_disconnected() \
169
+ and (self.last_charging_session.session_end_date is None or element.last_changed is None
170
+ or self.last_charging_session.session_end_date > (element.last_changed - allowed_interrupt)):
171
+ LOG.debug("Continuing existing charging session for vehicle %s", self.vehicle.vin)
172
+ try:
173
+ self.last_charging_session.session_end_date = None
174
+ self.last_charging_session.end_level = None
175
+ self._update_session_charging_type(session, self.last_charging_session)
176
+ session.commit()
177
+ except DatabaseError as err:
178
+ session.rollback()
179
+ LOG.error('DatabaseError while updating charging session for vehicle %s in database: %s', self.vehicle.vin, err)
180
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
181
+ else:
182
+ LOG.info("Starting new charging session for vehicle %s", self.vehicle.vin)
183
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, session_start_date=element.last_changed)
184
+ try:
185
+ session.add(new_session)
186
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
187
+ self._update_session_odometer(session, new_session)
188
+ self._update_session_position(session, new_session)
189
+ self._update_session_charging_type(session, new_session)
190
+ self.last_charging_session = new_session
191
+ session.commit()
192
+ except DatabaseError as err:
193
+ session.rollback()
194
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
195
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
196
+ else:
197
+ if self.last_charging_session.was_started():
198
+ LOG.debug("Continuing existing charging session for vehicle %s", self.vehicle.vin)
199
+ else:
200
+ LOG.debug("Starting charging in existing charging session for vehicle %s", self.vehicle.vin)
201
+ try:
202
+ self.last_charging_session.session_start_date = element.last_changed
203
+ self._update_session_odometer(session, self.last_charging_session)
204
+ self._update_session_position(session, self.last_charging_session)
205
+ self._update_session_charging_type(session, self.last_charging_session)
206
+ session.commit()
207
+ except DatabaseError as err:
208
+ session.rollback()
209
+ LOG.error('DatabaseError while starting charging session for vehicle %s in database: %s', self.vehicle.vin, err)
210
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
211
+ # Update startlevel at beginning of charging
212
+ if self.last_charging_session is not None and isinstance(self.carconnectivity_vehicle, ElectricVehicle):
213
+ electric_drive: Optional[ElectricDrive] = self.carconnectivity_vehicle.get_electric_drive()
214
+ if electric_drive is not None and electric_drive.level.enabled and electric_drive.level.value is not None:
215
+ try:
216
+ self.last_charging_session.start_level = electric_drive.level.value
217
+ session.commit()
218
+ except DatabaseError as err:
219
+ session.rollback()
220
+ LOG.error('DatabaseError while setting start level for vehicle %s in database: %s', self.vehicle.vin, err)
221
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
222
+ elif element.value not in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION) \
223
+ and self.carconnectivity_last_charging_state in (Charging.ChargingState.CHARGING, Charging.ChargingState.CONSERVATION):
224
+ if self.last_charging_session is not None and not self.last_charging_session.was_ended():
225
+ LOG.info("Ending charging session for vehicle %s", self.vehicle.vin)
226
+ try:
227
+ self.last_charging_session.session_end_date = element.last_changed
228
+ session.commit()
229
+ except DatabaseError as err:
230
+ session.rollback()
231
+ LOG.error('DatabaseError while ending charging session for vehicle %s in database: %s', self.vehicle.vin, err)
232
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
233
+ if isinstance(self.carconnectivity_vehicle, ElectricVehicle):
234
+ electric_drive: Optional[ElectricDrive] = self.carconnectivity_vehicle.get_electric_drive()
235
+ if electric_drive is not None and electric_drive.level.enabled and electric_drive.level.value is not None:
236
+ try:
237
+ self.last_charging_session.end_level = electric_drive.level.value
238
+ session.commit()
239
+ except DatabaseError as err:
240
+ session.rollback()
241
+ LOG.error('DatabaseError while setting start level for vehicle %s in database: %s', self.vehicle.vin, err)
242
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
243
+ self.carconnectivity_last_charging_state = element.value
244
+ self.session_factory.remove()
245
+
246
+ def __on_charging_rate_change(self, element: SpeedAttribute, flags: Observable.ObserverEvent) -> None:
247
+ del flags
248
+ if self.carconnectivity_vehicle is None:
249
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
250
+ if element.enabled:
251
+ with self.last_charging_rate_lock:
252
+ with self.session_factory() as session:
253
+ if self.last_charging_rate is not None:
254
+ self.last_charging_rate = session.merge(self.last_charging_rate)
255
+ session.refresh(self.last_charging_rate)
256
+ if element.last_updated is not None \
257
+ and (self.last_charging_rate is None or (self.last_charging_rate.rate != element.value
258
+ and element.last_updated > self.last_charging_rate.last_date)):
259
+ new_charging_rate: ChargingRate = ChargingRate(vin=self.vehicle.vin, first_date=element.last_updated,
260
+ last_date=element.last_updated, rate=element.value)
261
+ try:
262
+ session.add(new_charging_rate)
263
+ session.commit()
264
+ LOG.debug('Added new charging rate %s for vehicle %s to database', element.value, self.vehicle.vin)
265
+ self.last_charging_rate = new_charging_rate
266
+ except DatabaseError as err:
267
+ session.rollback()
268
+ LOG.error('DatabaseError while adding charging rate for vehicle %s to database: %s', self.vehicle.vin, err)
269
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
270
+ elif self.last_charging_rate is not None and self.last_charging_rate.rate == element.value and element.last_updated is not None:
271
+ if self.last_charging_rate.last_date is None or element.last_updated > self.last_charging_rate.last_date:
272
+ try:
273
+ self.last_charging_rate.last_date = element.last_updated
274
+ session.commit()
275
+ LOG.debug('Updated charging rate %s for vehicle %s in database', element.value, self.vehicle.vin)
276
+ except DatabaseError as err:
277
+ session.rollback()
278
+ LOG.error('DatabaseError while updating charging rate for vehicle %s in database: %s', self.vehicle.vin, err)
279
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
280
+ self.session_factory.remove()
281
+
282
+ def __on_charging_power_change(self, element: PowerAttribute, flags: Observable.ObserverEvent) -> None:
283
+ del flags
284
+ if self.carconnectivity_vehicle is None:
285
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
286
+ if element.enabled:
287
+ with self.last_charging_power_lock:
288
+ with self.session_factory() as session:
289
+ if self.last_charging_power is not None:
290
+ self.last_charging_power = session.merge(self.last_charging_power)
291
+ session.refresh(self.last_charging_power)
292
+ if element.last_updated is not None \
293
+ and (self.last_charging_power is None or (self.last_charging_power.power != element.value
294
+ and element.last_updated > self.last_charging_power.last_date)):
295
+ new_charging_power: ChargingPower = ChargingPower(vin=self.vehicle.vin, first_date=element.last_updated,
296
+ last_date=element.last_updated, power=element.value)
297
+ try:
298
+ session.add(new_charging_power)
299
+ session.commit()
300
+ LOG.debug('Added new charging power %s for vehicle %s to database', element.value, self.vehicle.vin)
301
+ self.last_charging_power = new_charging_power
302
+ except DatabaseError as err:
303
+ session.rollback()
304
+ LOG.error('DatabaseError while adding charging power for vehicle %s to database: %s', self.vehicle.vin, err)
305
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
306
+ elif self.last_charging_power is not None and self.last_charging_power.power == element.value and element.last_updated is not None:
307
+ if self.last_charging_power.last_date is None or element.last_updated > self.last_charging_power.last_date:
308
+ try:
309
+ self.last_charging_power.last_date = element.last_updated
310
+ LOG.debug('Updated charging power %s for vehicle %s in database', element.value, self.vehicle.vin)
311
+ except DatabaseError as err:
312
+ session.rollback()
313
+ LOG.error('DatabaseError while updating charging power for vehicle %s in database: %s', self.vehicle.vin, err)
314
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
315
+ self.session_factory.remove()
316
+
317
+ def __on_connector_state_change(self, element: EnumAttribute[ChargingConnector.ChargingConnectorConnectionState], flags: Observable.ObserverEvent) -> None:
318
+ del flags
319
+ if self.carconnectivity_vehicle is None:
320
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
321
+
322
+ with self.session_factory() as session:
323
+ with self.last_charging_session_lock:
324
+ if self.last_charging_session is not None:
325
+ self.last_charging_session = session.merge(self.last_charging_session)
326
+ session.refresh(self.last_charging_session)
327
+
328
+ if element.value == ChargingConnector.ChargingConnectorConnectionState.CONNECTED \
329
+ and self.carconnectivity_last_connector_state is not None \
330
+ and self.carconnectivity_last_connector_state != ChargingConnector.ChargingConnectorConnectionState.CONNECTED:
331
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
332
+ LOG.info("Starting new charging session for vehicle %s due to connector connected state", self.vehicle.vin)
333
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, plug_connected_date=element.last_changed)
334
+ try:
335
+ session.add(new_session)
336
+ self._update_session_odometer(session, new_session)
337
+ self._update_session_position(session, new_session)
338
+ session.commit()
339
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
340
+ self.last_charging_session = new_session
341
+ except DatabaseError as err:
342
+ session.rollback()
343
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
344
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
345
+ elif not self.last_charging_session.was_connected():
346
+ LOG.debug("Continuing existing charging session for vehicle %s, writing connected date", self.vehicle.vin)
347
+ try:
348
+ self.last_charging_session.plug_connected_date = element.last_changed
349
+ self._update_session_odometer(session, self.last_charging_session)
350
+ self._update_session_position(session, self.last_charging_session)
351
+ session.commit()
352
+ except DatabaseError as err:
353
+ session.rollback()
354
+ LOG.error('DatabaseError while starting 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
+ elif element.value != ChargingConnector.ChargingConnectorConnectionState.CONNECTED \
357
+ and self.carconnectivity_last_connector_state == ChargingConnector.ChargingConnectorConnectionState.CONNECTED:
358
+ if self.last_charging_session is not None and not self.last_charging_session.was_disconnected():
359
+ LOG.info("Writing plug disconnected date for charging session of vehicle %s", self.vehicle.vin)
360
+ try:
361
+ self.last_charging_session.plug_disconnected_date = element.last_changed
362
+ session.commit()
363
+ except DatabaseError as err:
364
+ session.rollback()
365
+ LOG.error('DatabaseError while ending charging session for vehicle %s in database: %s', self.vehicle.vin, err)
366
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
367
+ # Create charging session when connected at startup
368
+ elif element.value == ChargingConnector.ChargingConnectorConnectionState.CONNECTED \
369
+ and self.carconnectivity_last_connector_state == ChargingConnector.ChargingConnectorConnectionState.CONNECTED:
370
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
371
+ # when the incoming connected state was during the last session, this is a continuation
372
+ if self.last_charging_session is None or element.last_changed is not None and element.last_changed > \
373
+ (self.last_charging_session.session_end_date
374
+ or self.last_charging_session.plug_unlocked_date
375
+ or datetime.min.replace(tzinfo=timezone.utc)):
376
+ LOG.info("Starting new charging session for vehicle %s due to connector connected state on startup", self.vehicle.vin)
377
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, plug_connected_date=element.last_changed)
378
+ try:
379
+ session.add(new_session)
380
+ self._update_session_odometer(session, new_session)
381
+ self._update_session_position(session, new_session)
382
+ session.commit()
383
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
384
+ self.last_charging_session = new_session
385
+ except DatabaseError as err:
386
+ session.rollback()
387
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
388
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
389
+ elif self.last_charging_session is not None and not self.last_charging_session.was_connected():
390
+ try:
391
+ self.last_charging_session.plug_connected_date = element.last_changed
392
+ session.commit()
393
+ LOG.info("Writing plug connected date for charging session of vehicle %s", self.vehicle.vin)
394
+ except DatabaseError as err:
395
+ session.rollback()
396
+ LOG.error('DatabaseError while changing charging session for vehicle %s to database: %s', self.vehicle.vin, err)
397
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
398
+ self.carconnectivity_last_connector_state = element.value
399
+ self.session_factory.remove()
400
+
401
+ def __on_connector_lock_state_change(self, element: EnumAttribute[ChargingConnector.ChargingConnectorLockState], flags: Observable.ObserverEvent) -> None:
402
+ del flags
403
+ if self.carconnectivity_vehicle is None:
404
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
405
+
406
+ with self.session_factory() as session:
407
+ with self.last_charging_session_lock:
408
+ if self.last_charging_session is not None:
409
+ self.last_charging_session = session.merge(self.last_charging_session)
410
+ session.refresh(self.last_charging_session)
411
+
412
+ if element.value == ChargingConnector.ChargingConnectorLockState.LOCKED \
413
+ and self.carconnectivity_last_connector_lock_state is not None \
414
+ and self.carconnectivity_last_connector_lock_state != ChargingConnector.ChargingConnectorLockState.LOCKED:
415
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
416
+ # In case this was an interrupted charging session (interrupt no longer than 24hours), continue by erasing end time
417
+ if self.last_charging_session is not None and not self.last_charging_session.was_disconnected() \
418
+ and (self.last_charging_session.plug_unlocked_date is None
419
+ or self.last_charging_session.plug_unlocked_date > ((element.last_changed or datetime.now(timezone.utc))
420
+ - timedelta(hours=24))):
421
+ LOG.debug("found a closed charging session that was not disconneced. This could be an interrupted session we want to continue")
422
+ try:
423
+ self.last_charging_session.plug_unlocked_date = None
424
+ session.commit()
425
+ except DatabaseError as err:
426
+ session.rollback()
427
+ LOG.error('DatabaseError while changing charging session for vehicle %s to database: %s', self.vehicle.vin, err)
428
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
429
+ else:
430
+ LOG.info("Starting new charging session for vehicle %s due to connector locked state", self.vehicle.vin)
431
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, plug_locked_date=element.last_changed)
432
+ try:
433
+ session.add(new_session)
434
+ self._update_session_odometer(session, new_session)
435
+ self._update_session_position(session, new_session)
436
+ session.commit()
437
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
438
+ self.last_charging_session = new_session
439
+ except DatabaseError as err:
440
+ session.rollback()
441
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
442
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
443
+ elif not self.last_charging_session.was_locked():
444
+ LOG.debug("Continuing existing charging session for vehicle %s, writing locked date", self.vehicle.vin)
445
+ try:
446
+ self.last_charging_session.plug_locked_date = element.last_changed
447
+ self._update_session_odometer(session, self.last_charging_session)
448
+ self._update_session_position(session, self.last_charging_session)
449
+ session.commit()
450
+ except DatabaseError as err:
451
+ session.rollback()
452
+ LOG.error('DatabaseError while starting charging session for vehicle %s in database: %s', self.vehicle.vin, err)
453
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
454
+ elif element.value != ChargingConnector.ChargingConnectorLockState.LOCKED \
455
+ and self.carconnectivity_last_connector_lock_state == ChargingConnector.ChargingConnectorLockState.LOCKED:
456
+ if self.last_charging_session is not None and not self.last_charging_session.was_unlocked():
457
+ LOG.info("Writing plug unlocked date for charging session of vehicle %s", self.vehicle.vin)
458
+ try:
459
+ self.last_charging_session.plug_unlocked_date = element.last_changed
460
+ session.commit()
461
+ except DatabaseError as err:
462
+ session.rollback()
463
+ LOG.error('DatabaseError while ending charging session for vehicle %s in database: %s', self.vehicle.vin, err)
464
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
465
+ # Create charging session when locked at startup
466
+ elif element.value == ChargingConnector.ChargingConnectorLockState.LOCKED \
467
+ and self.carconnectivity_last_connector_lock_state == ChargingConnector.ChargingConnectorLockState.LOCKED:
468
+ if self.last_charging_session is None or self.last_charging_session.is_closed():
469
+ # In case this was an interrupted charging session (interrupt no longer than 24hours), continue by erasing end time
470
+ if self.last_charging_session is not None and not self.last_charging_session.was_disconnected() \
471
+ and (self.last_charging_session.plug_unlocked_date is None
472
+ or self.last_charging_session.plug_unlocked_date > ((element.last_changed or datetime.now(timezone.utc))
473
+ - timedelta(hours=24))):
474
+ LOG.debug("found a closed charging session that was not disconneced. This could be an interrupted session we want to continue")
475
+ try:
476
+ self.last_charging_session.plug_unlocked_date = None
477
+ session.commit()
478
+ except DatabaseError as err:
479
+ session.rollback()
480
+ LOG.error('DatabaseError while changing charging session for vehicle %s to database: %s', self.vehicle.vin, err)
481
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
482
+ else:
483
+ LOG.info("Starting new charging session for vehicle %s due to connector locked state on startup", self.vehicle.vin)
484
+ new_session: ChargingSession = ChargingSession(vin=self.vehicle.vin, plug_locked_date=element.last_changed)
485
+ try:
486
+ session.add(new_session)
487
+ self._update_session_odometer(session, new_session)
488
+ self._update_session_position(session, new_session)
489
+ session.commit()
490
+ LOG.debug('Added new charging session for vehicle %s to database', self.vehicle.vin)
491
+ self.last_charging_session = new_session
492
+ except DatabaseError as err:
493
+ session.rollback()
494
+ LOG.error('DatabaseError while adding charging session for vehicle %s to database: %s', self.vehicle.vin, err)
495
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
496
+ elif self.last_charging_session is not None and not self.last_charging_session.was_locked():
497
+ try:
498
+ self.last_charging_session.plug_locked_date = element.last_changed
499
+ session.commit()
500
+ LOG.info("Writing plug locked date for charging session of vehicle %s", self.vehicle.vin)
501
+ except DatabaseError as err:
502
+ session.rollback()
503
+ LOG.error('DatabaseError while changing charging session for vehicle %s to database: %s', self.vehicle.vin, err)
504
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
505
+ self.carconnectivity_last_connector_lock_state = element.value
506
+ self.session_factory.remove()
507
+
508
+ def _update_session_odometer(self, session: Session, charging_session: ChargingSession) -> None:
509
+ if self.carconnectivity_vehicle is None:
510
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
511
+ if self.carconnectivity_vehicle.odometer.enabled:
512
+ if charging_session.session_odometer is None:
513
+ try:
514
+ charging_session.session_odometer = self.carconnectivity_vehicle.odometer.value
515
+ except DatabaseError as err:
516
+ session.rollback()
517
+ LOG.error('DatabaseError while updating odometer for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
518
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
519
+
520
+ def _update_session_charging_type(self, session: Session, charging_session: ChargingSession) -> None:
521
+ if self.carconnectivity_vehicle is None:
522
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
523
+ if isinstance(self.carconnectivity_vehicle, ElectricVehicle) and self.carconnectivity_vehicle.charging.type.enabled \
524
+ and self.carconnectivity_vehicle.charging.type.value is not None:
525
+ if charging_session.charging_type is None:
526
+ try:
527
+ charging_session.charging_type = self.carconnectivity_vehicle.charging.type.value
528
+ except DatabaseError as err:
529
+ session.rollback()
530
+ LOG.error('DatabaseError while updating charging type for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
531
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
532
+
533
+ def _update_session_position(self, session: Session, charging_session: ChargingSession) -> None:
534
+ if self.carconnectivity_vehicle is None:
535
+ raise ValueError("Vehicle's carconnectivity_vehicle attribute is None")
536
+ if self.carconnectivity_vehicle.position.enabled and self.carconnectivity_vehicle.position.latitude.enabled \
537
+ and self.carconnectivity_vehicle.position.longitude.enabled \
538
+ and self.carconnectivity_vehicle.position.latitude.value is not None \
539
+ and self.carconnectivity_vehicle.position.longitude.value is not None:
540
+ if charging_session.session_position_latitude is None and charging_session.session_position_longitude is None:
541
+ try:
542
+ charging_session.session_position_latitude = self.carconnectivity_vehicle.position.latitude.value
543
+ charging_session.session_position_longitude = self.carconnectivity_vehicle.position.longitude.value
544
+ except DatabaseError as err:
545
+ session.rollback()
546
+ LOG.error('DatabaseError while updating position for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
547
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
548
+ if charging_session.location is None and self.carconnectivity_vehicle.position.location.enabled:
549
+ location: Location = Location.from_carconnectivity_location(location=self.carconnectivity_vehicle.position.location)
550
+ try:
551
+ location = session.merge(location)
552
+ charging_session.location = location
553
+ except DatabaseError as err:
554
+ session.rollback()
555
+ LOG.error('DatabaseError while merging location for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
556
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
557
+ if charging_session.charging_station is None \
558
+ and isinstance(self.carconnectivity_vehicle, ElectricVehicle) and self.carconnectivity_vehicle.charging is not None \
559
+ and self.carconnectivity_vehicle.charging.enabled and self.carconnectivity_vehicle.charging.charging_station.enabled:
560
+ charging_station: ChargingStation = ChargingStation.from_carconnectivity_charging_station(
561
+ charging_station=self.carconnectivity_vehicle.charging.charging_station)
562
+ try:
563
+ charging_station = session.merge(charging_station)
564
+ charging_session.charging_station = charging_station
565
+ except DatabaseError as err:
566
+ session.rollback()
567
+ LOG.error('DatabaseError while merging charging station for charging session of vehicle %s in database: %s', self.vehicle.vin, err)
568
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
569
+
570
+ def _on_charging_type_change(self, element: EnumAttribute[Charging.ChargingType], flags: Observable.ObserverEvent) -> None:
571
+ del flags
572
+ if element.enabled:
573
+ with self.session_factory() as session:
574
+ with self.last_charging_session_lock:
575
+ if self.last_charging_session is not None:
576
+ self.last_charging_session = session.merge(self.last_charging_session)
577
+ session.refresh(self.last_charging_session)
578
+ if self.last_charging_session is not None and not self.last_charging_session.is_closed() \
579
+ and element.value in [Charging.ChargingType.AC, Charging.ChargingType.DC]:
580
+ try:
581
+ self.last_charging_session.charging_type = element.value
582
+ session.commit()
583
+ except DatabaseError as err:
584
+ session.rollback()
585
+ LOG.error('DatabaseError while updating type of charging session for vehicle %s in database: %s', self.vehicle.vin, err)
586
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
587
+ self.session_factory.remove()
588
+
589
+ def _on_battery_level_change(self, element: FloatAttribute, flags: Observable.ObserverEvent) -> None:
590
+ del flags
591
+ if element.enabled and element.value is not None:
592
+ # We try to see if there was a late battery level update for a finished session
593
+ with self.session_factory() as session:
594
+ with self.last_charging_session_lock:
595
+ if self.last_charging_session is not None:
596
+ self.last_charging_session = session.merge(self.last_charging_session)
597
+ session.refresh(self.last_charging_session)
598
+ if self.last_charging_session is not None and self.last_charging_session.session_end_date is not None:
599
+ if element.last_updated is not None and (element.last_updated <= (self.last_charging_session.session_end_date + timedelta(minutes=1))):
600
+ # Only update if we have no end level yet or the new level is higher than the previous one (this happens with late level updates)
601
+ if self.last_charging_session.end_level is None or self.last_charging_session.end_level < element.value:
602
+ try:
603
+ self.last_charging_session.end_level = element.value
604
+ session.commit()
605
+ except DatabaseError as err:
606
+ session.rollback()
607
+ LOG.error('DatabaseError while updating battery level of charging session for vehicle %s in database: %s',
608
+ self.vehicle.vin, err)
609
+ self.database_plugin.healthy._set_value(value=False) # pylint: disable=protected-access
610
+ self.session_factory.remove()