arthexis 0.1.9__py3-none-any.whl → 0.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- arthexis-0.1.26.dist-info/METADATA +272 -0
- arthexis-0.1.26.dist-info/RECORD +111 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +29 -29
- config/auth_app.py +7 -7
- config/celery.py +32 -25
- config/context_processors.py +67 -68
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +71 -25
- config/offline.py +49 -49
- config/settings.py +676 -492
- config/settings_helpers.py +109 -0
- config/urls.py +228 -159
- config/wsgi.py +17 -17
- core/admin.py +4052 -2066
- core/admin_history.py +50 -50
- core/admindocs.py +192 -151
- core/apps.py +350 -223
- core/auto_upgrade.py +72 -0
- core/backends.py +311 -124
- core/changelog.py +403 -0
- core/entity.py +149 -133
- core/environment.py +60 -43
- core/fields.py +168 -75
- core/form_fields.py +75 -0
- core/github_helper.py +188 -25
- core/github_issues.py +183 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +89 -83
- core/middleware.py +91 -91
- core/models.py +5041 -2195
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +107 -0
- core/release.py +940 -346
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -131
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +250 -284
- core/system.py +1425 -230
- core/tasks.py +538 -199
- core/temp_passwords.py +181 -0
- core/test_system_info.py +202 -43
- core/tests.py +2673 -1069
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +681 -495
- core/views.py +2484 -789
- core/widgets.py +213 -51
- nodes/admin.py +2236 -445
- nodes/apps.py +98 -70
- nodes/backends.py +160 -53
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/lcd.py +165 -165
- nodes/models.py +2375 -870
- nodes/reports.py +411 -0
- nodes/rfid_sync.py +210 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +141 -46
- nodes/tests.py +5045 -1489
- nodes/urls.py +29 -13
- nodes/utils.py +172 -73
- nodes/views.py +1768 -304
- ocpp/admin.py +1775 -481
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1843 -630
- ocpp/evcs.py +844 -928
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +1417 -640
- ocpp/network.py +398 -0
- ocpp/reference_utils.py +42 -0
- ocpp/routing.py +11 -9
- ocpp/simulator.py +745 -368
- ocpp/status_display.py +26 -0
- ocpp/store.py +603 -403
- ocpp/tasks.py +479 -31
- ocpp/test_export_import.py +131 -130
- ocpp/test_rfid.py +1072 -540
- ocpp/tests.py +5494 -2296
- ocpp/transactions_io.py +197 -165
- ocpp/urls.py +50 -50
- ocpp/views.py +2024 -912
- pages/admin.py +1123 -396
- pages/apps.py +45 -10
- pages/checks.py +40 -40
- pages/context_processors.py +151 -85
- pages/defaults.py +13 -0
- pages/forms.py +221 -0
- pages/middleware.py +213 -153
- pages/models.py +720 -252
- pages/module_defaults.py +156 -0
- pages/site_config.py +137 -0
- pages/tasks.py +74 -0
- pages/tests.py +4009 -1389
- pages/urls.py +38 -20
- pages/utils.py +93 -12
- pages/views.py +1736 -762
- arthexis-0.1.9.dist-info/METADATA +0 -168
- arthexis-0.1.9.dist-info/RECORD +0 -92
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- nodes/actions.py +0 -70
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
ocpp/network.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from decimal import Decimal, InvalidOperation
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from django.utils import timezone
|
|
9
|
+
from django.utils.dateparse import parse_datetime
|
|
10
|
+
|
|
11
|
+
from .models import Charger, Location, MeterValue, Transaction
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_remote_datetime(value) -> datetime | None:
|
|
15
|
+
if not value:
|
|
16
|
+
return None
|
|
17
|
+
if isinstance(value, datetime):
|
|
18
|
+
timestamp = value
|
|
19
|
+
else:
|
|
20
|
+
timestamp = parse_datetime(str(value))
|
|
21
|
+
if not timestamp:
|
|
22
|
+
return None
|
|
23
|
+
if timezone.is_naive(timestamp):
|
|
24
|
+
timestamp = timezone.make_aware(timestamp, timezone.get_current_timezone())
|
|
25
|
+
return timestamp
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _to_decimal(value) -> Decimal | None:
|
|
29
|
+
if value in (None, ""):
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
return Decimal(str(value))
|
|
33
|
+
except (InvalidOperation, TypeError, ValueError):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def serialize_charger_for_network(charger: Charger) -> dict[str, object]:
|
|
38
|
+
simple_fields = [
|
|
39
|
+
"display_name",
|
|
40
|
+
"language",
|
|
41
|
+
"public_display",
|
|
42
|
+
"require_rfid",
|
|
43
|
+
"firmware_status",
|
|
44
|
+
"firmware_status_info",
|
|
45
|
+
"last_status",
|
|
46
|
+
"last_error_code",
|
|
47
|
+
"last_status_vendor_info",
|
|
48
|
+
"availability_state",
|
|
49
|
+
"availability_requested_state",
|
|
50
|
+
"availability_request_status",
|
|
51
|
+
"availability_request_details",
|
|
52
|
+
"temperature",
|
|
53
|
+
"temperature_unit",
|
|
54
|
+
"diagnostics_status",
|
|
55
|
+
"diagnostics_location",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
datetime_fields = [
|
|
59
|
+
"firmware_timestamp",
|
|
60
|
+
"last_heartbeat",
|
|
61
|
+
"availability_state_updated_at",
|
|
62
|
+
"availability_requested_at",
|
|
63
|
+
"availability_request_status_at",
|
|
64
|
+
"diagnostics_timestamp",
|
|
65
|
+
"last_status_timestamp",
|
|
66
|
+
"last_online_at",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
data: dict[str, object] = {
|
|
70
|
+
"charger_id": charger.charger_id,
|
|
71
|
+
"connector_id": charger.connector_id,
|
|
72
|
+
"allow_remote": charger.allow_remote,
|
|
73
|
+
"export_transactions": charger.export_transactions,
|
|
74
|
+
"last_meter_values": charger.last_meter_values or {},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for field in simple_fields:
|
|
78
|
+
data[field] = getattr(charger, field)
|
|
79
|
+
|
|
80
|
+
for field in datetime_fields:
|
|
81
|
+
value = getattr(charger, field)
|
|
82
|
+
data[field] = value.isoformat() if value else None
|
|
83
|
+
|
|
84
|
+
if charger.location:
|
|
85
|
+
location = charger.location
|
|
86
|
+
data["location"] = {
|
|
87
|
+
"name": location.name,
|
|
88
|
+
"latitude": location.latitude,
|
|
89
|
+
"longitude": location.longitude,
|
|
90
|
+
"zone": location.zone,
|
|
91
|
+
"contract_type": location.contract_type,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return data
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def apply_remote_charger_payload(
|
|
98
|
+
node,
|
|
99
|
+
payload: Mapping,
|
|
100
|
+
*,
|
|
101
|
+
create_missing: bool = True,
|
|
102
|
+
) -> Charger | None:
|
|
103
|
+
serial = Charger.normalize_serial(payload.get("charger_id"))
|
|
104
|
+
if not serial or Charger.is_placeholder_serial(serial):
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
connector = payload.get("connector_id")
|
|
108
|
+
if connector in (None, ""):
|
|
109
|
+
connector_value = None
|
|
110
|
+
elif isinstance(connector, int):
|
|
111
|
+
connector_value = connector
|
|
112
|
+
else:
|
|
113
|
+
try:
|
|
114
|
+
connector_value = int(str(connector))
|
|
115
|
+
except (TypeError, ValueError):
|
|
116
|
+
connector_value = None
|
|
117
|
+
|
|
118
|
+
charger = Charger.objects.filter(
|
|
119
|
+
charger_id=serial, connector_id=connector_value
|
|
120
|
+
).select_related("location").first()
|
|
121
|
+
|
|
122
|
+
if charger is None and not create_missing:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
if charger is None:
|
|
126
|
+
charger = Charger.objects.create(
|
|
127
|
+
charger_id=serial,
|
|
128
|
+
connector_id=connector_value,
|
|
129
|
+
node_origin=node,
|
|
130
|
+
forwarded_to=None,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
location_obj = None
|
|
134
|
+
location_payload = payload.get("location")
|
|
135
|
+
if isinstance(location_payload, Mapping):
|
|
136
|
+
name = location_payload.get("name")
|
|
137
|
+
if name:
|
|
138
|
+
location_obj, _ = Location.objects.get_or_create(name=name)
|
|
139
|
+
for field in ("latitude", "longitude", "zone", "contract_type"):
|
|
140
|
+
setattr(location_obj, field, location_payload.get(field))
|
|
141
|
+
location_obj.save()
|
|
142
|
+
|
|
143
|
+
datetime_fields = [
|
|
144
|
+
"firmware_timestamp",
|
|
145
|
+
"last_heartbeat",
|
|
146
|
+
"availability_state_updated_at",
|
|
147
|
+
"availability_requested_at",
|
|
148
|
+
"availability_request_status_at",
|
|
149
|
+
"diagnostics_timestamp",
|
|
150
|
+
"last_status_timestamp",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
updates: dict[str, object] = {
|
|
154
|
+
"node_origin": node,
|
|
155
|
+
"allow_remote": bool(payload.get("allow_remote", False)),
|
|
156
|
+
"export_transactions": bool(payload.get("export_transactions", False)),
|
|
157
|
+
"last_online_at": timezone.now(),
|
|
158
|
+
"forwarded_to": None,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
simple_fields = [
|
|
162
|
+
"display_name",
|
|
163
|
+
"language",
|
|
164
|
+
"public_display",
|
|
165
|
+
"require_rfid",
|
|
166
|
+
"firmware_status",
|
|
167
|
+
"firmware_status_info",
|
|
168
|
+
"last_status",
|
|
169
|
+
"last_error_code",
|
|
170
|
+
"last_status_vendor_info",
|
|
171
|
+
"availability_state",
|
|
172
|
+
"availability_requested_state",
|
|
173
|
+
"availability_request_status",
|
|
174
|
+
"availability_request_details",
|
|
175
|
+
"temperature",
|
|
176
|
+
"temperature_unit",
|
|
177
|
+
"diagnostics_status",
|
|
178
|
+
"diagnostics_location",
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
for field in simple_fields:
|
|
182
|
+
if field in payload:
|
|
183
|
+
value = payload.get(field)
|
|
184
|
+
if field in {"require_rfid", "public_display"} and value is None:
|
|
185
|
+
value = False
|
|
186
|
+
updates[field] = value
|
|
187
|
+
else:
|
|
188
|
+
updates[field] = getattr(charger, field)
|
|
189
|
+
|
|
190
|
+
for field in datetime_fields:
|
|
191
|
+
updates[field] = _parse_remote_datetime(payload.get(field))
|
|
192
|
+
|
|
193
|
+
updates["last_meter_values"] = payload.get("last_meter_values") or {}
|
|
194
|
+
|
|
195
|
+
if location_obj is not None:
|
|
196
|
+
updates["location"] = location_obj
|
|
197
|
+
|
|
198
|
+
Charger.objects.filter(pk=charger.pk).update(**updates)
|
|
199
|
+
charger.refresh_from_db()
|
|
200
|
+
return charger
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _normalize_connector(value) -> int | None:
|
|
204
|
+
if value in (None, ""):
|
|
205
|
+
return None
|
|
206
|
+
if isinstance(value, int):
|
|
207
|
+
return value
|
|
208
|
+
try:
|
|
209
|
+
return int(str(value))
|
|
210
|
+
except (TypeError, ValueError):
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def sync_transactions_payload(payload: Mapping) -> int:
|
|
215
|
+
if not isinstance(payload, Mapping):
|
|
216
|
+
return 0
|
|
217
|
+
|
|
218
|
+
chargers_map: dict[tuple[str, int | None], Charger] = {}
|
|
219
|
+
charger_entries = payload.get("chargers", [])
|
|
220
|
+
if isinstance(charger_entries, Iterable):
|
|
221
|
+
for entry in charger_entries:
|
|
222
|
+
if not isinstance(entry, Mapping):
|
|
223
|
+
continue
|
|
224
|
+
serial = Charger.normalize_serial(entry.get("charger_id"))
|
|
225
|
+
if not serial or Charger.is_placeholder_serial(serial):
|
|
226
|
+
continue
|
|
227
|
+
connector_value = _normalize_connector(entry.get("connector_id"))
|
|
228
|
+
charger = Charger.objects.filter(
|
|
229
|
+
charger_id=serial, connector_id=connector_value
|
|
230
|
+
).first()
|
|
231
|
+
if charger:
|
|
232
|
+
chargers_map[(serial, connector_value)] = charger
|
|
233
|
+
|
|
234
|
+
imported = 0
|
|
235
|
+
transaction_entries = payload.get("transactions", [])
|
|
236
|
+
if not isinstance(transaction_entries, Iterable):
|
|
237
|
+
return 0
|
|
238
|
+
|
|
239
|
+
for tx in transaction_entries:
|
|
240
|
+
if not isinstance(tx, Mapping):
|
|
241
|
+
continue
|
|
242
|
+
serial = Charger.normalize_serial(tx.get("charger"))
|
|
243
|
+
if not serial:
|
|
244
|
+
continue
|
|
245
|
+
connector_value = _normalize_connector(tx.get("connector_id"))
|
|
246
|
+
|
|
247
|
+
charger = chargers_map.get((serial, connector_value))
|
|
248
|
+
if charger is None:
|
|
249
|
+
charger = chargers_map.get((serial, None))
|
|
250
|
+
if charger is None:
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
start_time = _parse_remote_datetime(tx.get("start_time"))
|
|
254
|
+
if start_time is None:
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
defaults = {
|
|
258
|
+
"connector_id": connector_value,
|
|
259
|
+
"account_id": tx.get("account"),
|
|
260
|
+
"rfid": tx.get("rfid", ""),
|
|
261
|
+
"vid": tx.get("vid", ""),
|
|
262
|
+
"vin": tx.get("vin", ""),
|
|
263
|
+
"meter_start": tx.get("meter_start"),
|
|
264
|
+
"meter_stop": tx.get("meter_stop"),
|
|
265
|
+
"voltage_start": _to_decimal(tx.get("voltage_start")),
|
|
266
|
+
"voltage_stop": _to_decimal(tx.get("voltage_stop")),
|
|
267
|
+
"current_import_start": _to_decimal(tx.get("current_import_start")),
|
|
268
|
+
"current_import_stop": _to_decimal(tx.get("current_import_stop")),
|
|
269
|
+
"current_offered_start": _to_decimal(tx.get("current_offered_start")),
|
|
270
|
+
"current_offered_stop": _to_decimal(tx.get("current_offered_stop")),
|
|
271
|
+
"temperature_start": _to_decimal(tx.get("temperature_start")),
|
|
272
|
+
"temperature_stop": _to_decimal(tx.get("temperature_stop")),
|
|
273
|
+
"soc_start": _to_decimal(tx.get("soc_start")),
|
|
274
|
+
"soc_stop": _to_decimal(tx.get("soc_stop")),
|
|
275
|
+
"stop_time": _parse_remote_datetime(tx.get("stop_time")),
|
|
276
|
+
"received_start_time": _parse_remote_datetime(tx.get("received_start_time")),
|
|
277
|
+
"received_stop_time": _parse_remote_datetime(tx.get("received_stop_time")),
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
transaction, created = Transaction.objects.update_or_create(
|
|
281
|
+
charger=charger,
|
|
282
|
+
start_time=start_time,
|
|
283
|
+
defaults=defaults,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if not created:
|
|
287
|
+
transaction.meter_values.all().delete()
|
|
288
|
+
|
|
289
|
+
meter_values = tx.get("meter_values", [])
|
|
290
|
+
if isinstance(meter_values, Iterable):
|
|
291
|
+
for mv in meter_values:
|
|
292
|
+
if not isinstance(mv, Mapping):
|
|
293
|
+
continue
|
|
294
|
+
timestamp = _parse_remote_datetime(mv.get("timestamp"))
|
|
295
|
+
if timestamp is None:
|
|
296
|
+
continue
|
|
297
|
+
connector_mv = _normalize_connector(mv.get("connector_id"))
|
|
298
|
+
MeterValue.objects.create(
|
|
299
|
+
charger=charger,
|
|
300
|
+
transaction=transaction,
|
|
301
|
+
connector_id=connector_mv,
|
|
302
|
+
timestamp=timestamp,
|
|
303
|
+
context=mv.get("context", ""),
|
|
304
|
+
energy=_to_decimal(mv.get("energy")),
|
|
305
|
+
voltage=_to_decimal(mv.get("voltage")),
|
|
306
|
+
current_import=_to_decimal(mv.get("current_import")),
|
|
307
|
+
current_offered=_to_decimal(mv.get("current_offered")),
|
|
308
|
+
temperature=_to_decimal(mv.get("temperature")),
|
|
309
|
+
soc=_to_decimal(mv.get("soc")),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
imported += 1
|
|
313
|
+
|
|
314
|
+
return imported
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def serialize_transactions_for_forwarding(
|
|
318
|
+
transactions: Iterable[Transaction],
|
|
319
|
+
) -> list[dict[str, object]]:
|
|
320
|
+
serialized: list[dict[str, object]] = []
|
|
321
|
+
for tx in transactions:
|
|
322
|
+
serialized.append(
|
|
323
|
+
{
|
|
324
|
+
"charger": tx.charger.charger_id if tx.charger else None,
|
|
325
|
+
"connector_id": tx.connector_id,
|
|
326
|
+
"account": tx.account_id,
|
|
327
|
+
"rfid": tx.rfid,
|
|
328
|
+
"vid": tx.vehicle_identifier,
|
|
329
|
+
"vin": tx.vin,
|
|
330
|
+
"meter_start": tx.meter_start,
|
|
331
|
+
"meter_stop": tx.meter_stop,
|
|
332
|
+
"voltage_start": str(tx.voltage_start)
|
|
333
|
+
if tx.voltage_start is not None
|
|
334
|
+
else None,
|
|
335
|
+
"voltage_stop": str(tx.voltage_stop)
|
|
336
|
+
if tx.voltage_stop is not None
|
|
337
|
+
else None,
|
|
338
|
+
"current_import_start": str(tx.current_import_start)
|
|
339
|
+
if tx.current_import_start is not None
|
|
340
|
+
else None,
|
|
341
|
+
"current_import_stop": str(tx.current_import_stop)
|
|
342
|
+
if tx.current_import_stop is not None
|
|
343
|
+
else None,
|
|
344
|
+
"current_offered_start": str(tx.current_offered_start)
|
|
345
|
+
if tx.current_offered_start is not None
|
|
346
|
+
else None,
|
|
347
|
+
"current_offered_stop": str(tx.current_offered_stop)
|
|
348
|
+
if tx.current_offered_stop is not None
|
|
349
|
+
else None,
|
|
350
|
+
"temperature_start": str(tx.temperature_start)
|
|
351
|
+
if tx.temperature_start is not None
|
|
352
|
+
else None,
|
|
353
|
+
"temperature_stop": str(tx.temperature_stop)
|
|
354
|
+
if tx.temperature_stop is not None
|
|
355
|
+
else None,
|
|
356
|
+
"soc_start": str(tx.soc_start) if tx.soc_start is not None else None,
|
|
357
|
+
"soc_stop": str(tx.soc_stop) if tx.soc_stop is not None else None,
|
|
358
|
+
"start_time": tx.start_time.isoformat() if tx.start_time else None,
|
|
359
|
+
"stop_time": tx.stop_time.isoformat() if tx.stop_time else None,
|
|
360
|
+
"received_start_time": tx.received_start_time.isoformat()
|
|
361
|
+
if tx.received_start_time
|
|
362
|
+
else None,
|
|
363
|
+
"received_stop_time": tx.received_stop_time.isoformat()
|
|
364
|
+
if tx.received_stop_time
|
|
365
|
+
else None,
|
|
366
|
+
"meter_values": [
|
|
367
|
+
{
|
|
368
|
+
"connector_id": mv.connector_id,
|
|
369
|
+
"timestamp": mv.timestamp.isoformat(),
|
|
370
|
+
"context": mv.context,
|
|
371
|
+
"energy": str(mv.energy) if mv.energy is not None else None,
|
|
372
|
+
"voltage": str(mv.voltage) if mv.voltage is not None else None,
|
|
373
|
+
"current_import": str(mv.current_import)
|
|
374
|
+
if mv.current_import is not None
|
|
375
|
+
else None,
|
|
376
|
+
"current_offered": str(mv.current_offered)
|
|
377
|
+
if mv.current_offered is not None
|
|
378
|
+
else None,
|
|
379
|
+
"temperature": str(mv.temperature)
|
|
380
|
+
if mv.temperature is not None
|
|
381
|
+
else None,
|
|
382
|
+
"soc": str(mv.soc) if mv.soc is not None else None,
|
|
383
|
+
}
|
|
384
|
+
for mv in tx.meter_values.all()
|
|
385
|
+
],
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
return serialized
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def newest_transaction_timestamp(transactions: Iterable[Transaction]) -> datetime | None:
|
|
392
|
+
latest: datetime | None = None
|
|
393
|
+
for tx in transactions:
|
|
394
|
+
if tx.start_time and (
|
|
395
|
+
latest is None or tx.start_time > latest
|
|
396
|
+
):
|
|
397
|
+
latest = tx.start_time
|
|
398
|
+
return latest
|
ocpp/reference_utils.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Helpers related to console Reference creation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _normalize_host(host: str | None) -> str:
|
|
10
|
+
"""Return a trimmed host string without surrounding brackets."""
|
|
11
|
+
|
|
12
|
+
if not host:
|
|
13
|
+
return ""
|
|
14
|
+
host = host.strip()
|
|
15
|
+
if host.startswith("[") and host.endswith("]"):
|
|
16
|
+
return host[1:-1]
|
|
17
|
+
return host
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def host_is_local_loopback(host: str | None) -> bool:
|
|
21
|
+
"""Return ``True`` when the host string points to 127.0.0.1."""
|
|
22
|
+
|
|
23
|
+
normalized = _normalize_host(host)
|
|
24
|
+
if not normalized:
|
|
25
|
+
return False
|
|
26
|
+
try:
|
|
27
|
+
return ipaddress.ip_address(normalized) == ipaddress.ip_address("127.0.0.1")
|
|
28
|
+
except ValueError:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def url_targets_local_loopback(url: str | None) -> bool:
|
|
33
|
+
"""Return ``True`` when the parsed URL host equals 127.0.0.1."""
|
|
34
|
+
|
|
35
|
+
if not url:
|
|
36
|
+
return False
|
|
37
|
+
parsed = urlparse(url)
|
|
38
|
+
return host_is_local_loopback(parsed.hostname)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ["host_is_local_loopback", "url_targets_local_loopback"]
|
|
42
|
+
|
ocpp/routing.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
from django.urls import re_path
|
|
2
|
-
|
|
3
|
-
from . import consumers
|
|
4
|
-
|
|
5
|
-
websocket_urlpatterns = [
|
|
6
|
-
re_path(r"^ws/sink/$", consumers.SinkConsumer.as_asgi()),
|
|
7
|
-
# Accept connections at any path; the last segment is the charger ID
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
from django.urls import re_path
|
|
2
|
+
|
|
3
|
+
from . import consumers
|
|
4
|
+
|
|
5
|
+
websocket_urlpatterns = [
|
|
6
|
+
re_path(r"^ws/sink/$", consumers.SinkConsumer.as_asgi()),
|
|
7
|
+
# Accept connections at any path; the last segment is the charger ID.
|
|
8
|
+
# Some charge points omit the final segment and only provide the
|
|
9
|
+
# identifier via query parameters, so allow an empty match here.
|
|
10
|
+
re_path(r"^(?:.*/)?(?P<cid>[^/]*)/?$", consumers.CSMSConsumer.as_asgi()),
|
|
11
|
+
]
|