arthexis 0.1.16__py3-none-any.whl → 0.1.26__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 arthexis might be problematic. Click here for more details.
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/METADATA +84 -35
- arthexis-0.1.26.dist-info/RECORD +111 -0
- config/asgi.py +1 -15
- config/middleware.py +47 -1
- config/settings.py +15 -30
- config/urls.py +53 -1
- core/admin.py +540 -450
- core/apps.py +0 -6
- core/auto_upgrade.py +19 -4
- core/backends.py +13 -3
- core/changelog.py +66 -5
- core/environment.py +4 -5
- core/models.py +1566 -203
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/release.py +55 -7
- core/sigil_builder.py +2 -2
- core/sigil_resolver.py +1 -66
- core/system.py +268 -2
- core/tasks.py +174 -48
- core/tests.py +314 -16
- core/user_data.py +42 -2
- core/views.py +278 -183
- nodes/admin.py +557 -65
- nodes/apps.py +11 -0
- nodes/models.py +658 -113
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +97 -2
- nodes/tests.py +1212 -116
- nodes/urls.py +15 -1
- nodes/utils.py +51 -3
- nodes/views.py +1239 -154
- ocpp/admin.py +979 -152
- ocpp/consumers.py +268 -28
- ocpp/models.py +488 -3
- ocpp/network.py +398 -0
- ocpp/store.py +6 -4
- ocpp/tasks.py +296 -2
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +121 -4
- ocpp/tests.py +950 -11
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +596 -51
- pages/admin.py +262 -30
- pages/apps.py +35 -0
- pages/context_processors.py +26 -21
- pages/defaults.py +1 -1
- pages/forms.py +31 -8
- pages/middleware.py +6 -2
- pages/models.py +77 -2
- pages/module_defaults.py +5 -5
- pages/site_config.py +137 -0
- pages/tests.py +885 -109
- pages/urls.py +13 -2
- pages/utils.py +70 -0
- pages/views.py +558 -55
- arthexis-0.1.16.dist-info/RECORD +0 -111
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
ocpp/consumers.py
CHANGED
|
@@ -5,6 +5,7 @@ from datetime import datetime
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import inspect
|
|
7
7
|
import json
|
|
8
|
+
import logging
|
|
8
9
|
from urllib.parse import parse_qs
|
|
9
10
|
from django.utils import timezone
|
|
10
11
|
from core.models import EnergyAccount, Reference, RFID as CoreRFID
|
|
@@ -19,7 +20,14 @@ from config.offline import requires_network
|
|
|
19
20
|
from . import store
|
|
20
21
|
from decimal import Decimal
|
|
21
22
|
from django.utils.dateparse import parse_datetime
|
|
22
|
-
from .models import
|
|
23
|
+
from .models import (
|
|
24
|
+
Transaction,
|
|
25
|
+
Charger,
|
|
26
|
+
ChargerConfiguration,
|
|
27
|
+
MeterValue,
|
|
28
|
+
DataTransferMessage,
|
|
29
|
+
CPReservation,
|
|
30
|
+
)
|
|
23
31
|
from .reference_utils import host_is_local_loopback
|
|
24
32
|
from .evcs_discovery import (
|
|
25
33
|
DEFAULT_CONSOLE_PORT,
|
|
@@ -32,6 +40,9 @@ from .evcs_discovery import (
|
|
|
32
40
|
FORWARDED_PAIR_RE = re.compile(r"for=(?:\"?)(?P<value>[^;,\"\s]+)(?:\"?)", re.IGNORECASE)
|
|
33
41
|
|
|
34
42
|
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
35
46
|
# Query parameter keys that may contain the charge point serial. Keys are
|
|
36
47
|
# matched case-insensitively and trimmed before use.
|
|
37
48
|
SERIAL_QUERY_PARAM_NAMES = (
|
|
@@ -134,6 +145,18 @@ def _parse_ocpp_timestamp(value) -> datetime | None:
|
|
|
134
145
|
return timestamp
|
|
135
146
|
|
|
136
147
|
|
|
148
|
+
def _extract_vehicle_identifier(payload: dict) -> tuple[str, str]:
|
|
149
|
+
"""Return normalized VID and VIN values from an OCPP message payload."""
|
|
150
|
+
|
|
151
|
+
raw_vid = payload.get("vid")
|
|
152
|
+
vid_value = str(raw_vid).strip() if raw_vid is not None else ""
|
|
153
|
+
raw_vin = payload.get("vin")
|
|
154
|
+
vin_value = str(raw_vin).strip() if raw_vin is not None else ""
|
|
155
|
+
if not vid_value and vin_value:
|
|
156
|
+
vid_value = vin_value
|
|
157
|
+
return vid_value, vin_value
|
|
158
|
+
|
|
159
|
+
|
|
137
160
|
class SinkConsumer(AsyncWebsocketConsumer):
|
|
138
161
|
"""Accept any message without validation."""
|
|
139
162
|
|
|
@@ -279,11 +302,18 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
279
302
|
"""Return the energy account for the provided RFID if valid."""
|
|
280
303
|
if not id_tag:
|
|
281
304
|
return None
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
286
|
-
|
|
305
|
+
|
|
306
|
+
def _resolve() -> EnergyAccount | None:
|
|
307
|
+
matches = CoreRFID.matching_queryset(id_tag).filter(allowed=True)
|
|
308
|
+
if not matches.exists():
|
|
309
|
+
return None
|
|
310
|
+
return (
|
|
311
|
+
EnergyAccount.objects.filter(rfids__in=matches)
|
|
312
|
+
.distinct()
|
|
313
|
+
.first()
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return await database_sync_to_async(_resolve)()
|
|
287
317
|
|
|
288
318
|
async def _ensure_rfid_seen(self, id_tag: str) -> CoreRFID | None:
|
|
289
319
|
"""Ensure an RFID record exists and update its last seen timestamp."""
|
|
@@ -309,6 +339,19 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
309
339
|
|
|
310
340
|
return await database_sync_to_async(_ensure)()
|
|
311
341
|
|
|
342
|
+
def _log_unlinked_rfid(self, rfid: str) -> None:
|
|
343
|
+
"""Record a warning when an RFID is authorized without an account."""
|
|
344
|
+
|
|
345
|
+
message = (
|
|
346
|
+
f"Authorized RFID {rfid} on charger {self.charger_id} without linked energy account"
|
|
347
|
+
)
|
|
348
|
+
logger.warning(message)
|
|
349
|
+
store.add_log(
|
|
350
|
+
store.pending_key(self.charger_id),
|
|
351
|
+
message,
|
|
352
|
+
log_type="charger",
|
|
353
|
+
)
|
|
354
|
+
|
|
312
355
|
async def _assign_connector(self, connector: int | str | None) -> None:
|
|
313
356
|
"""Ensure ``self.charger`` matches the provided connector id."""
|
|
314
357
|
if connector in (None, "", "-"):
|
|
@@ -709,6 +752,54 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
709
752
|
task.add_done_callback(lambda _: setattr(self, "_consumption_task", None))
|
|
710
753
|
self._consumption_task = task
|
|
711
754
|
|
|
755
|
+
def _persist_configuration_result(
|
|
756
|
+
self, payload: dict, connector_hint: int | str | None
|
|
757
|
+
) -> ChargerConfiguration | None:
|
|
758
|
+
if not isinstance(payload, dict):
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
connector_value: int | None = None
|
|
762
|
+
if connector_hint not in (None, ""):
|
|
763
|
+
try:
|
|
764
|
+
connector_value = int(connector_hint)
|
|
765
|
+
except (TypeError, ValueError):
|
|
766
|
+
connector_value = None
|
|
767
|
+
|
|
768
|
+
normalized_entries: list[dict[str, object]] = []
|
|
769
|
+
for entry in payload.get("configurationKey") or []:
|
|
770
|
+
if not isinstance(entry, dict):
|
|
771
|
+
continue
|
|
772
|
+
key = str(entry.get("key") or "")
|
|
773
|
+
normalized: dict[str, object] = {"key": key}
|
|
774
|
+
if "value" in entry:
|
|
775
|
+
normalized["value"] = entry.get("value")
|
|
776
|
+
normalized["readonly"] = bool(entry.get("readonly"))
|
|
777
|
+
normalized_entries.append(normalized)
|
|
778
|
+
|
|
779
|
+
unknown_values: list[str] = []
|
|
780
|
+
for value in payload.get("unknownKey") or []:
|
|
781
|
+
if value is None:
|
|
782
|
+
continue
|
|
783
|
+
unknown_values.append(str(value))
|
|
784
|
+
|
|
785
|
+
try:
|
|
786
|
+
raw_payload = json.loads(json.dumps(payload, ensure_ascii=False))
|
|
787
|
+
except (TypeError, ValueError):
|
|
788
|
+
raw_payload = payload
|
|
789
|
+
|
|
790
|
+
configuration = ChargerConfiguration.objects.create(
|
|
791
|
+
charger_identifier=self.charger_id,
|
|
792
|
+
connector_id=connector_value,
|
|
793
|
+
configuration_keys=normalized_entries,
|
|
794
|
+
unknown_keys=unknown_values,
|
|
795
|
+
evcs_snapshot_at=timezone.now(),
|
|
796
|
+
raw_payload=raw_payload,
|
|
797
|
+
)
|
|
798
|
+
Charger.objects.filter(charger_id=self.charger_id).update(
|
|
799
|
+
configuration=configuration
|
|
800
|
+
)
|
|
801
|
+
return configuration
|
|
802
|
+
|
|
712
803
|
async def _handle_call_result(self, message_id: str, payload: dict | None) -> None:
|
|
713
804
|
metadata = store.pop_pending_call(message_id)
|
|
714
805
|
if not metadata:
|
|
@@ -770,6 +861,17 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
770
861
|
f"GetConfiguration result: {payload_text}",
|
|
771
862
|
log_type="charger",
|
|
772
863
|
)
|
|
864
|
+
configuration = await database_sync_to_async(
|
|
865
|
+
self._persist_configuration_result
|
|
866
|
+
)(payload_data, metadata.get("connector_id"))
|
|
867
|
+
if configuration:
|
|
868
|
+
if self.charger and self.charger.charger_id == self.charger_id:
|
|
869
|
+
self.charger.configuration = configuration
|
|
870
|
+
if (
|
|
871
|
+
self.aggregate_charger
|
|
872
|
+
and self.aggregate_charger.charger_id == self.charger_id
|
|
873
|
+
):
|
|
874
|
+
self.aggregate_charger.configuration = configuration
|
|
773
875
|
store.record_pending_call_result(
|
|
774
876
|
message_id,
|
|
775
877
|
metadata=metadata,
|
|
@@ -802,6 +904,43 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
802
904
|
payload=payload_data,
|
|
803
905
|
)
|
|
804
906
|
return
|
|
907
|
+
if action == "ReserveNow":
|
|
908
|
+
status_value = str(payload_data.get("status") or "").strip()
|
|
909
|
+
message = "ReserveNow result"
|
|
910
|
+
if status_value:
|
|
911
|
+
message += f": status={status_value}"
|
|
912
|
+
store.add_log(log_key, message, log_type="charger")
|
|
913
|
+
|
|
914
|
+
reservation_pk = metadata.get("reservation_pk")
|
|
915
|
+
|
|
916
|
+
def _apply():
|
|
917
|
+
if not reservation_pk:
|
|
918
|
+
return
|
|
919
|
+
reservation = CPReservation.objects.filter(pk=reservation_pk).first()
|
|
920
|
+
if not reservation:
|
|
921
|
+
return
|
|
922
|
+
reservation.evcs_status = status_value
|
|
923
|
+
reservation.evcs_error = ""
|
|
924
|
+
confirmed = status_value.casefold() == "accepted"
|
|
925
|
+
reservation.evcs_confirmed = confirmed
|
|
926
|
+
reservation.evcs_confirmed_at = timezone.now() if confirmed else None
|
|
927
|
+
reservation.save(
|
|
928
|
+
update_fields=[
|
|
929
|
+
"evcs_status",
|
|
930
|
+
"evcs_error",
|
|
931
|
+
"evcs_confirmed",
|
|
932
|
+
"evcs_confirmed_at",
|
|
933
|
+
"updated_on",
|
|
934
|
+
]
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
await database_sync_to_async(_apply)()
|
|
938
|
+
store.record_pending_call_result(
|
|
939
|
+
message_id,
|
|
940
|
+
metadata=metadata,
|
|
941
|
+
payload=payload_data,
|
|
942
|
+
)
|
|
943
|
+
return
|
|
805
944
|
if action == "RemoteStartTransaction":
|
|
806
945
|
status_value = str(payload_data.get("status") or "").strip()
|
|
807
946
|
message = "RemoteStartTransaction result"
|
|
@@ -983,6 +1122,66 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
983
1122
|
error_details=details,
|
|
984
1123
|
)
|
|
985
1124
|
return
|
|
1125
|
+
if action == "ReserveNow":
|
|
1126
|
+
parts: list[str] = []
|
|
1127
|
+
code_text = (error_code or "").strip() if error_code else ""
|
|
1128
|
+
if code_text:
|
|
1129
|
+
parts.append(f"code={code_text}")
|
|
1130
|
+
description_text = (description or "").strip() if description else ""
|
|
1131
|
+
if description_text:
|
|
1132
|
+
parts.append(f"description={description_text}")
|
|
1133
|
+
details_text = ""
|
|
1134
|
+
if details:
|
|
1135
|
+
try:
|
|
1136
|
+
details_text = json.dumps(details, sort_keys=True, ensure_ascii=False)
|
|
1137
|
+
except TypeError:
|
|
1138
|
+
details_text = str(details)
|
|
1139
|
+
if details_text:
|
|
1140
|
+
parts.append(f"details={details_text}")
|
|
1141
|
+
message = "ReserveNow error"
|
|
1142
|
+
if parts:
|
|
1143
|
+
message += ": " + ", ".join(parts)
|
|
1144
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1145
|
+
|
|
1146
|
+
reservation_pk = metadata.get("reservation_pk")
|
|
1147
|
+
|
|
1148
|
+
def _apply():
|
|
1149
|
+
if not reservation_pk:
|
|
1150
|
+
return
|
|
1151
|
+
reservation = CPReservation.objects.filter(pk=reservation_pk).first()
|
|
1152
|
+
if not reservation:
|
|
1153
|
+
return
|
|
1154
|
+
summary_parts = []
|
|
1155
|
+
if code_text:
|
|
1156
|
+
summary_parts.append(code_text)
|
|
1157
|
+
if description_text:
|
|
1158
|
+
summary_parts.append(description_text)
|
|
1159
|
+
if details_text:
|
|
1160
|
+
summary_parts.append(details_text)
|
|
1161
|
+
reservation.evcs_status = ""
|
|
1162
|
+
reservation.evcs_error = "; ".join(summary_parts)
|
|
1163
|
+
reservation.evcs_confirmed = False
|
|
1164
|
+
reservation.evcs_confirmed_at = None
|
|
1165
|
+
reservation.save(
|
|
1166
|
+
update_fields=[
|
|
1167
|
+
"evcs_status",
|
|
1168
|
+
"evcs_error",
|
|
1169
|
+
"evcs_confirmed",
|
|
1170
|
+
"evcs_confirmed_at",
|
|
1171
|
+
"updated_on",
|
|
1172
|
+
]
|
|
1173
|
+
)
|
|
1174
|
+
|
|
1175
|
+
await database_sync_to_async(_apply)()
|
|
1176
|
+
store.record_pending_call_result(
|
|
1177
|
+
message_id,
|
|
1178
|
+
metadata=metadata,
|
|
1179
|
+
success=False,
|
|
1180
|
+
error_code=error_code,
|
|
1181
|
+
error_description=description,
|
|
1182
|
+
error_details=details,
|
|
1183
|
+
)
|
|
1184
|
+
return
|
|
986
1185
|
if action == "RemoteStartTransaction":
|
|
987
1186
|
message = "RemoteStartTransaction error"
|
|
988
1187
|
if error_code:
|
|
@@ -1321,8 +1520,13 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1321
1520
|
reply_payload = {"currentTime": datetime.utcnow().isoformat() + "Z"}
|
|
1322
1521
|
now = timezone.now()
|
|
1323
1522
|
self.charger.last_heartbeat = now
|
|
1523
|
+
if (
|
|
1524
|
+
self.aggregate_charger
|
|
1525
|
+
and self.aggregate_charger is not self.charger
|
|
1526
|
+
):
|
|
1527
|
+
self.aggregate_charger.last_heartbeat = now
|
|
1324
1528
|
await database_sync_to_async(
|
|
1325
|
-
Charger.objects.filter(
|
|
1529
|
+
Charger.objects.filter(charger_id=self.charger_id).update
|
|
1326
1530
|
)(last_heartbeat=now)
|
|
1327
1531
|
elif action == "StatusNotification":
|
|
1328
1532
|
await self._assign_connector(payload.get("connectorId"))
|
|
@@ -1395,13 +1599,25 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1395
1599
|
elif action == "Authorize":
|
|
1396
1600
|
id_tag = payload.get("idTag")
|
|
1397
1601
|
account = await self._get_account(id_tag)
|
|
1602
|
+
status = "Invalid"
|
|
1398
1603
|
if self.charger.require_rfid:
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1604
|
+
tag = None
|
|
1605
|
+
tag_created = False
|
|
1606
|
+
if id_tag:
|
|
1607
|
+
tag, tag_created = await database_sync_to_async(
|
|
1608
|
+
CoreRFID.register_scan
|
|
1609
|
+
)(id_tag)
|
|
1610
|
+
if account:
|
|
1611
|
+
if await database_sync_to_async(account.can_authorize)():
|
|
1612
|
+
status = "Accepted"
|
|
1613
|
+
elif (
|
|
1614
|
+
id_tag
|
|
1615
|
+
and tag
|
|
1616
|
+
and not tag_created
|
|
1617
|
+
and tag.allowed
|
|
1618
|
+
):
|
|
1619
|
+
status = "Accepted"
|
|
1620
|
+
self._log_unlinked_rfid(tag.rfid)
|
|
1405
1621
|
else:
|
|
1406
1622
|
await self._ensure_rfid_seen(id_tag)
|
|
1407
1623
|
status = "Accepted"
|
|
@@ -1475,30 +1691,47 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1475
1691
|
reply_payload = {}
|
|
1476
1692
|
elif action == "StartTransaction":
|
|
1477
1693
|
id_tag = payload.get("idTag")
|
|
1478
|
-
|
|
1694
|
+
tag = None
|
|
1695
|
+
tag_created = False
|
|
1479
1696
|
if id_tag:
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1697
|
+
tag, tag_created = await database_sync_to_async(
|
|
1698
|
+
CoreRFID.register_scan
|
|
1699
|
+
)(id_tag)
|
|
1700
|
+
account = await self._get_account(id_tag)
|
|
1701
|
+
if id_tag and not self.charger.require_rfid:
|
|
1702
|
+
seen_tag = await self._ensure_rfid_seen(id_tag)
|
|
1703
|
+
if seen_tag:
|
|
1704
|
+
tag = seen_tag
|
|
1486
1705
|
await self._assign_connector(payload.get("connectorId"))
|
|
1706
|
+
authorized = True
|
|
1707
|
+
authorized_via_tag = False
|
|
1487
1708
|
if self.charger.require_rfid:
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1709
|
+
if account is not None:
|
|
1710
|
+
authorized = await database_sync_to_async(
|
|
1711
|
+
account.can_authorize
|
|
1712
|
+
)()
|
|
1713
|
+
elif (
|
|
1714
|
+
id_tag
|
|
1715
|
+
and tag
|
|
1716
|
+
and not tag_created
|
|
1717
|
+
and getattr(tag, "allowed", False)
|
|
1718
|
+
):
|
|
1719
|
+
authorized = True
|
|
1720
|
+
authorized_via_tag = True
|
|
1721
|
+
else:
|
|
1722
|
+
authorized = False
|
|
1494
1723
|
if authorized:
|
|
1724
|
+
if authorized_via_tag and tag:
|
|
1725
|
+
self._log_unlinked_rfid(tag.rfid)
|
|
1495
1726
|
start_timestamp = _parse_ocpp_timestamp(payload.get("timestamp"))
|
|
1496
1727
|
received_start = timezone.now()
|
|
1728
|
+
vid_value, vin_value = _extract_vehicle_identifier(payload)
|
|
1497
1729
|
tx_obj = await database_sync_to_async(Transaction.objects.create)(
|
|
1498
1730
|
charger=self.charger,
|
|
1499
1731
|
account=account,
|
|
1500
1732
|
rfid=(id_tag or ""),
|
|
1501
|
-
|
|
1733
|
+
vid=vid_value,
|
|
1734
|
+
vin=vin_value,
|
|
1502
1735
|
connector_id=payload.get("connectorId"),
|
|
1503
1736
|
meter_start=payload.get("meterStart"),
|
|
1504
1737
|
start_time=start_timestamp or received_start,
|
|
@@ -1524,6 +1757,7 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1524
1757
|
)()
|
|
1525
1758
|
if not tx_obj and tx_id is not None:
|
|
1526
1759
|
received_start = timezone.now()
|
|
1760
|
+
vid_value, vin_value = _extract_vehicle_identifier(payload)
|
|
1527
1761
|
tx_obj = await database_sync_to_async(Transaction.objects.create)(
|
|
1528
1762
|
pk=tx_id,
|
|
1529
1763
|
charger=self.charger,
|
|
@@ -1531,12 +1765,18 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1531
1765
|
received_start_time=received_start,
|
|
1532
1766
|
meter_start=payload.get("meterStart")
|
|
1533
1767
|
or payload.get("meterStop"),
|
|
1534
|
-
|
|
1768
|
+
vid=vid_value,
|
|
1769
|
+
vin=vin_value,
|
|
1535
1770
|
)
|
|
1536
1771
|
if tx_obj:
|
|
1537
1772
|
stop_timestamp = _parse_ocpp_timestamp(payload.get("timestamp"))
|
|
1538
1773
|
received_stop = timezone.now()
|
|
1539
1774
|
tx_obj.meter_stop = payload.get("meterStop")
|
|
1775
|
+
vid_value, vin_value = _extract_vehicle_identifier(payload)
|
|
1776
|
+
if vid_value:
|
|
1777
|
+
tx_obj.vid = vid_value
|
|
1778
|
+
if vin_value:
|
|
1779
|
+
tx_obj.vin = vin_value
|
|
1540
1780
|
tx_obj.stop_time = stop_timestamp or received_stop
|
|
1541
1781
|
tx_obj.received_stop_time = received_stop
|
|
1542
1782
|
await database_sync_to_async(tx_obj.save)()
|