arthexis 0.1.16__py3-none-any.whl → 0.1.28__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.28.dist-info}/METADATA +95 -41
- arthexis-0.1.28.dist-info/RECORD +112 -0
- config/asgi.py +1 -15
- config/middleware.py +47 -1
- config/settings.py +21 -30
- config/settings_helpers.py +176 -1
- config/urls.py +69 -1
- core/admin.py +805 -473
- core/apps.py +6 -8
- core/auto_upgrade.py +19 -4
- core/backends.py +13 -3
- core/celery_utils.py +73 -0
- core/changelog.py +66 -5
- core/environment.py +4 -5
- core/models.py +1825 -218
- 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 +285 -4
- core/tasks.py +439 -138
- core/test_system_info.py +43 -5
- core/tests.py +516 -18
- core/user_data.py +94 -21
- core/views.py +348 -186
- nodes/admin.py +904 -67
- nodes/apps.py +12 -1
- nodes/feature_checks.py +30 -0
- nodes/models.py +800 -127
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +98 -3
- nodes/tests.py +1381 -152
- nodes/urls.py +15 -1
- nodes/utils.py +51 -3
- nodes/views.py +1382 -152
- ocpp/admin.py +1970 -152
- ocpp/consumers.py +839 -34
- ocpp/models.py +968 -17
- ocpp/network.py +398 -0
- ocpp/store.py +411 -43
- ocpp/tasks.py +261 -3
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +194 -6
- ocpp/tests.py +1918 -87
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +8 -3
- ocpp/views.py +700 -53
- pages/admin.py +262 -30
- pages/apps.py +35 -0
- pages/context_processors.py +28 -21
- pages/defaults.py +1 -1
- pages/forms.py +31 -8
- pages/middleware.py +6 -2
- pages/models.py +86 -2
- pages/module_defaults.py +5 -5
- pages/site_config.py +137 -0
- pages/tests.py +1050 -126
- pages/urls.py +14 -2
- pages/utils.py +70 -0
- pages/views.py +622 -56
- 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.28.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/top_level.txt +0 -0
ocpp/consumers.py
CHANGED
|
@@ -3,8 +3,10 @@ import ipaddress
|
|
|
3
3
|
import re
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
import asyncio
|
|
6
|
+
from collections import deque
|
|
6
7
|
import inspect
|
|
7
8
|
import json
|
|
9
|
+
import logging
|
|
8
10
|
from urllib.parse import parse_qs
|
|
9
11
|
from django.utils import timezone
|
|
10
12
|
from core.models import EnergyAccount, Reference, RFID as CoreRFID
|
|
@@ -19,7 +21,16 @@ from config.offline import requires_network
|
|
|
19
21
|
from . import store
|
|
20
22
|
from decimal import Decimal
|
|
21
23
|
from django.utils.dateparse import parse_datetime
|
|
22
|
-
from .models import
|
|
24
|
+
from .models import (
|
|
25
|
+
Transaction,
|
|
26
|
+
Charger,
|
|
27
|
+
ChargerConfiguration,
|
|
28
|
+
MeterValue,
|
|
29
|
+
DataTransferMessage,
|
|
30
|
+
CPReservation,
|
|
31
|
+
CPFirmwareDeployment,
|
|
32
|
+
RFIDSessionAttempt,
|
|
33
|
+
)
|
|
23
34
|
from .reference_utils import host_is_local_loopback
|
|
24
35
|
from .evcs_discovery import (
|
|
25
36
|
DEFAULT_CONSOLE_PORT,
|
|
@@ -32,6 +43,9 @@ from .evcs_discovery import (
|
|
|
32
43
|
FORWARDED_PAIR_RE = re.compile(r"for=(?:\"?)(?P<value>[^;,\"\s]+)(?:\"?)", re.IGNORECASE)
|
|
33
44
|
|
|
34
45
|
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
35
49
|
# Query parameter keys that may contain the charge point serial. Keys are
|
|
36
50
|
# matched case-insensitively and trimmed before use.
|
|
37
51
|
SERIAL_QUERY_PARAM_NAMES = (
|
|
@@ -134,6 +148,18 @@ def _parse_ocpp_timestamp(value) -> datetime | None:
|
|
|
134
148
|
return timestamp
|
|
135
149
|
|
|
136
150
|
|
|
151
|
+
def _extract_vehicle_identifier(payload: dict) -> tuple[str, str]:
|
|
152
|
+
"""Return normalized VID and VIN values from an OCPP message payload."""
|
|
153
|
+
|
|
154
|
+
raw_vid = payload.get("vid")
|
|
155
|
+
vid_value = str(raw_vid).strip() if raw_vid is not None else ""
|
|
156
|
+
raw_vin = payload.get("vin")
|
|
157
|
+
vin_value = str(raw_vin).strip() if raw_vin is not None else ""
|
|
158
|
+
if not vid_value and vin_value:
|
|
159
|
+
vid_value = vin_value
|
|
160
|
+
return vid_value, vin_value
|
|
161
|
+
|
|
162
|
+
|
|
137
163
|
class SinkConsumer(AsyncWebsocketConsumer):
|
|
138
164
|
"""Accept any message without validation."""
|
|
139
165
|
|
|
@@ -194,8 +220,20 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
194
220
|
trimmed = value.strip()
|
|
195
221
|
if trimmed:
|
|
196
222
|
return trimmed
|
|
197
|
-
|
|
198
|
-
|
|
223
|
+
|
|
224
|
+
serial = self.scope["url_route"]["kwargs"].get("cid", "")
|
|
225
|
+
if serial:
|
|
226
|
+
return serial
|
|
227
|
+
|
|
228
|
+
path = (self.scope.get("path") or "").strip("/")
|
|
229
|
+
if not path:
|
|
230
|
+
return ""
|
|
231
|
+
|
|
232
|
+
segments = [segment for segment in path.split("/") if segment]
|
|
233
|
+
if not segments:
|
|
234
|
+
return ""
|
|
235
|
+
|
|
236
|
+
return segments[-1]
|
|
199
237
|
|
|
200
238
|
@requires_network
|
|
201
239
|
async def connect(self):
|
|
@@ -253,7 +291,9 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
253
291
|
log_type="charger",
|
|
254
292
|
)
|
|
255
293
|
store.connections[self.store_key] = self
|
|
256
|
-
store.logs["charger"].setdefault(
|
|
294
|
+
store.logs["charger"].setdefault(
|
|
295
|
+
self.store_key, deque(maxlen=store.MAX_IN_MEMORY_LOG_ENTRIES)
|
|
296
|
+
)
|
|
257
297
|
self.charger, created = await database_sync_to_async(
|
|
258
298
|
Charger.objects.get_or_create
|
|
259
299
|
)(
|
|
@@ -279,11 +319,18 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
279
319
|
"""Return the energy account for the provided RFID if valid."""
|
|
280
320
|
if not id_tag:
|
|
281
321
|
return None
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
286
|
-
|
|
322
|
+
|
|
323
|
+
def _resolve() -> EnergyAccount | None:
|
|
324
|
+
matches = CoreRFID.matching_queryset(id_tag).filter(allowed=True)
|
|
325
|
+
if not matches.exists():
|
|
326
|
+
return None
|
|
327
|
+
return (
|
|
328
|
+
EnergyAccount.objects.filter(rfids__in=matches)
|
|
329
|
+
.distinct()
|
|
330
|
+
.first()
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return await database_sync_to_async(_resolve)()
|
|
287
334
|
|
|
288
335
|
async def _ensure_rfid_seen(self, id_tag: str) -> CoreRFID | None:
|
|
289
336
|
"""Ensure an RFID record exists and update its last seen timestamp."""
|
|
@@ -300,15 +347,58 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
300
347
|
if not tag.allowed:
|
|
301
348
|
tag.allowed = True
|
|
302
349
|
updates.append("allowed")
|
|
350
|
+
if not tag.released:
|
|
351
|
+
tag.released = True
|
|
352
|
+
updates.append("released")
|
|
303
353
|
if tag.last_seen_on != now:
|
|
304
354
|
tag.last_seen_on = now
|
|
305
355
|
updates.append("last_seen_on")
|
|
306
356
|
if updates:
|
|
307
|
-
tag.save(update_fields=updates)
|
|
357
|
+
tag.save(update_fields=sorted(set(updates)))
|
|
308
358
|
return tag
|
|
309
359
|
|
|
310
360
|
return await database_sync_to_async(_ensure)()
|
|
311
361
|
|
|
362
|
+
def _log_unlinked_rfid(self, rfid: str) -> None:
|
|
363
|
+
"""Record a warning when an RFID is authorized without an account."""
|
|
364
|
+
|
|
365
|
+
message = (
|
|
366
|
+
f"Authorized RFID {rfid} on charger {self.charger_id} without linked energy account"
|
|
367
|
+
)
|
|
368
|
+
logger.warning(message)
|
|
369
|
+
store.add_log(
|
|
370
|
+
store.pending_key(self.charger_id),
|
|
371
|
+
message,
|
|
372
|
+
log_type="charger",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
async def _record_rfid_attempt(
|
|
376
|
+
self,
|
|
377
|
+
*,
|
|
378
|
+
rfid: str,
|
|
379
|
+
status: RFIDSessionAttempt.Status,
|
|
380
|
+
account: EnergyAccount | None,
|
|
381
|
+
transaction: Transaction | None = None,
|
|
382
|
+
) -> None:
|
|
383
|
+
"""Persist RFID session attempt metadata for reporting."""
|
|
384
|
+
|
|
385
|
+
normalized = (rfid or "").strip().upper()
|
|
386
|
+
if not normalized:
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
charger = self.charger
|
|
390
|
+
|
|
391
|
+
def _create_attempt() -> None:
|
|
392
|
+
RFIDSessionAttempt.objects.create(
|
|
393
|
+
charger=charger,
|
|
394
|
+
rfid=normalized,
|
|
395
|
+
status=status,
|
|
396
|
+
account=account,
|
|
397
|
+
transaction=transaction,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
await database_sync_to_async(_create_attempt)()
|
|
401
|
+
|
|
312
402
|
async def _assign_connector(self, connector: int | str | None) -> None:
|
|
313
403
|
"""Ensure ``self.charger`` matches the provided connector id."""
|
|
314
404
|
if connector in (None, "", "-"):
|
|
@@ -345,7 +435,9 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
345
435
|
await existing_consumer.close()
|
|
346
436
|
store.reassign_identity(previous_key, new_key)
|
|
347
437
|
store.connections[new_key] = self
|
|
348
|
-
store.logs["charger"].setdefault(
|
|
438
|
+
store.logs["charger"].setdefault(
|
|
439
|
+
new_key, deque(maxlen=store.MAX_IN_MEMORY_LOG_ENTRIES)
|
|
440
|
+
)
|
|
349
441
|
aggregate_name = await sync_to_async(
|
|
350
442
|
lambda: self.charger.name or self.charger.charger_id
|
|
351
443
|
)()
|
|
@@ -414,7 +506,9 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
414
506
|
await existing_consumer.close()
|
|
415
507
|
store.reassign_identity(previous_key, new_key)
|
|
416
508
|
store.connections[new_key] = self
|
|
417
|
-
store.logs["charger"].setdefault(
|
|
509
|
+
store.logs["charger"].setdefault(
|
|
510
|
+
new_key, deque(maxlen=store.MAX_IN_MEMORY_LOG_ENTRIES)
|
|
511
|
+
)
|
|
418
512
|
connector_name = await sync_to_async(
|
|
419
513
|
lambda: self.charger.name or self.charger.charger_id
|
|
420
514
|
)()
|
|
@@ -613,6 +707,23 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
613
707
|
target.firmware_status_info = status_info
|
|
614
708
|
target.firmware_timestamp = timestamp
|
|
615
709
|
|
|
710
|
+
def _update_deployments(ids: list[int]) -> None:
|
|
711
|
+
deployments = list(
|
|
712
|
+
CPFirmwareDeployment.objects.filter(
|
|
713
|
+
charger_id__in=ids, completed_at__isnull=True
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
payload = {"status": status, "statusInfo": status_info}
|
|
717
|
+
for deployment in deployments:
|
|
718
|
+
deployment.mark_status(
|
|
719
|
+
status,
|
|
720
|
+
status_info,
|
|
721
|
+
timestamp,
|
|
722
|
+
response=payload,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
await database_sync_to_async(_update_deployments)([target.pk for target in targets])
|
|
726
|
+
|
|
616
727
|
async def _cancel_consumption_message(self) -> None:
|
|
617
728
|
"""Stop any scheduled consumption message updates."""
|
|
618
729
|
|
|
@@ -709,6 +820,136 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
709
820
|
task.add_done_callback(lambda _: setattr(self, "_consumption_task", None))
|
|
710
821
|
self._consumption_task = task
|
|
711
822
|
|
|
823
|
+
def _persist_configuration_result(
|
|
824
|
+
self, payload: dict, connector_hint: int | str | None
|
|
825
|
+
) -> ChargerConfiguration | None:
|
|
826
|
+
if not isinstance(payload, dict):
|
|
827
|
+
return None
|
|
828
|
+
|
|
829
|
+
connector_value: int | None = None
|
|
830
|
+
if connector_hint not in (None, ""):
|
|
831
|
+
try:
|
|
832
|
+
connector_value = int(connector_hint)
|
|
833
|
+
except (TypeError, ValueError):
|
|
834
|
+
connector_value = None
|
|
835
|
+
|
|
836
|
+
normalized_entries: list[dict[str, object]] = []
|
|
837
|
+
for entry in payload.get("configurationKey") or []:
|
|
838
|
+
if not isinstance(entry, dict):
|
|
839
|
+
continue
|
|
840
|
+
key = str(entry.get("key") or "")
|
|
841
|
+
normalized: dict[str, object] = {"key": key}
|
|
842
|
+
if "value" in entry:
|
|
843
|
+
normalized["value"] = entry.get("value")
|
|
844
|
+
normalized["readonly"] = bool(entry.get("readonly"))
|
|
845
|
+
normalized_entries.append(normalized)
|
|
846
|
+
|
|
847
|
+
unknown_values: list[str] = []
|
|
848
|
+
for value in payload.get("unknownKey") or []:
|
|
849
|
+
if value is None:
|
|
850
|
+
continue
|
|
851
|
+
unknown_values.append(str(value))
|
|
852
|
+
|
|
853
|
+
try:
|
|
854
|
+
raw_payload = json.loads(json.dumps(payload, ensure_ascii=False))
|
|
855
|
+
except (TypeError, ValueError):
|
|
856
|
+
raw_payload = payload
|
|
857
|
+
|
|
858
|
+
configuration = ChargerConfiguration.objects.create(
|
|
859
|
+
charger_identifier=self.charger_id,
|
|
860
|
+
connector_id=connector_value,
|
|
861
|
+
unknown_keys=unknown_values,
|
|
862
|
+
evcs_snapshot_at=timezone.now(),
|
|
863
|
+
raw_payload=raw_payload,
|
|
864
|
+
)
|
|
865
|
+
configuration.replace_configuration_keys(normalized_entries)
|
|
866
|
+
Charger.objects.filter(charger_id=self.charger_id).update(
|
|
867
|
+
configuration=configuration
|
|
868
|
+
)
|
|
869
|
+
return configuration
|
|
870
|
+
|
|
871
|
+
def _apply_change_configuration_snapshot(
|
|
872
|
+
self,
|
|
873
|
+
key: str,
|
|
874
|
+
value: str | None,
|
|
875
|
+
connector_hint: int | str | None,
|
|
876
|
+
) -> ChargerConfiguration:
|
|
877
|
+
connector_value: int | None = None
|
|
878
|
+
if connector_hint not in (None, ""):
|
|
879
|
+
try:
|
|
880
|
+
connector_value = int(connector_hint)
|
|
881
|
+
except (TypeError, ValueError):
|
|
882
|
+
connector_value = None
|
|
883
|
+
|
|
884
|
+
queryset = ChargerConfiguration.objects.filter(
|
|
885
|
+
charger_identifier=self.charger_id
|
|
886
|
+
)
|
|
887
|
+
if connector_value is None:
|
|
888
|
+
queryset = queryset.filter(connector_id__isnull=True)
|
|
889
|
+
else:
|
|
890
|
+
queryset = queryset.filter(connector_id=connector_value)
|
|
891
|
+
|
|
892
|
+
configuration = queryset.order_by("-created_at").first()
|
|
893
|
+
if configuration is None:
|
|
894
|
+
configuration = ChargerConfiguration.objects.create(
|
|
895
|
+
charger_identifier=self.charger_id,
|
|
896
|
+
connector_id=connector_value,
|
|
897
|
+
unknown_keys=[],
|
|
898
|
+
evcs_snapshot_at=timezone.now(),
|
|
899
|
+
raw_payload={},
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
entries = configuration.configuration_keys
|
|
903
|
+
updated = False
|
|
904
|
+
for entry in entries:
|
|
905
|
+
if entry.get("key") == key:
|
|
906
|
+
updated = True
|
|
907
|
+
if value is None:
|
|
908
|
+
entry.pop("value", None)
|
|
909
|
+
else:
|
|
910
|
+
entry["value"] = value
|
|
911
|
+
if not updated:
|
|
912
|
+
new_entry: dict[str, object] = {"key": key, "readonly": False}
|
|
913
|
+
if value is not None:
|
|
914
|
+
new_entry["value"] = value
|
|
915
|
+
entries.append(new_entry)
|
|
916
|
+
|
|
917
|
+
configuration.replace_configuration_keys(entries)
|
|
918
|
+
|
|
919
|
+
raw_payload = configuration.raw_payload or {}
|
|
920
|
+
if not isinstance(raw_payload, dict):
|
|
921
|
+
raw_payload = {}
|
|
922
|
+
else:
|
|
923
|
+
raw_payload = dict(raw_payload)
|
|
924
|
+
|
|
925
|
+
payload_entries: list[dict[str, object]] = []
|
|
926
|
+
seen = False
|
|
927
|
+
for item in raw_payload.get("configurationKey", []):
|
|
928
|
+
if not isinstance(item, dict):
|
|
929
|
+
continue
|
|
930
|
+
entry_copy = dict(item)
|
|
931
|
+
if str(entry_copy.get("key") or "") == key:
|
|
932
|
+
if value is None:
|
|
933
|
+
entry_copy.pop("value", None)
|
|
934
|
+
else:
|
|
935
|
+
entry_copy["value"] = value
|
|
936
|
+
seen = True
|
|
937
|
+
payload_entries.append(entry_copy)
|
|
938
|
+
if not seen:
|
|
939
|
+
payload_entry: dict[str, object] = {"key": key}
|
|
940
|
+
if value is not None:
|
|
941
|
+
payload_entry["value"] = value
|
|
942
|
+
payload_entries.append(payload_entry)
|
|
943
|
+
|
|
944
|
+
raw_payload["configurationKey"] = payload_entries
|
|
945
|
+
configuration.raw_payload = raw_payload
|
|
946
|
+
configuration.evcs_snapshot_at = timezone.now()
|
|
947
|
+
configuration.save(update_fields=["raw_payload", "evcs_snapshot_at", "updated_at"])
|
|
948
|
+
Charger.objects.filter(charger_id=self.charger_id).update(
|
|
949
|
+
configuration=configuration
|
|
950
|
+
)
|
|
951
|
+
return configuration
|
|
952
|
+
|
|
712
953
|
async def _handle_call_result(self, message_id: str, payload: dict | None) -> None:
|
|
713
954
|
metadata = store.pop_pending_call(message_id)
|
|
714
955
|
if not metadata:
|
|
@@ -718,6 +959,46 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
718
959
|
action = metadata.get("action")
|
|
719
960
|
log_key = metadata.get("log_key") or self.store_key
|
|
720
961
|
payload_data = payload if isinstance(payload, dict) else {}
|
|
962
|
+
if action == "ChangeConfiguration":
|
|
963
|
+
key_value = str(metadata.get("key") or "").strip()
|
|
964
|
+
status_value = str(payload_data.get("status") or "").strip()
|
|
965
|
+
stored_value = metadata.get("value")
|
|
966
|
+
parts: list[str] = []
|
|
967
|
+
if status_value:
|
|
968
|
+
parts.append(f"status={status_value}")
|
|
969
|
+
if key_value:
|
|
970
|
+
parts.append(f"key={key_value}")
|
|
971
|
+
if stored_value is not None:
|
|
972
|
+
parts.append(f"value={stored_value}")
|
|
973
|
+
message = "ChangeConfiguration result"
|
|
974
|
+
if parts:
|
|
975
|
+
message += ": " + ", ".join(parts)
|
|
976
|
+
store.add_log(log_key, message, log_type="charger")
|
|
977
|
+
if status_value.casefold() in {"accepted", "rebootrequired"} and key_value:
|
|
978
|
+
connector_hint = metadata.get("connector_id")
|
|
979
|
+
|
|
980
|
+
def _apply() -> ChargerConfiguration:
|
|
981
|
+
return self._apply_change_configuration_snapshot(
|
|
982
|
+
key_value,
|
|
983
|
+
stored_value if isinstance(stored_value, str) else None,
|
|
984
|
+
connector_hint,
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
configuration = await database_sync_to_async(_apply)()
|
|
988
|
+
if configuration:
|
|
989
|
+
if self.charger and self.charger.charger_id == self.charger_id:
|
|
990
|
+
self.charger.configuration = configuration
|
|
991
|
+
if (
|
|
992
|
+
self.aggregate_charger
|
|
993
|
+
and self.aggregate_charger.charger_id == self.charger_id
|
|
994
|
+
):
|
|
995
|
+
self.aggregate_charger.configuration = configuration
|
|
996
|
+
store.record_pending_call_result(
|
|
997
|
+
message_id,
|
|
998
|
+
metadata=metadata,
|
|
999
|
+
payload=payload_data,
|
|
1000
|
+
)
|
|
1001
|
+
return
|
|
721
1002
|
if action == "DataTransfer":
|
|
722
1003
|
message_pk = metadata.get("message_pk")
|
|
723
1004
|
if not message_pk:
|
|
@@ -751,6 +1032,97 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
751
1032
|
]
|
|
752
1033
|
)
|
|
753
1034
|
|
|
1035
|
+
await database_sync_to_async(_apply)()
|
|
1036
|
+
store.record_pending_call_result(
|
|
1037
|
+
message_id,
|
|
1038
|
+
metadata=metadata,
|
|
1039
|
+
payload=payload_data,
|
|
1040
|
+
)
|
|
1041
|
+
return
|
|
1042
|
+
if action == "SendLocalList":
|
|
1043
|
+
status_value = str(payload_data.get("status") or "").strip()
|
|
1044
|
+
version_candidate = (
|
|
1045
|
+
payload_data.get("currentLocalListVersion")
|
|
1046
|
+
or payload_data.get("listVersion")
|
|
1047
|
+
or metadata.get("list_version")
|
|
1048
|
+
)
|
|
1049
|
+
message = "SendLocalList result"
|
|
1050
|
+
if status_value:
|
|
1051
|
+
message += f": status={status_value}"
|
|
1052
|
+
if version_candidate is not None:
|
|
1053
|
+
message += f", version={version_candidate}"
|
|
1054
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1055
|
+
version_int = None
|
|
1056
|
+
if version_candidate is not None:
|
|
1057
|
+
try:
|
|
1058
|
+
version_int = int(version_candidate)
|
|
1059
|
+
except (TypeError, ValueError):
|
|
1060
|
+
version_int = None
|
|
1061
|
+
await self._update_local_authorization_state(version_int)
|
|
1062
|
+
store.record_pending_call_result(
|
|
1063
|
+
message_id,
|
|
1064
|
+
metadata=metadata,
|
|
1065
|
+
payload=payload_data,
|
|
1066
|
+
)
|
|
1067
|
+
return
|
|
1068
|
+
if action == "GetLocalListVersion":
|
|
1069
|
+
version_candidate = payload_data.get("listVersion")
|
|
1070
|
+
processed = 0
|
|
1071
|
+
auth_list = payload_data.get("localAuthorizationList")
|
|
1072
|
+
if isinstance(auth_list, list):
|
|
1073
|
+
processed = await self._apply_local_authorization_entries(auth_list)
|
|
1074
|
+
message = "GetLocalListVersion result"
|
|
1075
|
+
if version_candidate is not None:
|
|
1076
|
+
message += f": version={version_candidate}"
|
|
1077
|
+
if processed:
|
|
1078
|
+
message += f", entries={processed}"
|
|
1079
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1080
|
+
version_int = None
|
|
1081
|
+
if version_candidate is not None:
|
|
1082
|
+
try:
|
|
1083
|
+
version_int = int(version_candidate)
|
|
1084
|
+
except (TypeError, ValueError):
|
|
1085
|
+
version_int = None
|
|
1086
|
+
await self._update_local_authorization_state(version_int)
|
|
1087
|
+
store.record_pending_call_result(
|
|
1088
|
+
message_id,
|
|
1089
|
+
metadata=metadata,
|
|
1090
|
+
payload=payload_data,
|
|
1091
|
+
)
|
|
1092
|
+
return
|
|
1093
|
+
if action == "ClearCache":
|
|
1094
|
+
status_value = str(payload_data.get("status") or "").strip()
|
|
1095
|
+
message = "ClearCache result"
|
|
1096
|
+
if status_value:
|
|
1097
|
+
message += f": status={status_value}"
|
|
1098
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1099
|
+
version_int = 0 if status_value == "Accepted" else None
|
|
1100
|
+
await self._update_local_authorization_state(version_int)
|
|
1101
|
+
store.record_pending_call_result(
|
|
1102
|
+
message_id,
|
|
1103
|
+
metadata=metadata,
|
|
1104
|
+
payload=payload_data,
|
|
1105
|
+
)
|
|
1106
|
+
return
|
|
1107
|
+
if action == "UpdateFirmware":
|
|
1108
|
+
deployment_pk = metadata.get("deployment_pk")
|
|
1109
|
+
|
|
1110
|
+
def _apply():
|
|
1111
|
+
if not deployment_pk:
|
|
1112
|
+
return
|
|
1113
|
+
deployment = CPFirmwareDeployment.objects.filter(
|
|
1114
|
+
pk=deployment_pk
|
|
1115
|
+
).first()
|
|
1116
|
+
if not deployment:
|
|
1117
|
+
return
|
|
1118
|
+
status_value = str(payload_data.get("status") or "").strip() or "Accepted"
|
|
1119
|
+
deployment.mark_status(
|
|
1120
|
+
status_value,
|
|
1121
|
+
"",
|
|
1122
|
+
timezone.now(),
|
|
1123
|
+
response=payload_data,
|
|
1124
|
+
)
|
|
1125
|
+
|
|
754
1126
|
await database_sync_to_async(_apply)()
|
|
755
1127
|
store.record_pending_call_result(
|
|
756
1128
|
message_id,
|
|
@@ -770,6 +1142,17 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
770
1142
|
f"GetConfiguration result: {payload_text}",
|
|
771
1143
|
log_type="charger",
|
|
772
1144
|
)
|
|
1145
|
+
configuration = await database_sync_to_async(
|
|
1146
|
+
self._persist_configuration_result
|
|
1147
|
+
)(payload_data, metadata.get("connector_id"))
|
|
1148
|
+
if configuration:
|
|
1149
|
+
if self.charger and self.charger.charger_id == self.charger_id:
|
|
1150
|
+
self.charger.configuration = configuration
|
|
1151
|
+
if (
|
|
1152
|
+
self.aggregate_charger
|
|
1153
|
+
and self.aggregate_charger.charger_id == self.charger_id
|
|
1154
|
+
):
|
|
1155
|
+
self.aggregate_charger.configuration = configuration
|
|
773
1156
|
store.record_pending_call_result(
|
|
774
1157
|
message_id,
|
|
775
1158
|
metadata=metadata,
|
|
@@ -802,6 +1185,79 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
802
1185
|
payload=payload_data,
|
|
803
1186
|
)
|
|
804
1187
|
return
|
|
1188
|
+
if action == "ReserveNow":
|
|
1189
|
+
status_value = str(payload_data.get("status") or "").strip()
|
|
1190
|
+
message = "ReserveNow result"
|
|
1191
|
+
if status_value:
|
|
1192
|
+
message += f": status={status_value}"
|
|
1193
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1194
|
+
|
|
1195
|
+
reservation_pk = metadata.get("reservation_pk")
|
|
1196
|
+
|
|
1197
|
+
def _apply():
|
|
1198
|
+
if not reservation_pk:
|
|
1199
|
+
return
|
|
1200
|
+
reservation = CPReservation.objects.filter(pk=reservation_pk).first()
|
|
1201
|
+
if not reservation:
|
|
1202
|
+
return
|
|
1203
|
+
reservation.evcs_status = status_value
|
|
1204
|
+
reservation.evcs_error = ""
|
|
1205
|
+
confirmed = status_value.casefold() == "accepted"
|
|
1206
|
+
reservation.evcs_confirmed = confirmed
|
|
1207
|
+
reservation.evcs_confirmed_at = timezone.now() if confirmed else None
|
|
1208
|
+
reservation.save(
|
|
1209
|
+
update_fields=[
|
|
1210
|
+
"evcs_status",
|
|
1211
|
+
"evcs_error",
|
|
1212
|
+
"evcs_confirmed",
|
|
1213
|
+
"evcs_confirmed_at",
|
|
1214
|
+
"updated_on",
|
|
1215
|
+
]
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
await database_sync_to_async(_apply)()
|
|
1219
|
+
store.record_pending_call_result(
|
|
1220
|
+
message_id,
|
|
1221
|
+
metadata=metadata,
|
|
1222
|
+
payload=payload_data,
|
|
1223
|
+
)
|
|
1224
|
+
return
|
|
1225
|
+
if action == "CancelReservation":
|
|
1226
|
+
status_value = str(payload_data.get("status") or "").strip()
|
|
1227
|
+
message = "CancelReservation result"
|
|
1228
|
+
if status_value:
|
|
1229
|
+
message += f": status={status_value}"
|
|
1230
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1231
|
+
|
|
1232
|
+
reservation_pk = metadata.get("reservation_pk")
|
|
1233
|
+
|
|
1234
|
+
def _apply():
|
|
1235
|
+
if not reservation_pk:
|
|
1236
|
+
return
|
|
1237
|
+
reservation = CPReservation.objects.filter(pk=reservation_pk).first()
|
|
1238
|
+
if not reservation:
|
|
1239
|
+
return
|
|
1240
|
+
reservation.evcs_status = status_value
|
|
1241
|
+
reservation.evcs_error = ""
|
|
1242
|
+
reservation.evcs_confirmed = False
|
|
1243
|
+
reservation.evcs_confirmed_at = None
|
|
1244
|
+
reservation.save(
|
|
1245
|
+
update_fields=[
|
|
1246
|
+
"evcs_status",
|
|
1247
|
+
"evcs_error",
|
|
1248
|
+
"evcs_confirmed",
|
|
1249
|
+
"evcs_confirmed_at",
|
|
1250
|
+
"updated_on",
|
|
1251
|
+
]
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
await database_sync_to_async(_apply)()
|
|
1255
|
+
store.record_pending_call_result(
|
|
1256
|
+
message_id,
|
|
1257
|
+
metadata=metadata,
|
|
1258
|
+
payload=payload_data,
|
|
1259
|
+
)
|
|
1260
|
+
return
|
|
805
1261
|
if action == "RemoteStartTransaction":
|
|
806
1262
|
status_value = str(payload_data.get("status") or "").strip()
|
|
807
1263
|
message = "RemoteStartTransaction result"
|
|
@@ -876,6 +1332,36 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
876
1332
|
return
|
|
877
1333
|
action = metadata.get("action")
|
|
878
1334
|
log_key = metadata.get("log_key") or self.store_key
|
|
1335
|
+
if action == "ChangeConfiguration":
|
|
1336
|
+
key_value = str(metadata.get("key") or "").strip()
|
|
1337
|
+
parts: list[str] = []
|
|
1338
|
+
if key_value:
|
|
1339
|
+
parts.append(f"key={key_value}")
|
|
1340
|
+
if error_code:
|
|
1341
|
+
parts.append(f"code={str(error_code).strip()}")
|
|
1342
|
+
if description:
|
|
1343
|
+
parts.append(f"description={str(description).strip()}")
|
|
1344
|
+
if details:
|
|
1345
|
+
try:
|
|
1346
|
+
parts.append(
|
|
1347
|
+
"details="
|
|
1348
|
+
+ json.dumps(details, sort_keys=True, ensure_ascii=False)
|
|
1349
|
+
)
|
|
1350
|
+
except TypeError:
|
|
1351
|
+
parts.append(f"details={details}")
|
|
1352
|
+
message = "ChangeConfiguration error"
|
|
1353
|
+
if parts:
|
|
1354
|
+
message += ": " + ", ".join(parts)
|
|
1355
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1356
|
+
store.record_pending_call_result(
|
|
1357
|
+
message_id,
|
|
1358
|
+
metadata=metadata,
|
|
1359
|
+
success=False,
|
|
1360
|
+
error_code=error_code,
|
|
1361
|
+
error_description=description,
|
|
1362
|
+
error_details=details,
|
|
1363
|
+
)
|
|
1364
|
+
return
|
|
879
1365
|
if action == "DataTransfer":
|
|
880
1366
|
message_pk = metadata.get("message_pk")
|
|
881
1367
|
if not message_pk:
|
|
@@ -922,6 +1408,35 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
922
1408
|
error_details=details,
|
|
923
1409
|
)
|
|
924
1410
|
return
|
|
1411
|
+
if action == "ClearCache":
|
|
1412
|
+
parts: list[str] = []
|
|
1413
|
+
code_text = (error_code or "").strip()
|
|
1414
|
+
if code_text:
|
|
1415
|
+
parts.append(f"code={code_text}")
|
|
1416
|
+
description_text = (description or "").strip()
|
|
1417
|
+
if description_text:
|
|
1418
|
+
parts.append(f"description={description_text}")
|
|
1419
|
+
if details:
|
|
1420
|
+
try:
|
|
1421
|
+
details_text = json.dumps(details, sort_keys=True, ensure_ascii=False)
|
|
1422
|
+
except TypeError:
|
|
1423
|
+
details_text = str(details)
|
|
1424
|
+
if details_text:
|
|
1425
|
+
parts.append(f"details={details_text}")
|
|
1426
|
+
message = "ClearCache error"
|
|
1427
|
+
if parts:
|
|
1428
|
+
message += ": " + ", ".join(parts)
|
|
1429
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1430
|
+
await self._update_local_authorization_state(None)
|
|
1431
|
+
store.record_pending_call_result(
|
|
1432
|
+
message_id,
|
|
1433
|
+
metadata=metadata,
|
|
1434
|
+
success=False,
|
|
1435
|
+
error_code=error_code,
|
|
1436
|
+
error_description=description,
|
|
1437
|
+
error_details=details,
|
|
1438
|
+
)
|
|
1439
|
+
return
|
|
925
1440
|
if action == "GetConfiguration":
|
|
926
1441
|
parts: list[str] = []
|
|
927
1442
|
code_text = (error_code or "").strip()
|
|
@@ -983,6 +1498,173 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
983
1498
|
error_details=details,
|
|
984
1499
|
)
|
|
985
1500
|
return
|
|
1501
|
+
if action == "UpdateFirmware":
|
|
1502
|
+
deployment_pk = metadata.get("deployment_pk")
|
|
1503
|
+
|
|
1504
|
+
def _apply():
|
|
1505
|
+
if not deployment_pk:
|
|
1506
|
+
return
|
|
1507
|
+
deployment = CPFirmwareDeployment.objects.filter(
|
|
1508
|
+
pk=deployment_pk
|
|
1509
|
+
).first()
|
|
1510
|
+
if not deployment:
|
|
1511
|
+
return
|
|
1512
|
+
parts: list[str] = []
|
|
1513
|
+
if error_code:
|
|
1514
|
+
parts.append(f"code={str(error_code).strip()}")
|
|
1515
|
+
if description:
|
|
1516
|
+
parts.append(f"description={str(description).strip()}")
|
|
1517
|
+
if details:
|
|
1518
|
+
try:
|
|
1519
|
+
details_text = json.dumps(
|
|
1520
|
+
details, sort_keys=True, ensure_ascii=False
|
|
1521
|
+
)
|
|
1522
|
+
except TypeError:
|
|
1523
|
+
details_text = str(details)
|
|
1524
|
+
if details_text:
|
|
1525
|
+
parts.append(f"details={details_text}")
|
|
1526
|
+
message = "UpdateFirmware error"
|
|
1527
|
+
if parts:
|
|
1528
|
+
message += ": " + ", ".join(parts)
|
|
1529
|
+
deployment.mark_status(
|
|
1530
|
+
"Error",
|
|
1531
|
+
message,
|
|
1532
|
+
timezone.now(),
|
|
1533
|
+
response=details or {},
|
|
1534
|
+
)
|
|
1535
|
+
deployment.completed_at = timezone.now()
|
|
1536
|
+
deployment.save(update_fields=["completed_at", "updated_at"])
|
|
1537
|
+
|
|
1538
|
+
await database_sync_to_async(_apply)()
|
|
1539
|
+
store.record_pending_call_result(
|
|
1540
|
+
message_id,
|
|
1541
|
+
metadata=metadata,
|
|
1542
|
+
success=False,
|
|
1543
|
+
error_code=error_code,
|
|
1544
|
+
error_description=description,
|
|
1545
|
+
error_details=details,
|
|
1546
|
+
)
|
|
1547
|
+
return
|
|
1548
|
+
if action == "ReserveNow":
|
|
1549
|
+
parts: list[str] = []
|
|
1550
|
+
code_text = (error_code or "").strip() if error_code else ""
|
|
1551
|
+
if code_text:
|
|
1552
|
+
parts.append(f"code={code_text}")
|
|
1553
|
+
description_text = (description or "").strip() if description else ""
|
|
1554
|
+
if description_text:
|
|
1555
|
+
parts.append(f"description={description_text}")
|
|
1556
|
+
details_text = ""
|
|
1557
|
+
if details:
|
|
1558
|
+
try:
|
|
1559
|
+
details_text = json.dumps(details, sort_keys=True, ensure_ascii=False)
|
|
1560
|
+
except TypeError:
|
|
1561
|
+
details_text = str(details)
|
|
1562
|
+
if details_text:
|
|
1563
|
+
parts.append(f"details={details_text}")
|
|
1564
|
+
message = "ReserveNow error"
|
|
1565
|
+
if parts:
|
|
1566
|
+
message += ": " + ", ".join(parts)
|
|
1567
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1568
|
+
|
|
1569
|
+
reservation_pk = metadata.get("reservation_pk")
|
|
1570
|
+
|
|
1571
|
+
def _apply():
|
|
1572
|
+
if not reservation_pk:
|
|
1573
|
+
return
|
|
1574
|
+
reservation = CPReservation.objects.filter(pk=reservation_pk).first()
|
|
1575
|
+
if not reservation:
|
|
1576
|
+
return
|
|
1577
|
+
summary_parts = []
|
|
1578
|
+
if code_text:
|
|
1579
|
+
summary_parts.append(code_text)
|
|
1580
|
+
if description_text:
|
|
1581
|
+
summary_parts.append(description_text)
|
|
1582
|
+
if details_text:
|
|
1583
|
+
summary_parts.append(details_text)
|
|
1584
|
+
reservation.evcs_status = ""
|
|
1585
|
+
reservation.evcs_error = "; ".join(summary_parts)
|
|
1586
|
+
reservation.evcs_confirmed = False
|
|
1587
|
+
reservation.evcs_confirmed_at = None
|
|
1588
|
+
reservation.save(
|
|
1589
|
+
update_fields=[
|
|
1590
|
+
"evcs_status",
|
|
1591
|
+
"evcs_error",
|
|
1592
|
+
"evcs_confirmed",
|
|
1593
|
+
"evcs_confirmed_at",
|
|
1594
|
+
"updated_on",
|
|
1595
|
+
]
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
await database_sync_to_async(_apply)()
|
|
1599
|
+
store.record_pending_call_result(
|
|
1600
|
+
message_id,
|
|
1601
|
+
metadata=metadata,
|
|
1602
|
+
success=False,
|
|
1603
|
+
error_code=error_code,
|
|
1604
|
+
error_description=description,
|
|
1605
|
+
error_details=details,
|
|
1606
|
+
)
|
|
1607
|
+
return
|
|
1608
|
+
if action == "CancelReservation":
|
|
1609
|
+
parts: list[str] = []
|
|
1610
|
+
code_text = (error_code or "").strip() if error_code else ""
|
|
1611
|
+
if code_text:
|
|
1612
|
+
parts.append(f"code={code_text}")
|
|
1613
|
+
description_text = (description or "").strip() if description else ""
|
|
1614
|
+
if description_text:
|
|
1615
|
+
parts.append(f"description={description_text}")
|
|
1616
|
+
details_text = ""
|
|
1617
|
+
if details:
|
|
1618
|
+
try:
|
|
1619
|
+
details_text = json.dumps(details, sort_keys=True, ensure_ascii=False)
|
|
1620
|
+
except TypeError:
|
|
1621
|
+
details_text = str(details)
|
|
1622
|
+
if details_text:
|
|
1623
|
+
parts.append(f"details={details_text}")
|
|
1624
|
+
message = "CancelReservation error"
|
|
1625
|
+
if parts:
|
|
1626
|
+
message += ": " + ", ".join(parts)
|
|
1627
|
+
store.add_log(log_key, message, log_type="charger")
|
|
1628
|
+
|
|
1629
|
+
reservation_pk = metadata.get("reservation_pk")
|
|
1630
|
+
|
|
1631
|
+
def _apply():
|
|
1632
|
+
if not reservation_pk:
|
|
1633
|
+
return
|
|
1634
|
+
reservation = CPReservation.objects.filter(pk=reservation_pk).first()
|
|
1635
|
+
if not reservation:
|
|
1636
|
+
return
|
|
1637
|
+
summary_parts = []
|
|
1638
|
+
if code_text:
|
|
1639
|
+
summary_parts.append(code_text)
|
|
1640
|
+
if description_text:
|
|
1641
|
+
summary_parts.append(description_text)
|
|
1642
|
+
if details_text:
|
|
1643
|
+
summary_parts.append(details_text)
|
|
1644
|
+
reservation.evcs_status = ""
|
|
1645
|
+
reservation.evcs_error = "; ".join(summary_parts)
|
|
1646
|
+
reservation.evcs_confirmed = False
|
|
1647
|
+
reservation.evcs_confirmed_at = None
|
|
1648
|
+
reservation.save(
|
|
1649
|
+
update_fields=[
|
|
1650
|
+
"evcs_status",
|
|
1651
|
+
"evcs_error",
|
|
1652
|
+
"evcs_confirmed",
|
|
1653
|
+
"evcs_confirmed_at",
|
|
1654
|
+
"updated_on",
|
|
1655
|
+
]
|
|
1656
|
+
)
|
|
1657
|
+
|
|
1658
|
+
await database_sync_to_async(_apply)()
|
|
1659
|
+
store.record_pending_call_result(
|
|
1660
|
+
message_id,
|
|
1661
|
+
metadata=metadata,
|
|
1662
|
+
success=False,
|
|
1663
|
+
error_code=error_code,
|
|
1664
|
+
error_description=description,
|
|
1665
|
+
error_details=details,
|
|
1666
|
+
)
|
|
1667
|
+
return
|
|
986
1668
|
if action == "RemoteStartTransaction":
|
|
987
1669
|
message = "RemoteStartTransaction error"
|
|
988
1670
|
if error_code:
|
|
@@ -1217,6 +1899,77 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1217
1899
|
|
|
1218
1900
|
await database_sync_to_async(_apply)()
|
|
1219
1901
|
|
|
1902
|
+
async def _update_local_authorization_state(self, version: int | None) -> None:
|
|
1903
|
+
"""Persist the reported local authorization list version."""
|
|
1904
|
+
|
|
1905
|
+
timestamp = timezone.now()
|
|
1906
|
+
|
|
1907
|
+
def _apply() -> None:
|
|
1908
|
+
updates: dict[str, object] = {"local_auth_list_updated_at": timestamp}
|
|
1909
|
+
if version is not None:
|
|
1910
|
+
updates["local_auth_list_version"] = int(version)
|
|
1911
|
+
|
|
1912
|
+
targets: list[Charger] = []
|
|
1913
|
+
if self.charger and getattr(self.charger, "pk", None):
|
|
1914
|
+
targets.append(self.charger)
|
|
1915
|
+
aggregate = self.aggregate_charger
|
|
1916
|
+
if (
|
|
1917
|
+
aggregate
|
|
1918
|
+
and getattr(aggregate, "pk", None)
|
|
1919
|
+
and not any(target.pk == aggregate.pk for target in targets if target.pk)
|
|
1920
|
+
):
|
|
1921
|
+
targets.append(aggregate)
|
|
1922
|
+
|
|
1923
|
+
if not targets:
|
|
1924
|
+
return
|
|
1925
|
+
|
|
1926
|
+
for target in targets:
|
|
1927
|
+
Charger.objects.filter(pk=target.pk).update(**updates)
|
|
1928
|
+
for field, value in updates.items():
|
|
1929
|
+
setattr(target, field, value)
|
|
1930
|
+
|
|
1931
|
+
await database_sync_to_async(_apply)()
|
|
1932
|
+
|
|
1933
|
+
async def _apply_local_authorization_entries(
|
|
1934
|
+
self, entries: list[dict[str, object]]
|
|
1935
|
+
) -> int:
|
|
1936
|
+
"""Create or update RFID records from a local authorization list."""
|
|
1937
|
+
|
|
1938
|
+
def _apply() -> int:
|
|
1939
|
+
processed = 0
|
|
1940
|
+
now = timezone.now()
|
|
1941
|
+
for entry in entries:
|
|
1942
|
+
if not isinstance(entry, dict):
|
|
1943
|
+
continue
|
|
1944
|
+
id_tag = entry.get("idTag")
|
|
1945
|
+
id_tag_text = str(id_tag or "").strip().upper()
|
|
1946
|
+
if not id_tag_text:
|
|
1947
|
+
continue
|
|
1948
|
+
info = entry.get("idTagInfo")
|
|
1949
|
+
status_value = ""
|
|
1950
|
+
if isinstance(info, dict):
|
|
1951
|
+
status_value = str(info.get("status") or "").strip()
|
|
1952
|
+
status_key = status_value.lower()
|
|
1953
|
+
allowed_flag = status_key in {"", "accepted", "concurrenttx"}
|
|
1954
|
+
defaults = {"allowed": allowed_flag, "released": allowed_flag}
|
|
1955
|
+
tag, _ = CoreRFID.update_or_create_from_code(id_tag_text, defaults)
|
|
1956
|
+
updates: set[str] = set()
|
|
1957
|
+
if tag.allowed != allowed_flag:
|
|
1958
|
+
tag.allowed = allowed_flag
|
|
1959
|
+
updates.add("allowed")
|
|
1960
|
+
if tag.released != allowed_flag:
|
|
1961
|
+
tag.released = allowed_flag
|
|
1962
|
+
updates.add("released")
|
|
1963
|
+
if tag.last_seen_on != now:
|
|
1964
|
+
tag.last_seen_on = now
|
|
1965
|
+
updates.add("last_seen_on")
|
|
1966
|
+
if updates:
|
|
1967
|
+
tag.save(update_fields=sorted(updates))
|
|
1968
|
+
processed += 1
|
|
1969
|
+
return processed
|
|
1970
|
+
|
|
1971
|
+
return await database_sync_to_async(_apply)()
|
|
1972
|
+
|
|
1220
1973
|
async def _update_availability_state(
|
|
1221
1974
|
self,
|
|
1222
1975
|
state: str,
|
|
@@ -1321,8 +2074,13 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1321
2074
|
reply_payload = {"currentTime": datetime.utcnow().isoformat() + "Z"}
|
|
1322
2075
|
now = timezone.now()
|
|
1323
2076
|
self.charger.last_heartbeat = now
|
|
2077
|
+
if (
|
|
2078
|
+
self.aggregate_charger
|
|
2079
|
+
and self.aggregate_charger is not self.charger
|
|
2080
|
+
):
|
|
2081
|
+
self.aggregate_charger.last_heartbeat = now
|
|
1324
2082
|
await database_sync_to_async(
|
|
1325
|
-
Charger.objects.filter(
|
|
2083
|
+
Charger.objects.filter(charger_id=self.charger_id).update
|
|
1326
2084
|
)(last_heartbeat=now)
|
|
1327
2085
|
elif action == "StatusNotification":
|
|
1328
2086
|
await self._assign_connector(payload.get("connectorId"))
|
|
@@ -1395,13 +2153,25 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1395
2153
|
elif action == "Authorize":
|
|
1396
2154
|
id_tag = payload.get("idTag")
|
|
1397
2155
|
account = await self._get_account(id_tag)
|
|
2156
|
+
status = "Invalid"
|
|
1398
2157
|
if self.charger.require_rfid:
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
2158
|
+
tag = None
|
|
2159
|
+
tag_created = False
|
|
2160
|
+
if id_tag:
|
|
2161
|
+
tag, tag_created = await database_sync_to_async(
|
|
2162
|
+
CoreRFID.register_scan
|
|
2163
|
+
)(id_tag)
|
|
2164
|
+
if account:
|
|
2165
|
+
if await database_sync_to_async(account.can_authorize)():
|
|
2166
|
+
status = "Accepted"
|
|
2167
|
+
elif (
|
|
2168
|
+
id_tag
|
|
2169
|
+
and tag
|
|
2170
|
+
and not tag_created
|
|
2171
|
+
and tag.allowed
|
|
2172
|
+
):
|
|
2173
|
+
status = "Accepted"
|
|
2174
|
+
self._log_unlinked_rfid(tag.rfid)
|
|
1405
2175
|
else:
|
|
1406
2176
|
await self._ensure_rfid_seen(id_tag)
|
|
1407
2177
|
status = "Accepted"
|
|
@@ -1475,30 +2245,47 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1475
2245
|
reply_payload = {}
|
|
1476
2246
|
elif action == "StartTransaction":
|
|
1477
2247
|
id_tag = payload.get("idTag")
|
|
1478
|
-
|
|
2248
|
+
tag = None
|
|
2249
|
+
tag_created = False
|
|
1479
2250
|
if id_tag:
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
2251
|
+
tag, tag_created = await database_sync_to_async(
|
|
2252
|
+
CoreRFID.register_scan
|
|
2253
|
+
)(id_tag)
|
|
2254
|
+
account = await self._get_account(id_tag)
|
|
2255
|
+
if id_tag and not self.charger.require_rfid:
|
|
2256
|
+
seen_tag = await self._ensure_rfid_seen(id_tag)
|
|
2257
|
+
if seen_tag:
|
|
2258
|
+
tag = seen_tag
|
|
1486
2259
|
await self._assign_connector(payload.get("connectorId"))
|
|
2260
|
+
authorized = True
|
|
2261
|
+
authorized_via_tag = False
|
|
1487
2262
|
if self.charger.require_rfid:
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
2263
|
+
if account is not None:
|
|
2264
|
+
authorized = await database_sync_to_async(
|
|
2265
|
+
account.can_authorize
|
|
2266
|
+
)()
|
|
2267
|
+
elif (
|
|
2268
|
+
id_tag
|
|
2269
|
+
and tag
|
|
2270
|
+
and not tag_created
|
|
2271
|
+
and getattr(tag, "allowed", False)
|
|
2272
|
+
):
|
|
2273
|
+
authorized = True
|
|
2274
|
+
authorized_via_tag = True
|
|
2275
|
+
else:
|
|
2276
|
+
authorized = False
|
|
1494
2277
|
if authorized:
|
|
2278
|
+
if authorized_via_tag and tag:
|
|
2279
|
+
self._log_unlinked_rfid(tag.rfid)
|
|
1495
2280
|
start_timestamp = _parse_ocpp_timestamp(payload.get("timestamp"))
|
|
1496
2281
|
received_start = timezone.now()
|
|
2282
|
+
vid_value, vin_value = _extract_vehicle_identifier(payload)
|
|
1497
2283
|
tx_obj = await database_sync_to_async(Transaction.objects.create)(
|
|
1498
2284
|
charger=self.charger,
|
|
1499
2285
|
account=account,
|
|
1500
2286
|
rfid=(id_tag or ""),
|
|
1501
|
-
|
|
2287
|
+
vid=vid_value,
|
|
2288
|
+
vin=vin_value,
|
|
1502
2289
|
connector_id=payload.get("connectorId"),
|
|
1503
2290
|
meter_start=payload.get("meterStart"),
|
|
1504
2291
|
start_time=start_timestamp or received_start,
|
|
@@ -1509,12 +2296,23 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1509
2296
|
store.start_session_lock()
|
|
1510
2297
|
store.add_session_message(self.store_key, text_data)
|
|
1511
2298
|
await self._start_consumption_updates(tx_obj)
|
|
2299
|
+
await self._record_rfid_attempt(
|
|
2300
|
+
rfid=id_tag or "",
|
|
2301
|
+
status=RFIDSessionAttempt.Status.ACCEPTED,
|
|
2302
|
+
account=account,
|
|
2303
|
+
transaction=tx_obj,
|
|
2304
|
+
)
|
|
1512
2305
|
reply_payload = {
|
|
1513
2306
|
"transactionId": tx_obj.pk,
|
|
1514
2307
|
"idTagInfo": {"status": "Accepted"},
|
|
1515
2308
|
}
|
|
1516
2309
|
else:
|
|
1517
2310
|
reply_payload = {"idTagInfo": {"status": "Invalid"}}
|
|
2311
|
+
await self._record_rfid_attempt(
|
|
2312
|
+
rfid=id_tag or "",
|
|
2313
|
+
status=RFIDSessionAttempt.Status.REJECTED,
|
|
2314
|
+
account=account,
|
|
2315
|
+
)
|
|
1518
2316
|
elif action == "StopTransaction":
|
|
1519
2317
|
tx_id = payload.get("transactionId")
|
|
1520
2318
|
tx_obj = store.transactions.pop(self.store_key, None)
|
|
@@ -1524,6 +2322,7 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1524
2322
|
)()
|
|
1525
2323
|
if not tx_obj and tx_id is not None:
|
|
1526
2324
|
received_start = timezone.now()
|
|
2325
|
+
vid_value, vin_value = _extract_vehicle_identifier(payload)
|
|
1527
2326
|
tx_obj = await database_sync_to_async(Transaction.objects.create)(
|
|
1528
2327
|
pk=tx_id,
|
|
1529
2328
|
charger=self.charger,
|
|
@@ -1531,12 +2330,18 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1531
2330
|
received_start_time=received_start,
|
|
1532
2331
|
meter_start=payload.get("meterStart")
|
|
1533
2332
|
or payload.get("meterStop"),
|
|
1534
|
-
|
|
2333
|
+
vid=vid_value,
|
|
2334
|
+
vin=vin_value,
|
|
1535
2335
|
)
|
|
1536
2336
|
if tx_obj:
|
|
1537
2337
|
stop_timestamp = _parse_ocpp_timestamp(payload.get("timestamp"))
|
|
1538
2338
|
received_stop = timezone.now()
|
|
1539
2339
|
tx_obj.meter_stop = payload.get("meterStop")
|
|
2340
|
+
vid_value, vin_value = _extract_vehicle_identifier(payload)
|
|
2341
|
+
if vid_value:
|
|
2342
|
+
tx_obj.vid = vid_value
|
|
2343
|
+
if vin_value:
|
|
2344
|
+
tx_obj.vin = vin_value
|
|
1540
2345
|
tx_obj.stop_time = stop_timestamp or received_stop
|
|
1541
2346
|
tx_obj.received_stop_time = received_stop
|
|
1542
2347
|
await database_sync_to_async(tx_obj.save)()
|