arthexis 0.1.23__py3-none-any.whl → 0.1.25__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.23.dist-info → arthexis-0.1.25.dist-info}/METADATA +39 -18
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/RECORD +31 -30
- config/settings.py +7 -0
- config/urls.py +2 -0
- core/admin.py +140 -213
- core/backends.py +3 -1
- core/models.py +612 -207
- core/system.py +67 -2
- core/tasks.py +25 -0
- core/views.py +0 -3
- nodes/admin.py +465 -292
- nodes/models.py +299 -23
- nodes/tasks.py +13 -16
- nodes/tests.py +291 -130
- nodes/urls.py +11 -0
- nodes/utils.py +9 -2
- nodes/views.py +588 -20
- ocpp/admin.py +729 -175
- ocpp/consumers.py +98 -0
- ocpp/models.py +299 -0
- ocpp/network.py +398 -0
- ocpp/tasks.py +177 -1
- ocpp/tests.py +179 -0
- ocpp/views.py +2 -0
- pages/middleware.py +3 -2
- pages/tests.py +40 -0
- pages/utils.py +70 -0
- pages/views.py +64 -32
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/WHEEL +0 -0
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/top_level.txt +0 -0
ocpp/consumers.py
CHANGED
|
@@ -26,6 +26,7 @@ from .models import (
|
|
|
26
26
|
ChargerConfiguration,
|
|
27
27
|
MeterValue,
|
|
28
28
|
DataTransferMessage,
|
|
29
|
+
CPReservation,
|
|
29
30
|
)
|
|
30
31
|
from .reference_utils import host_is_local_loopback
|
|
31
32
|
from .evcs_discovery import (
|
|
@@ -903,6 +904,43 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
903
904
|
payload=payload_data,
|
|
904
905
|
)
|
|
905
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
|
|
906
944
|
if action == "RemoteStartTransaction":
|
|
907
945
|
status_value = str(payload_data.get("status") or "").strip()
|
|
908
946
|
message = "RemoteStartTransaction result"
|
|
@@ -1084,6 +1122,66 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
1084
1122
|
error_details=details,
|
|
1085
1123
|
)
|
|
1086
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
|
|
1087
1185
|
if action == "RemoteStartTransaction":
|
|
1088
1186
|
message = "RemoteStartTransaction error"
|
|
1089
1187
|
if error_code:
|
ocpp/models.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import re
|
|
2
3
|
import socket
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import timedelta
|
|
3
6
|
from decimal import Decimal, InvalidOperation
|
|
4
7
|
|
|
5
8
|
from django.conf import settings
|
|
@@ -9,6 +12,9 @@ from django.db.models import Q
|
|
|
9
12
|
from django.core.exceptions import ValidationError
|
|
10
13
|
from django.urls import reverse
|
|
11
14
|
from django.utils.translation import gettext_lazy as _
|
|
15
|
+
from django.utils import timezone
|
|
16
|
+
|
|
17
|
+
from asgiref.sync import async_to_sync
|
|
12
18
|
|
|
13
19
|
from core.entity import Entity, EntityManager
|
|
14
20
|
from nodes.models import Node
|
|
@@ -23,6 +29,7 @@ from core.models import (
|
|
|
23
29
|
EVModel as CoreEVModel,
|
|
24
30
|
SecurityGroup,
|
|
25
31
|
)
|
|
32
|
+
from . import store
|
|
26
33
|
from .reference_utils import url_targets_local_loopback
|
|
27
34
|
|
|
28
35
|
|
|
@@ -232,6 +239,13 @@ class Charger(Entity):
|
|
|
232
239
|
"Latest GetConfiguration response received from this charge point."
|
|
233
240
|
),
|
|
234
241
|
)
|
|
242
|
+
node_origin = models.ForeignKey(
|
|
243
|
+
"nodes.Node",
|
|
244
|
+
on_delete=models.SET_NULL,
|
|
245
|
+
null=True,
|
|
246
|
+
blank=True,
|
|
247
|
+
related_name="origin_chargers",
|
|
248
|
+
)
|
|
235
249
|
manager_node = models.ForeignKey(
|
|
236
250
|
"nodes.Node",
|
|
237
251
|
on_delete=models.SET_NULL,
|
|
@@ -239,6 +253,22 @@ class Charger(Entity):
|
|
|
239
253
|
blank=True,
|
|
240
254
|
related_name="managed_chargers",
|
|
241
255
|
)
|
|
256
|
+
forwarded_to = models.ForeignKey(
|
|
257
|
+
"nodes.Node",
|
|
258
|
+
on_delete=models.SET_NULL,
|
|
259
|
+
null=True,
|
|
260
|
+
blank=True,
|
|
261
|
+
related_name="forwarded_chargers",
|
|
262
|
+
help_text=_("Remote node receiving forwarded transactions."),
|
|
263
|
+
)
|
|
264
|
+
forwarding_watermark = models.DateTimeField(
|
|
265
|
+
null=True,
|
|
266
|
+
blank=True,
|
|
267
|
+
help_text=_("Timestamp of the last forwarded transaction."),
|
|
268
|
+
)
|
|
269
|
+
allow_remote = models.BooleanField(default=False)
|
|
270
|
+
export_transactions = models.BooleanField(default=False)
|
|
271
|
+
last_online_at = models.DateTimeField(null=True, blank=True)
|
|
242
272
|
owner_users = models.ManyToManyField(
|
|
243
273
|
settings.AUTH_USER_MODEL,
|
|
244
274
|
blank=True,
|
|
@@ -293,6 +323,24 @@ class Charger(Entity):
|
|
|
293
323
|
user_group_ids = user.groups.values_list("pk", flat=True)
|
|
294
324
|
return self.owner_groups.filter(pk__in=user_group_ids).exists()
|
|
295
325
|
|
|
326
|
+
@property
|
|
327
|
+
def is_local(self) -> bool:
|
|
328
|
+
"""Return ``True`` when this charger originates from the local node."""
|
|
329
|
+
|
|
330
|
+
local = Node.get_local()
|
|
331
|
+
if not local:
|
|
332
|
+
return False
|
|
333
|
+
if self.node_origin_id is None:
|
|
334
|
+
return True
|
|
335
|
+
return self.node_origin_id == local.pk
|
|
336
|
+
|
|
337
|
+
def save(self, *args, **kwargs):
|
|
338
|
+
if self.node_origin_id is None:
|
|
339
|
+
local = Node.get_local()
|
|
340
|
+
if local:
|
|
341
|
+
self.node_origin = local
|
|
342
|
+
super().save(*args, **kwargs)
|
|
343
|
+
|
|
296
344
|
class Meta:
|
|
297
345
|
verbose_name = _("Charge Point")
|
|
298
346
|
verbose_name_plural = _("Charge Points")
|
|
@@ -1086,6 +1134,257 @@ class DataTransferMessage(models.Model):
|
|
|
1086
1134
|
return f"{self.get_direction_display()} {self.vendor_id or 'DataTransfer'}"
|
|
1087
1135
|
|
|
1088
1136
|
|
|
1137
|
+
class CPReservation(Entity):
|
|
1138
|
+
"""Track connector reservations dispatched to an EVCS."""
|
|
1139
|
+
|
|
1140
|
+
location = models.ForeignKey(
|
|
1141
|
+
Location,
|
|
1142
|
+
on_delete=models.PROTECT,
|
|
1143
|
+
related_name="reservations",
|
|
1144
|
+
verbose_name=_("Location"),
|
|
1145
|
+
)
|
|
1146
|
+
connector = models.ForeignKey(
|
|
1147
|
+
Charger,
|
|
1148
|
+
on_delete=models.PROTECT,
|
|
1149
|
+
related_name="reservations",
|
|
1150
|
+
verbose_name=_("Connector"),
|
|
1151
|
+
)
|
|
1152
|
+
account = models.ForeignKey(
|
|
1153
|
+
EnergyAccount,
|
|
1154
|
+
on_delete=models.SET_NULL,
|
|
1155
|
+
null=True,
|
|
1156
|
+
blank=True,
|
|
1157
|
+
related_name="cp_reservations",
|
|
1158
|
+
verbose_name=_("Energy account"),
|
|
1159
|
+
)
|
|
1160
|
+
rfid = models.ForeignKey(
|
|
1161
|
+
CoreRFID,
|
|
1162
|
+
on_delete=models.SET_NULL,
|
|
1163
|
+
null=True,
|
|
1164
|
+
blank=True,
|
|
1165
|
+
related_name="cp_reservations",
|
|
1166
|
+
verbose_name=_("RFID"),
|
|
1167
|
+
)
|
|
1168
|
+
id_tag = models.CharField(
|
|
1169
|
+
_("Id Tag"),
|
|
1170
|
+
max_length=255,
|
|
1171
|
+
blank=True,
|
|
1172
|
+
default="",
|
|
1173
|
+
help_text=_("Identifier sent to the EVCS when reserving the connector."),
|
|
1174
|
+
)
|
|
1175
|
+
start_time = models.DateTimeField(verbose_name=_("Start time"))
|
|
1176
|
+
duration_minutes = models.PositiveIntegerField(
|
|
1177
|
+
verbose_name=_("Duration (minutes)"),
|
|
1178
|
+
default=120,
|
|
1179
|
+
help_text=_("Reservation window length in minutes."),
|
|
1180
|
+
)
|
|
1181
|
+
evcs_status = models.CharField(
|
|
1182
|
+
max_length=32,
|
|
1183
|
+
blank=True,
|
|
1184
|
+
default="",
|
|
1185
|
+
verbose_name=_("EVCS status"),
|
|
1186
|
+
)
|
|
1187
|
+
evcs_error = models.CharField(
|
|
1188
|
+
max_length=255,
|
|
1189
|
+
blank=True,
|
|
1190
|
+
default="",
|
|
1191
|
+
verbose_name=_("EVCS error"),
|
|
1192
|
+
)
|
|
1193
|
+
evcs_confirmed = models.BooleanField(
|
|
1194
|
+
default=False,
|
|
1195
|
+
verbose_name=_("Reservation confirmed"),
|
|
1196
|
+
)
|
|
1197
|
+
evcs_confirmed_at = models.DateTimeField(
|
|
1198
|
+
null=True,
|
|
1199
|
+
blank=True,
|
|
1200
|
+
verbose_name=_("Confirmed at"),
|
|
1201
|
+
)
|
|
1202
|
+
ocpp_message_id = models.CharField(
|
|
1203
|
+
max_length=36,
|
|
1204
|
+
blank=True,
|
|
1205
|
+
default="",
|
|
1206
|
+
editable=False,
|
|
1207
|
+
verbose_name=_("OCPP message id"),
|
|
1208
|
+
)
|
|
1209
|
+
created_on = models.DateTimeField(auto_now_add=True, verbose_name=_("Created on"))
|
|
1210
|
+
updated_on = models.DateTimeField(auto_now=True, verbose_name=_("Updated on"))
|
|
1211
|
+
|
|
1212
|
+
class Meta:
|
|
1213
|
+
ordering = ["-start_time"]
|
|
1214
|
+
verbose_name = _("CP Reservation")
|
|
1215
|
+
verbose_name_plural = _("CP Reservations")
|
|
1216
|
+
|
|
1217
|
+
def __str__(self) -> str: # pragma: no cover - simple representation
|
|
1218
|
+
start = timezone.localtime(self.start_time) if self.start_time else ""
|
|
1219
|
+
return f"{self.location} @ {start}" if self.location else str(start)
|
|
1220
|
+
|
|
1221
|
+
@property
|
|
1222
|
+
def end_time(self):
|
|
1223
|
+
duration = max(int(self.duration_minutes or 0), 0)
|
|
1224
|
+
return self.start_time + timedelta(minutes=duration)
|
|
1225
|
+
|
|
1226
|
+
@property
|
|
1227
|
+
def connector_label(self) -> str:
|
|
1228
|
+
if self.connector_id:
|
|
1229
|
+
return self.connector.connector_label
|
|
1230
|
+
return ""
|
|
1231
|
+
|
|
1232
|
+
@property
|
|
1233
|
+
def id_tag_value(self) -> str:
|
|
1234
|
+
if self.id_tag:
|
|
1235
|
+
return self.id_tag.strip()
|
|
1236
|
+
if self.rfid_id:
|
|
1237
|
+
return (self.rfid.rfid or "").strip()
|
|
1238
|
+
return ""
|
|
1239
|
+
|
|
1240
|
+
def allocate_connector(self, *, force: bool = False) -> Charger:
|
|
1241
|
+
"""Select an available connector for this reservation."""
|
|
1242
|
+
|
|
1243
|
+
if not self.location_id:
|
|
1244
|
+
raise ValidationError({"location": _("Select a location for the reservation.")})
|
|
1245
|
+
if not self.start_time:
|
|
1246
|
+
raise ValidationError({"start_time": _("Provide a start time for the reservation.")})
|
|
1247
|
+
if self.duration_minutes <= 0:
|
|
1248
|
+
raise ValidationError(
|
|
1249
|
+
{"duration_minutes": _("Reservation window must be at least one minute.")}
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
candidates = list(
|
|
1253
|
+
Charger.objects.filter(
|
|
1254
|
+
location=self.location, connector_id__isnull=False
|
|
1255
|
+
).order_by("connector_id")
|
|
1256
|
+
)
|
|
1257
|
+
if not candidates:
|
|
1258
|
+
raise ValidationError(
|
|
1259
|
+
{"location": _("No connectors are configured for the selected location.")}
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
def _priority(charger: Charger) -> tuple[int, int]:
|
|
1263
|
+
connector_id = charger.connector_id or 0
|
|
1264
|
+
if connector_id == 2:
|
|
1265
|
+
return (0, connector_id)
|
|
1266
|
+
if connector_id == 1:
|
|
1267
|
+
return (1, connector_id)
|
|
1268
|
+
return (2, connector_id)
|
|
1269
|
+
|
|
1270
|
+
def _is_available(charger: Charger) -> bool:
|
|
1271
|
+
existing = type(self).objects.filter(connector=charger).exclude(pk=self.pk)
|
|
1272
|
+
start = self.start_time
|
|
1273
|
+
end = self.end_time
|
|
1274
|
+
for entry in existing:
|
|
1275
|
+
if entry.start_time < end and entry.end_time > start:
|
|
1276
|
+
return False
|
|
1277
|
+
return True
|
|
1278
|
+
|
|
1279
|
+
if self.connector_id:
|
|
1280
|
+
current = next((c for c in candidates if c.pk == self.connector_id), None)
|
|
1281
|
+
if current and _is_available(current) and not force:
|
|
1282
|
+
return current
|
|
1283
|
+
|
|
1284
|
+
for charger in sorted(candidates, key=_priority):
|
|
1285
|
+
if _is_available(charger):
|
|
1286
|
+
self.connector = charger
|
|
1287
|
+
return charger
|
|
1288
|
+
|
|
1289
|
+
raise ValidationError(
|
|
1290
|
+
_("All connectors at this location are reserved for the selected time window.")
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
def clean(self):
|
|
1294
|
+
super().clean()
|
|
1295
|
+
if self.start_time and timezone.is_naive(self.start_time):
|
|
1296
|
+
self.start_time = timezone.make_aware(
|
|
1297
|
+
self.start_time, timezone.get_current_timezone()
|
|
1298
|
+
)
|
|
1299
|
+
if self.duration_minutes <= 0:
|
|
1300
|
+
raise ValidationError(
|
|
1301
|
+
{"duration_minutes": _("Reservation window must be at least one minute.")}
|
|
1302
|
+
)
|
|
1303
|
+
try:
|
|
1304
|
+
self.allocate_connector(force=bool(self.pk))
|
|
1305
|
+
except ValidationError as exc:
|
|
1306
|
+
raise ValidationError(exc) from exc
|
|
1307
|
+
|
|
1308
|
+
def save(self, *args, **kwargs):
|
|
1309
|
+
if self.start_time and timezone.is_naive(self.start_time):
|
|
1310
|
+
self.start_time = timezone.make_aware(
|
|
1311
|
+
self.start_time, timezone.get_current_timezone()
|
|
1312
|
+
)
|
|
1313
|
+
update_fields = kwargs.get("update_fields")
|
|
1314
|
+
relevant_fields = {"location", "start_time", "duration_minutes", "connector"}
|
|
1315
|
+
should_allocate = True
|
|
1316
|
+
if update_fields is not None and not relevant_fields.intersection(update_fields):
|
|
1317
|
+
should_allocate = False
|
|
1318
|
+
if should_allocate:
|
|
1319
|
+
self.allocate_connector(force=bool(self.pk))
|
|
1320
|
+
super().save(*args, **kwargs)
|
|
1321
|
+
|
|
1322
|
+
def send_reservation_request(self) -> str:
|
|
1323
|
+
"""Dispatch a ReserveNow request to the associated connector."""
|
|
1324
|
+
|
|
1325
|
+
if not self.pk:
|
|
1326
|
+
raise ValidationError(_("Save the reservation before sending it to the EVCS."))
|
|
1327
|
+
connector = self.connector
|
|
1328
|
+
if connector is None or connector.connector_id is None:
|
|
1329
|
+
raise ValidationError(_("Unable to determine which connector to reserve."))
|
|
1330
|
+
id_tag = self.id_tag_value
|
|
1331
|
+
if not id_tag:
|
|
1332
|
+
raise ValidationError(
|
|
1333
|
+
_("Provide an RFID or idTag before creating the reservation.")
|
|
1334
|
+
)
|
|
1335
|
+
connection = store.get_connection(connector.charger_id, connector.connector_id)
|
|
1336
|
+
if connection is None:
|
|
1337
|
+
raise ValidationError(
|
|
1338
|
+
_("The selected charge point is not currently connected to the system.")
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1341
|
+
message_id = uuid.uuid4().hex
|
|
1342
|
+
expiry = timezone.localtime(self.end_time)
|
|
1343
|
+
payload = {
|
|
1344
|
+
"connectorId": connector.connector_id,
|
|
1345
|
+
"expiryDate": expiry.isoformat(),
|
|
1346
|
+
"idTag": id_tag,
|
|
1347
|
+
"reservationId": self.pk,
|
|
1348
|
+
}
|
|
1349
|
+
frame = json.dumps([2, message_id, "ReserveNow", payload])
|
|
1350
|
+
|
|
1351
|
+
log_key = store.identity_key(connector.charger_id, connector.connector_id)
|
|
1352
|
+
store.add_log(
|
|
1353
|
+
log_key,
|
|
1354
|
+
f"ReserveNow request: reservation={self.pk}, expiry={expiry.isoformat()}",
|
|
1355
|
+
log_type="charger",
|
|
1356
|
+
)
|
|
1357
|
+
async_to_sync(connection.send)(frame)
|
|
1358
|
+
|
|
1359
|
+
metadata = {
|
|
1360
|
+
"action": "ReserveNow",
|
|
1361
|
+
"charger_id": connector.charger_id,
|
|
1362
|
+
"connector_id": connector.connector_id,
|
|
1363
|
+
"log_key": log_key,
|
|
1364
|
+
"reservation_pk": self.pk,
|
|
1365
|
+
"requested_at": timezone.now(),
|
|
1366
|
+
}
|
|
1367
|
+
store.register_pending_call(message_id, metadata)
|
|
1368
|
+
store.schedule_call_timeout(message_id, action="ReserveNow", log_key=log_key)
|
|
1369
|
+
|
|
1370
|
+
self.ocpp_message_id = message_id
|
|
1371
|
+
self.evcs_status = ""
|
|
1372
|
+
self.evcs_error = ""
|
|
1373
|
+
self.evcs_confirmed = False
|
|
1374
|
+
self.evcs_confirmed_at = None
|
|
1375
|
+
super().save(
|
|
1376
|
+
update_fields=[
|
|
1377
|
+
"ocpp_message_id",
|
|
1378
|
+
"evcs_status",
|
|
1379
|
+
"evcs_error",
|
|
1380
|
+
"evcs_confirmed",
|
|
1381
|
+
"evcs_confirmed_at",
|
|
1382
|
+
"updated_on",
|
|
1383
|
+
]
|
|
1384
|
+
)
|
|
1385
|
+
return message_id
|
|
1386
|
+
|
|
1387
|
+
|
|
1089
1388
|
class RFID(CoreRFID):
|
|
1090
1389
|
class Meta:
|
|
1091
1390
|
proxy = True
|