arthexis 0.1.16__py3-none-any.whl → 0.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/METADATA +84 -35
- arthexis-0.1.26.dist-info/RECORD +111 -0
- config/asgi.py +1 -15
- config/middleware.py +47 -1
- config/settings.py +15 -30
- config/urls.py +53 -1
- core/admin.py +540 -450
- core/apps.py +0 -6
- core/auto_upgrade.py +19 -4
- core/backends.py +13 -3
- core/changelog.py +66 -5
- core/environment.py +4 -5
- core/models.py +1566 -203
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/release.py +55 -7
- core/sigil_builder.py +2 -2
- core/sigil_resolver.py +1 -66
- core/system.py +268 -2
- core/tasks.py +174 -48
- core/tests.py +314 -16
- core/user_data.py +42 -2
- core/views.py +278 -183
- nodes/admin.py +557 -65
- nodes/apps.py +11 -0
- nodes/models.py +658 -113
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +97 -2
- nodes/tests.py +1212 -116
- nodes/urls.py +15 -1
- nodes/utils.py +51 -3
- nodes/views.py +1239 -154
- ocpp/admin.py +979 -152
- ocpp/consumers.py +268 -28
- ocpp/models.py +488 -3
- ocpp/network.py +398 -0
- ocpp/store.py +6 -4
- ocpp/tasks.py +296 -2
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +121 -4
- ocpp/tests.py +950 -11
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +596 -51
- pages/admin.py +262 -30
- pages/apps.py +35 -0
- pages/context_processors.py +26 -21
- pages/defaults.py +1 -1
- pages/forms.py +31 -8
- pages/middleware.py +6 -2
- pages/models.py +77 -2
- pages/module_defaults.py +5 -5
- pages/site_config.py +137 -0
- pages/tests.py +885 -109
- pages/urls.py +13 -2
- pages/utils.py +70 -0
- pages/views.py +558 -55
- arthexis-0.1.16.dist-info/RECORD +0 -111
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
ocpp/tasks.py
CHANGED
|
@@ -1,21 +1,143 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
1
3
|
import logging
|
|
4
|
+
import uuid
|
|
2
5
|
from datetime import date, datetime, time, timedelta
|
|
3
6
|
from pathlib import Path
|
|
4
7
|
|
|
8
|
+
from asgiref.sync import async_to_sync
|
|
5
9
|
from celery import shared_task
|
|
6
10
|
from django.conf import settings
|
|
7
11
|
from django.contrib.auth import get_user_model
|
|
8
|
-
from django.utils import timezone
|
|
9
12
|
from django.db.models import Q
|
|
13
|
+
from django.utils import timezone
|
|
14
|
+
import requests
|
|
15
|
+
from requests import RequestException
|
|
16
|
+
from cryptography.hazmat.primitives import hashes
|
|
17
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
10
18
|
|
|
11
19
|
from core import mailer
|
|
12
20
|
from nodes.models import Node
|
|
13
21
|
|
|
14
|
-
from .
|
|
22
|
+
from . import store
|
|
23
|
+
from .models import Charger, MeterValue, Transaction
|
|
24
|
+
from .network import (
|
|
25
|
+
newest_transaction_timestamp,
|
|
26
|
+
serialize_charger_for_network,
|
|
27
|
+
serialize_transactions_for_forwarding,
|
|
28
|
+
)
|
|
15
29
|
|
|
16
30
|
logger = logging.getLogger(__name__)
|
|
17
31
|
|
|
18
32
|
|
|
33
|
+
def _sign_payload(payload_json: str, private_key) -> str | None:
|
|
34
|
+
if not private_key:
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
signature = private_key.sign(
|
|
38
|
+
payload_json.encode(),
|
|
39
|
+
padding.PKCS1v15(),
|
|
40
|
+
hashes.SHA256(),
|
|
41
|
+
)
|
|
42
|
+
except Exception:
|
|
43
|
+
return None
|
|
44
|
+
return base64.b64encode(signature).decode()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@shared_task
|
|
48
|
+
def check_charge_point_configuration(charger_pk: int) -> bool:
|
|
49
|
+
"""Request the latest configuration from a connected charge point."""
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
charger = Charger.objects.get(pk=charger_pk)
|
|
53
|
+
except Charger.DoesNotExist:
|
|
54
|
+
logger.warning(
|
|
55
|
+
"Unable to request configuration for missing charger %s",
|
|
56
|
+
charger_pk,
|
|
57
|
+
)
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
connector_value = charger.connector_id
|
|
61
|
+
if connector_value is not None:
|
|
62
|
+
logger.debug(
|
|
63
|
+
"Skipping charger %s: connector %s is not eligible for automatic configuration checks",
|
|
64
|
+
charger.charger_id,
|
|
65
|
+
connector_value,
|
|
66
|
+
)
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
ws = store.get_connection(charger.charger_id, connector_value)
|
|
70
|
+
if ws is None:
|
|
71
|
+
logger.info(
|
|
72
|
+
"Charge point %s is not connected; configuration request skipped",
|
|
73
|
+
charger.charger_id,
|
|
74
|
+
)
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
message_id = uuid.uuid4().hex
|
|
78
|
+
payload: dict[str, object] = {}
|
|
79
|
+
msg = json.dumps([2, message_id, "GetConfiguration", payload])
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
async_to_sync(ws.send)(msg)
|
|
83
|
+
except Exception as exc: # pragma: no cover - network error
|
|
84
|
+
logger.warning(
|
|
85
|
+
"Failed to send GetConfiguration to %s (%s)",
|
|
86
|
+
charger.charger_id,
|
|
87
|
+
exc,
|
|
88
|
+
)
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
log_key = store.identity_key(charger.charger_id, connector_value)
|
|
92
|
+
store.add_log(log_key, f"< {msg}", log_type="charger")
|
|
93
|
+
store.register_pending_call(
|
|
94
|
+
message_id,
|
|
95
|
+
{
|
|
96
|
+
"action": "GetConfiguration",
|
|
97
|
+
"charger_id": charger.charger_id,
|
|
98
|
+
"connector_id": connector_value,
|
|
99
|
+
"log_key": log_key,
|
|
100
|
+
"requested_at": timezone.now(),
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
store.schedule_call_timeout(
|
|
104
|
+
message_id,
|
|
105
|
+
timeout=5.0,
|
|
106
|
+
action="GetConfiguration",
|
|
107
|
+
log_key=log_key,
|
|
108
|
+
message=(
|
|
109
|
+
"GetConfiguration timed out: charger did not respond"
|
|
110
|
+
" (operation may not be supported)"
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
logger.info(
|
|
114
|
+
"Requested configuration from charge point %s",
|
|
115
|
+
charger.charger_id,
|
|
116
|
+
)
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@shared_task
|
|
121
|
+
def schedule_daily_charge_point_configuration_checks() -> int:
|
|
122
|
+
"""Dispatch configuration requests for eligible charge points."""
|
|
123
|
+
|
|
124
|
+
charger_ids = list(
|
|
125
|
+
Charger.objects.filter(connector_id__isnull=True).values_list("pk", flat=True)
|
|
126
|
+
)
|
|
127
|
+
if not charger_ids:
|
|
128
|
+
logger.debug("No eligible charge points available for configuration check")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
scheduled = 0
|
|
132
|
+
for charger_pk in charger_ids:
|
|
133
|
+
check_charge_point_configuration.delay(charger_pk)
|
|
134
|
+
scheduled += 1
|
|
135
|
+
logger.info(
|
|
136
|
+
"Scheduled configuration checks for %s charge point(s)", scheduled
|
|
137
|
+
)
|
|
138
|
+
return scheduled
|
|
139
|
+
|
|
140
|
+
|
|
19
141
|
@shared_task
|
|
20
142
|
def purge_meter_values() -> int:
|
|
21
143
|
"""Delete meter values older than 7 days.
|
|
@@ -37,6 +159,174 @@ def purge_meter_values() -> int:
|
|
|
37
159
|
purge_meter_readings = purge_meter_values
|
|
38
160
|
|
|
39
161
|
|
|
162
|
+
@shared_task
|
|
163
|
+
def push_forwarded_charge_points() -> int:
|
|
164
|
+
"""Push local charge point sessions to configured upstream nodes."""
|
|
165
|
+
|
|
166
|
+
local = Node.get_local()
|
|
167
|
+
if not local:
|
|
168
|
+
logger.debug("Forwarding skipped: local node not registered")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
private_key = local.get_private_key()
|
|
172
|
+
if private_key is None:
|
|
173
|
+
logger.warning("Forwarding skipped: missing local node private key")
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
chargers_qs = (
|
|
177
|
+
Charger.objects.filter(export_transactions=True, forwarded_to__isnull=False)
|
|
178
|
+
.select_related("forwarded_to", "node_origin")
|
|
179
|
+
.order_by("pk")
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
node_filter = Q(node_origin__isnull=True)
|
|
183
|
+
if local.pk:
|
|
184
|
+
node_filter |= Q(node_origin=local)
|
|
185
|
+
|
|
186
|
+
chargers = list(chargers_qs.filter(node_filter))
|
|
187
|
+
if not chargers:
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
grouped: dict[Node, list[Charger]] = {}
|
|
191
|
+
for charger in chargers:
|
|
192
|
+
target = charger.forwarded_to
|
|
193
|
+
if not target:
|
|
194
|
+
continue
|
|
195
|
+
if local.pk and target.pk == local.pk:
|
|
196
|
+
continue
|
|
197
|
+
grouped.setdefault(target, []).append(charger)
|
|
198
|
+
|
|
199
|
+
if not grouped:
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
forwarded_total = 0
|
|
203
|
+
|
|
204
|
+
for node, node_chargers in grouped.items():
|
|
205
|
+
if not node_chargers:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
initializing = [ch for ch in node_chargers if ch.forwarding_watermark is None]
|
|
209
|
+
charger_by_pk = {ch.pk: ch for ch in node_chargers}
|
|
210
|
+
transactions_map: dict[int, list[Transaction]] = {}
|
|
211
|
+
|
|
212
|
+
for charger in node_chargers:
|
|
213
|
+
watermark = charger.forwarding_watermark
|
|
214
|
+
if watermark is None:
|
|
215
|
+
continue
|
|
216
|
+
tx_queryset = (
|
|
217
|
+
Transaction.objects.filter(charger=charger, start_time__gt=watermark)
|
|
218
|
+
.select_related("charger")
|
|
219
|
+
.prefetch_related("meter_values")
|
|
220
|
+
.order_by("start_time")
|
|
221
|
+
)
|
|
222
|
+
txs = list(tx_queryset)
|
|
223
|
+
if txs:
|
|
224
|
+
transactions_map[charger.pk] = txs
|
|
225
|
+
|
|
226
|
+
transaction_payload = {"chargers": [], "transactions": []}
|
|
227
|
+
for charger_pk, txs in transactions_map.items():
|
|
228
|
+
charger = charger_by_pk[charger_pk]
|
|
229
|
+
transaction_payload["chargers"].append(
|
|
230
|
+
{
|
|
231
|
+
"charger_id": charger.charger_id,
|
|
232
|
+
"connector_id": charger.connector_id,
|
|
233
|
+
"require_rfid": charger.require_rfid,
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
transaction_payload["transactions"].extend(
|
|
237
|
+
serialize_transactions_for_forwarding(txs)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
payload = {
|
|
241
|
+
"requester": str(local.uuid),
|
|
242
|
+
"requester_mac": local.mac_address,
|
|
243
|
+
"requester_public_key": local.public_key,
|
|
244
|
+
"chargers": [serialize_charger_for_network(ch) for ch in initializing],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
has_transactions = bool(transaction_payload["transactions"])
|
|
248
|
+
if has_transactions or payload["chargers"]:
|
|
249
|
+
payload["transactions"] = transaction_payload
|
|
250
|
+
else:
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
payload_json = json.dumps(payload, separators=(",", ":"), sort_keys=True)
|
|
254
|
+
signature = _sign_payload(payload_json, private_key)
|
|
255
|
+
headers = {"Content-Type": "application/json"}
|
|
256
|
+
if signature:
|
|
257
|
+
headers["X-Signature"] = signature
|
|
258
|
+
|
|
259
|
+
success = False
|
|
260
|
+
attempted = False
|
|
261
|
+
for url in node.iter_remote_urls("/nodes/network/chargers/forward/"):
|
|
262
|
+
if not url:
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
attempted = True
|
|
266
|
+
try:
|
|
267
|
+
response = requests.post(
|
|
268
|
+
url, data=payload_json, headers=headers, timeout=5
|
|
269
|
+
)
|
|
270
|
+
except RequestException as exc:
|
|
271
|
+
logger.warning("Failed to forward chargers to %s: %s", node, exc)
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
if not response.ok:
|
|
275
|
+
logger.warning(
|
|
276
|
+
"Forwarding request to %s via %s returned %s",
|
|
277
|
+
node,
|
|
278
|
+
url,
|
|
279
|
+
response.status_code,
|
|
280
|
+
)
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
data = response.json()
|
|
285
|
+
except ValueError:
|
|
286
|
+
logger.warning("Invalid JSON payload received from %s", node)
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
if data.get("status") != "ok":
|
|
290
|
+
detail = data.get("detail") if isinstance(data, dict) else None
|
|
291
|
+
logger.warning(
|
|
292
|
+
"Forwarding rejected by %s via %s: %s",
|
|
293
|
+
node,
|
|
294
|
+
url,
|
|
295
|
+
detail or response.text or "Remote node rejected the request.",
|
|
296
|
+
)
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
success = True
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
if not success:
|
|
303
|
+
if not attempted:
|
|
304
|
+
logger.warning(
|
|
305
|
+
"No reachable host found for %s when forwarding chargers", node
|
|
306
|
+
)
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
updates: dict[int, datetime] = {}
|
|
310
|
+
now = timezone.now()
|
|
311
|
+
for charger in initializing:
|
|
312
|
+
updates[charger.pk] = now
|
|
313
|
+
for charger_pk, txs in transactions_map.items():
|
|
314
|
+
latest = newest_transaction_timestamp(txs)
|
|
315
|
+
if latest:
|
|
316
|
+
updates[charger_pk] = latest
|
|
317
|
+
|
|
318
|
+
for pk, timestamp in updates.items():
|
|
319
|
+
Charger.objects.filter(pk=pk).update(forwarding_watermark=timestamp)
|
|
320
|
+
|
|
321
|
+
forwarded_total += len(transaction_payload["transactions"])
|
|
322
|
+
|
|
323
|
+
return forwarded_total
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# Backwards compatibility alias for legacy schedules
|
|
327
|
+
sync_remote_chargers = push_forwarded_charge_points
|
|
328
|
+
|
|
329
|
+
|
|
40
330
|
def _resolve_report_window() -> tuple[datetime, datetime, date]:
|
|
41
331
|
"""Return the start/end datetimes for today's reporting window."""
|
|
42
332
|
|
|
@@ -149,6 +439,10 @@ def send_daily_session_report() -> int:
|
|
|
149
439
|
lines.append(f" Account: {account}")
|
|
150
440
|
if transaction.rfid:
|
|
151
441
|
lines.append(f" RFID: {transaction.rfid}")
|
|
442
|
+
identifier = transaction.vehicle_identifier
|
|
443
|
+
if identifier:
|
|
444
|
+
label = "VID" if transaction.vehicle_identifier_source == "vid" else "VIN"
|
|
445
|
+
lines.append(f" {label}: {identifier}")
|
|
152
446
|
if connector:
|
|
153
447
|
lines.append(f" {connector}")
|
|
154
448
|
lines.append(
|
ocpp/test_export_import.py
CHANGED
ocpp/test_rfid.py
CHANGED
|
@@ -131,7 +131,7 @@ class ScanNextViewTests(TestCase):
|
|
|
131
131
|
self.assertEqual(
|
|
132
132
|
resp.json(), {"rfid": "ABCD1234", "label_id": 1, "created": False}
|
|
133
133
|
)
|
|
134
|
-
mock_validate.assert_called_once_with("ABCD1234", kind=None)
|
|
134
|
+
mock_validate.assert_called_once_with("ABCD1234", kind=None, endianness=None)
|
|
135
135
|
|
|
136
136
|
@patch("config.middleware.Node.get_local", return_value=None)
|
|
137
137
|
@patch("config.middleware.get_site")
|
|
@@ -342,16 +342,20 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
342
342
|
tag.released = False
|
|
343
343
|
tag.reference = None
|
|
344
344
|
tag.kind = RFID.CLASSIC
|
|
345
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
345
346
|
mock_register.return_value = (tag, True)
|
|
346
347
|
|
|
347
348
|
result = validate_rfid_value("abcd1234")
|
|
348
349
|
|
|
349
|
-
mock_register.assert_called_once_with(
|
|
350
|
+
mock_register.assert_called_once_with(
|
|
351
|
+
"ABCD1234", kind=None, endianness=RFID.BIG_ENDIAN
|
|
352
|
+
)
|
|
350
353
|
tag.save.assert_called_once_with(update_fields=["last_seen_on"])
|
|
351
354
|
self.assertIs(tag.last_seen_on, fake_now)
|
|
352
355
|
mock_notify.assert_called_once_with("RFID 1 OK", "ABCD1234 B")
|
|
353
356
|
self.assertTrue(result["created"])
|
|
354
357
|
self.assertEqual(result["rfid"], "ABCD1234")
|
|
358
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
355
359
|
|
|
356
360
|
@patch("ocpp.rfid.reader.timezone.now")
|
|
357
361
|
@patch("ocpp.rfid.reader.notify_async")
|
|
@@ -367,11 +371,14 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
367
371
|
tag.released = True
|
|
368
372
|
tag.reference = None
|
|
369
373
|
tag.kind = RFID.CLASSIC
|
|
374
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
370
375
|
mock_register.return_value = (tag, False)
|
|
371
376
|
|
|
372
377
|
result = validate_rfid_value("abcd", kind=RFID.NTAG215)
|
|
373
378
|
|
|
374
|
-
mock_register.assert_called_once_with(
|
|
379
|
+
mock_register.assert_called_once_with(
|
|
380
|
+
"ABCD", kind=RFID.NTAG215, endianness=RFID.BIG_ENDIAN
|
|
381
|
+
)
|
|
375
382
|
tag.save.assert_called_once_with(update_fields=["kind", "last_seen_on"])
|
|
376
383
|
self.assertIs(tag.last_seen_on, fake_now)
|
|
377
384
|
self.assertEqual(tag.kind, RFID.NTAG215)
|
|
@@ -379,6 +386,36 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
379
386
|
self.assertFalse(result["allowed"])
|
|
380
387
|
self.assertFalse(result["created"])
|
|
381
388
|
self.assertEqual(result["kind"], RFID.NTAG215)
|
|
389
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
390
|
+
|
|
391
|
+
@patch("ocpp.rfid.reader.timezone.now")
|
|
392
|
+
@patch("ocpp.rfid.reader.notify_async")
|
|
393
|
+
@patch("ocpp.rfid.reader.RFID.register_scan")
|
|
394
|
+
def test_registers_little_endian_value(
|
|
395
|
+
self, mock_register, mock_notify, mock_now
|
|
396
|
+
):
|
|
397
|
+
fake_now = object()
|
|
398
|
+
mock_now.return_value = fake_now
|
|
399
|
+
tag = MagicMock()
|
|
400
|
+
tag.pk = 7
|
|
401
|
+
tag.label_id = 7
|
|
402
|
+
tag.allowed = True
|
|
403
|
+
tag.color = "B"
|
|
404
|
+
tag.released = False
|
|
405
|
+
tag.reference = None
|
|
406
|
+
tag.kind = RFID.CLASSIC
|
|
407
|
+
tag.endianness = RFID.LITTLE_ENDIAN
|
|
408
|
+
mock_register.return_value = (tag, True)
|
|
409
|
+
|
|
410
|
+
result = validate_rfid_value("A1B2C3D4", endianness=RFID.LITTLE_ENDIAN)
|
|
411
|
+
|
|
412
|
+
mock_register.assert_called_once_with(
|
|
413
|
+
"D4C3B2A1", kind=None, endianness=RFID.LITTLE_ENDIAN
|
|
414
|
+
)
|
|
415
|
+
tag.save.assert_called_once_with(update_fields=["last_seen_on"])
|
|
416
|
+
self.assertEqual(result["rfid"], "D4C3B2A1")
|
|
417
|
+
self.assertEqual(result["endianness"], RFID.LITTLE_ENDIAN)
|
|
418
|
+
mock_notify.assert_called_once()
|
|
382
419
|
|
|
383
420
|
def test_rejects_invalid_value(self):
|
|
384
421
|
result = validate_rfid_value("invalid!")
|
|
@@ -412,6 +449,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
412
449
|
tag.released = False
|
|
413
450
|
tag.reference = None
|
|
414
451
|
tag.kind = RFID.CLASSIC
|
|
452
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
415
453
|
mock_register.return_value = (tag, False)
|
|
416
454
|
mock_run.return_value = types.SimpleNamespace(
|
|
417
455
|
returncode=0, stdout="ok\n", stderr=""
|
|
@@ -427,6 +465,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
427
465
|
env = run_kwargs.get("env", {})
|
|
428
466
|
self.assertEqual(env.get("RFID_VALUE"), "ABCD1234")
|
|
429
467
|
self.assertEqual(env.get("RFID_LABEL_ID"), "1")
|
|
468
|
+
self.assertEqual(env.get("RFID_ENDIANNESS"), RFID.BIG_ENDIAN)
|
|
430
469
|
mock_popen.assert_not_called()
|
|
431
470
|
mock_notify.assert_called_once_with("RFID 1 OK", "ABCD1234 B")
|
|
432
471
|
tag.save.assert_called_once_with(update_fields=["last_seen_on"])
|
|
@@ -437,6 +476,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
437
476
|
self.assertEqual(output.get("stderr"), "")
|
|
438
477
|
self.assertEqual(output.get("returncode"), 0)
|
|
439
478
|
self.assertEqual(output.get("error"), "")
|
|
479
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
440
480
|
|
|
441
481
|
@patch("ocpp.rfid.reader.timezone.now")
|
|
442
482
|
@patch("ocpp.rfid.reader.notify_async")
|
|
@@ -457,6 +497,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
457
497
|
tag.released = False
|
|
458
498
|
tag.reference = None
|
|
459
499
|
tag.kind = RFID.CLASSIC
|
|
500
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
460
501
|
mock_register.return_value = (tag, False)
|
|
461
502
|
mock_run.return_value = types.SimpleNamespace(
|
|
462
503
|
returncode=1, stdout="", stderr="failure"
|
|
@@ -476,6 +517,77 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
476
517
|
self.assertEqual(output.get("stderr"), "failure")
|
|
477
518
|
self.assertEqual(output.get("error"), "")
|
|
478
519
|
mock_popen.assert_not_called()
|
|
520
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
521
|
+
|
|
522
|
+
@patch("ocpp.rfid.reader.timezone.now")
|
|
523
|
+
@patch("ocpp.rfid.reader.notify_async")
|
|
524
|
+
@patch("ocpp.rfid.reader.subprocess.Popen")
|
|
525
|
+
@patch("ocpp.rfid.reader.subprocess.run")
|
|
526
|
+
@patch("ocpp.rfid.reader.RFID.register_scan")
|
|
527
|
+
def test_external_command_strips_trailing_percent_tokens(
|
|
528
|
+
self, mock_register, mock_run, mock_popen, mock_notify, mock_now
|
|
529
|
+
):
|
|
530
|
+
mock_now.return_value = timezone.now()
|
|
531
|
+
tag = MagicMock()
|
|
532
|
+
tag.pk = 3
|
|
533
|
+
tag.label_id = 3
|
|
534
|
+
tag.allowed = True
|
|
535
|
+
tag.external_command = "echo weird"
|
|
536
|
+
tag.color = "Y"
|
|
537
|
+
tag.released = False
|
|
538
|
+
tag.reference = None
|
|
539
|
+
tag.kind = RFID.CLASSIC
|
|
540
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
541
|
+
mock_register.return_value = (tag, False)
|
|
542
|
+
mock_run.return_value = types.SimpleNamespace(
|
|
543
|
+
returncode=0,
|
|
544
|
+
stdout="first %\nsecond 50%\r\nthird % %\n",
|
|
545
|
+
stderr="oops %\n",
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
result = validate_rfid_value("abc3")
|
|
549
|
+
|
|
550
|
+
output = result.get("command_output")
|
|
551
|
+
self.assertIsNotNone(output)
|
|
552
|
+
self.assertEqual(
|
|
553
|
+
output.get("stdout"), "first\nsecond 50%\r\nthird\n"
|
|
554
|
+
)
|
|
555
|
+
self.assertEqual(output.get("stderr"), "oops\n")
|
|
556
|
+
self.assertEqual(output.get("returncode"), 0)
|
|
557
|
+
self.assertEqual(output.get("error"), "")
|
|
558
|
+
mock_popen.assert_not_called()
|
|
559
|
+
|
|
560
|
+
@patch("ocpp.rfid.reader.timezone.now")
|
|
561
|
+
@patch("ocpp.rfid.reader.notify_async")
|
|
562
|
+
@patch("ocpp.rfid.reader.subprocess.Popen")
|
|
563
|
+
@patch("ocpp.rfid.reader.subprocess.run")
|
|
564
|
+
@patch("ocpp.rfid.reader.RFID.register_scan")
|
|
565
|
+
def test_external_command_error_strips_trailing_percent_tokens(
|
|
566
|
+
self, mock_register, mock_run, mock_popen, mock_notify, mock_now
|
|
567
|
+
):
|
|
568
|
+
mock_now.return_value = timezone.now()
|
|
569
|
+
tag = MagicMock()
|
|
570
|
+
tag.pk = 4
|
|
571
|
+
tag.label_id = 4
|
|
572
|
+
tag.allowed = True
|
|
573
|
+
tag.external_command = "echo boom"
|
|
574
|
+
tag.color = "R"
|
|
575
|
+
tag.released = False
|
|
576
|
+
tag.reference = None
|
|
577
|
+
tag.kind = RFID.CLASSIC
|
|
578
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
579
|
+
mock_register.return_value = (tag, False)
|
|
580
|
+
mock_run.side_effect = RuntimeError("bad % %")
|
|
581
|
+
|
|
582
|
+
result = validate_rfid_value("abcd")
|
|
583
|
+
|
|
584
|
+
output = result.get("command_output")
|
|
585
|
+
self.assertIsInstance(output, dict)
|
|
586
|
+
self.assertEqual(output.get("stdout"), "")
|
|
587
|
+
self.assertEqual(output.get("stderr"), "")
|
|
588
|
+
self.assertEqual(output.get("error"), "bad")
|
|
589
|
+
self.assertFalse(result["allowed"])
|
|
590
|
+
mock_popen.assert_not_called()
|
|
479
591
|
|
|
480
592
|
@patch("ocpp.rfid.reader.timezone.now")
|
|
481
593
|
@patch("ocpp.rfid.reader.notify_async")
|
|
@@ -497,6 +609,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
497
609
|
tag.released = False
|
|
498
610
|
tag.reference = None
|
|
499
611
|
tag.kind = RFID.CLASSIC
|
|
612
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
500
613
|
mock_register.return_value = (tag, False)
|
|
501
614
|
result = validate_rfid_value("abcdef")
|
|
502
615
|
|
|
@@ -507,10 +620,12 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
507
620
|
env = kwargs.get("env", {})
|
|
508
621
|
self.assertEqual(env.get("RFID_VALUE"), "ABCDEF")
|
|
509
622
|
self.assertEqual(env.get("RFID_LABEL_ID"), "3")
|
|
623
|
+
self.assertEqual(env.get("RFID_ENDIANNESS"), RFID.BIG_ENDIAN)
|
|
510
624
|
self.assertIs(kwargs.get("stdout"), subprocess.DEVNULL)
|
|
511
625
|
self.assertIs(kwargs.get("stderr"), subprocess.DEVNULL)
|
|
512
626
|
self.assertTrue(result["allowed"])
|
|
513
627
|
mock_notify.assert_called_once_with("RFID 3 OK", "ABCDEF B")
|
|
628
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
514
629
|
|
|
515
630
|
|
|
516
631
|
class CardTypeDetectionTests(TestCase):
|
|
@@ -656,7 +771,9 @@ class RFIDLandingTests(TestCase):
|
|
|
656
771
|
app = Application.objects.create(name="Ocpp")
|
|
657
772
|
module = Module.objects.create(node_role=role, application=app, path="/ocpp/")
|
|
658
773
|
module.create_landings()
|
|
659
|
-
self.assertTrue(
|
|
774
|
+
self.assertTrue(
|
|
775
|
+
module.landings.filter(path="/ocpp/rfid/validator/").exists()
|
|
776
|
+
)
|
|
660
777
|
|
|
661
778
|
|
|
662
779
|
class ScannerTemplateTests(TestCase):
|