arthexis 0.1.26__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.26.dist-info → arthexis-0.1.28.dist-info}/METADATA +16 -11
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/RECORD +39 -38
- config/settings.py +7 -1
- config/settings_helpers.py +176 -1
- config/urls.py +18 -2
- core/admin.py +265 -23
- core/apps.py +6 -2
- core/celery_utils.py +73 -0
- core/models.py +307 -63
- core/system.py +17 -2
- core/tasks.py +304 -129
- core/test_system_info.py +43 -5
- core/tests.py +202 -2
- core/user_data.py +52 -19
- core/views.py +70 -3
- nodes/admin.py +348 -3
- nodes/apps.py +1 -1
- nodes/feature_checks.py +30 -0
- nodes/models.py +146 -18
- nodes/tasks.py +1 -1
- nodes/tests.py +181 -48
- nodes/views.py +148 -3
- ocpp/admin.py +1001 -10
- ocpp/consumers.py +572 -7
- ocpp/models.py +499 -33
- ocpp/store.py +406 -40
- ocpp/tasks.py +109 -145
- ocpp/test_rfid.py +73 -2
- ocpp/tests.py +982 -90
- ocpp/urls.py +5 -0
- ocpp/views.py +172 -70
- pages/context_processors.py +2 -0
- pages/models.py +9 -0
- pages/tests.py +166 -18
- pages/urls.py +1 -0
- pages/views.py +66 -3
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.26.dist-info → arthexis-0.1.28.dist-info}/top_level.txt +0 -0
ocpp/consumers.py
CHANGED
|
@@ -3,6 +3,7 @@ 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
|
|
8
9
|
import logging
|
|
@@ -27,6 +28,8 @@ from .models import (
|
|
|
27
28
|
MeterValue,
|
|
28
29
|
DataTransferMessage,
|
|
29
30
|
CPReservation,
|
|
31
|
+
CPFirmwareDeployment,
|
|
32
|
+
RFIDSessionAttempt,
|
|
30
33
|
)
|
|
31
34
|
from .reference_utils import host_is_local_loopback
|
|
32
35
|
from .evcs_discovery import (
|
|
@@ -217,8 +220,20 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
217
220
|
trimmed = value.strip()
|
|
218
221
|
if trimmed:
|
|
219
222
|
return trimmed
|
|
220
|
-
|
|
221
|
-
|
|
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]
|
|
222
237
|
|
|
223
238
|
@requires_network
|
|
224
239
|
async def connect(self):
|
|
@@ -276,7 +291,9 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
276
291
|
log_type="charger",
|
|
277
292
|
)
|
|
278
293
|
store.connections[self.store_key] = self
|
|
279
|
-
store.logs["charger"].setdefault(
|
|
294
|
+
store.logs["charger"].setdefault(
|
|
295
|
+
self.store_key, deque(maxlen=store.MAX_IN_MEMORY_LOG_ENTRIES)
|
|
296
|
+
)
|
|
280
297
|
self.charger, created = await database_sync_to_async(
|
|
281
298
|
Charger.objects.get_or_create
|
|
282
299
|
)(
|
|
@@ -330,11 +347,14 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
330
347
|
if not tag.allowed:
|
|
331
348
|
tag.allowed = True
|
|
332
349
|
updates.append("allowed")
|
|
350
|
+
if not tag.released:
|
|
351
|
+
tag.released = True
|
|
352
|
+
updates.append("released")
|
|
333
353
|
if tag.last_seen_on != now:
|
|
334
354
|
tag.last_seen_on = now
|
|
335
355
|
updates.append("last_seen_on")
|
|
336
356
|
if updates:
|
|
337
|
-
tag.save(update_fields=updates)
|
|
357
|
+
tag.save(update_fields=sorted(set(updates)))
|
|
338
358
|
return tag
|
|
339
359
|
|
|
340
360
|
return await database_sync_to_async(_ensure)()
|
|
@@ -352,6 +372,33 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
352
372
|
log_type="charger",
|
|
353
373
|
)
|
|
354
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
|
+
|
|
355
402
|
async def _assign_connector(self, connector: int | str | None) -> None:
|
|
356
403
|
"""Ensure ``self.charger`` matches the provided connector id."""
|
|
357
404
|
if connector in (None, "", "-"):
|
|
@@ -388,7 +435,9 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
388
435
|
await existing_consumer.close()
|
|
389
436
|
store.reassign_identity(previous_key, new_key)
|
|
390
437
|
store.connections[new_key] = self
|
|
391
|
-
store.logs["charger"].setdefault(
|
|
438
|
+
store.logs["charger"].setdefault(
|
|
439
|
+
new_key, deque(maxlen=store.MAX_IN_MEMORY_LOG_ENTRIES)
|
|
440
|
+
)
|
|
392
441
|
aggregate_name = await sync_to_async(
|
|
393
442
|
lambda: self.charger.name or self.charger.charger_id
|
|
394
443
|
)()
|
|
@@ -457,7 +506,9 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
457
506
|
await existing_consumer.close()
|
|
458
507
|
store.reassign_identity(previous_key, new_key)
|
|
459
508
|
store.connections[new_key] = self
|
|
460
|
-
store.logs["charger"].setdefault(
|
|
509
|
+
store.logs["charger"].setdefault(
|
|
510
|
+
new_key, deque(maxlen=store.MAX_IN_MEMORY_LOG_ENTRIES)
|
|
511
|
+
)
|
|
461
512
|
connector_name = await sync_to_async(
|
|
462
513
|
lambda: self.charger.name or self.charger.charger_id
|
|
463
514
|
)()
|
|
@@ -656,6 +707,23 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
656
707
|
target.firmware_status_info = status_info
|
|
657
708
|
target.firmware_timestamp = timestamp
|
|
658
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
|
+
|
|
659
727
|
async def _cancel_consumption_message(self) -> None:
|
|
660
728
|
"""Stop any scheduled consumption message updates."""
|
|
661
729
|
|
|
@@ -790,11 +858,93 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
790
858
|
configuration = ChargerConfiguration.objects.create(
|
|
791
859
|
charger_identifier=self.charger_id,
|
|
792
860
|
connector_id=connector_value,
|
|
793
|
-
configuration_keys=normalized_entries,
|
|
794
861
|
unknown_keys=unknown_values,
|
|
795
862
|
evcs_snapshot_at=timezone.now(),
|
|
796
863
|
raw_payload=raw_payload,
|
|
797
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"])
|
|
798
948
|
Charger.objects.filter(charger_id=self.charger_id).update(
|
|
799
949
|
configuration=configuration
|
|
800
950
|
)
|
|
@@ -809,6 +959,46 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
809
959
|
action = metadata.get("action")
|
|
810
960
|
log_key = metadata.get("log_key") or self.store_key
|
|
811
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
|
|
812
1002
|
if action == "DataTransfer":
|
|
813
1003
|
message_pk = metadata.get("message_pk")
|
|
814
1004
|
if not message_pk:
|
|
@@ -842,6 +1032,97 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
842
1032
|
]
|
|
843
1033
|
)
|
|
844
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
|
+
|
|
845
1126
|
await database_sync_to_async(_apply)()
|
|
846
1127
|
store.record_pending_call_result(
|
|
847
1128
|
message_id,
|
|
@@ -934,6 +1215,42 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
934
1215
|
]
|
|
935
1216
|
)
|
|
936
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
|
+
|
|
937
1254
|
await database_sync_to_async(_apply)()
|
|
938
1255
|
store.record_pending_call_result(
|
|
939
1256
|
message_id,
|
|
@@ -1015,6 +1332,36 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1015
1332
|
return
|
|
1016
1333
|
action = metadata.get("action")
|
|
1017
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
|
|
1018
1365
|
if action == "DataTransfer":
|
|
1019
1366
|
message_pk = metadata.get("message_pk")
|
|
1020
1367
|
if not message_pk:
|
|
@@ -1061,6 +1408,35 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1061
1408
|
error_details=details,
|
|
1062
1409
|
)
|
|
1063
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
|
|
1064
1440
|
if action == "GetConfiguration":
|
|
1065
1441
|
parts: list[str] = []
|
|
1066
1442
|
code_text = (error_code or "").strip()
|
|
@@ -1122,6 +1498,53 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1122
1498
|
error_details=details,
|
|
1123
1499
|
)
|
|
1124
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
|
|
1125
1548
|
if action == "ReserveNow":
|
|
1126
1549
|
parts: list[str] = []
|
|
1127
1550
|
code_text = (error_code or "").strip() if error_code else ""
|
|
@@ -1145,6 +1568,66 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1145
1568
|
|
|
1146
1569
|
reservation_pk = metadata.get("reservation_pk")
|
|
1147
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
|
+
|
|
1148
1631
|
def _apply():
|
|
1149
1632
|
if not reservation_pk:
|
|
1150
1633
|
return
|
|
@@ -1416,6 +1899,77 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1416
1899
|
|
|
1417
1900
|
await database_sync_to_async(_apply)()
|
|
1418
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
|
+
|
|
1419
1973
|
async def _update_availability_state(
|
|
1420
1974
|
self,
|
|
1421
1975
|
state: str,
|
|
@@ -1742,12 +2296,23 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1742
2296
|
store.start_session_lock()
|
|
1743
2297
|
store.add_session_message(self.store_key, text_data)
|
|
1744
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
|
+
)
|
|
1745
2305
|
reply_payload = {
|
|
1746
2306
|
"transactionId": tx_obj.pk,
|
|
1747
2307
|
"idTagInfo": {"status": "Accepted"},
|
|
1748
2308
|
}
|
|
1749
2309
|
else:
|
|
1750
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
|
+
)
|
|
1751
2316
|
elif action == "StopTransaction":
|
|
1752
2317
|
tx_id = payload.get("transactionId")
|
|
1753
2318
|
tx_obj = store.transactions.pop(self.store_key, None)
|