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.

Files changed (63) hide show
  1. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/METADATA +84 -35
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. config/asgi.py +1 -15
  4. config/middleware.py +47 -1
  5. config/settings.py +15 -30
  6. config/urls.py +53 -1
  7. core/admin.py +540 -450
  8. core/apps.py +0 -6
  9. core/auto_upgrade.py +19 -4
  10. core/backends.py +13 -3
  11. core/changelog.py +66 -5
  12. core/environment.py +4 -5
  13. core/models.py +1566 -203
  14. core/notifications.py +1 -1
  15. core/reference_utils.py +10 -11
  16. core/release.py +55 -7
  17. core/sigil_builder.py +2 -2
  18. core/sigil_resolver.py +1 -66
  19. core/system.py +268 -2
  20. core/tasks.py +174 -48
  21. core/tests.py +314 -16
  22. core/user_data.py +42 -2
  23. core/views.py +278 -183
  24. nodes/admin.py +557 -65
  25. nodes/apps.py +11 -0
  26. nodes/models.py +658 -113
  27. nodes/rfid_sync.py +1 -1
  28. nodes/tasks.py +97 -2
  29. nodes/tests.py +1212 -116
  30. nodes/urls.py +15 -1
  31. nodes/utils.py +51 -3
  32. nodes/views.py +1239 -154
  33. ocpp/admin.py +979 -152
  34. ocpp/consumers.py +268 -28
  35. ocpp/models.py +488 -3
  36. ocpp/network.py +398 -0
  37. ocpp/store.py +6 -4
  38. ocpp/tasks.py +296 -2
  39. ocpp/test_export_import.py +1 -0
  40. ocpp/test_rfid.py +121 -4
  41. ocpp/tests.py +950 -11
  42. ocpp/transactions_io.py +9 -1
  43. ocpp/urls.py +3 -3
  44. ocpp/views.py +596 -51
  45. pages/admin.py +262 -30
  46. pages/apps.py +35 -0
  47. pages/context_processors.py +26 -21
  48. pages/defaults.py +1 -1
  49. pages/forms.py +31 -8
  50. pages/middleware.py +6 -2
  51. pages/models.py +77 -2
  52. pages/module_defaults.py +5 -5
  53. pages/site_config.py +137 -0
  54. pages/tests.py +885 -109
  55. pages/urls.py +13 -2
  56. pages/utils.py +70 -0
  57. pages/views.py +558 -55
  58. arthexis-0.1.16.dist-info/RECORD +0 -111
  59. core/workgroup_urls.py +0 -17
  60. core/workgroup_views.py +0 -94
  61. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  62. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +0 -0
  63. {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 .models import MeterValue, Transaction
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(
@@ -111,6 +111,7 @@ class TransactionAdminExportImportTests(TestCase):
111
111
  "charger": "C9",
112
112
  "account": None,
113
113
  "rfid": "",
114
+ "vid": "",
114
115
  "vin": "",
115
116
  "meter_start": 0,
116
117
  "meter_stop": 0,
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("ABCD1234", kind=None)
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("ABCD", kind=RFID.NTAG215)
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(module.landings.filter(path="/ocpp/rfid/").exists())
774
+ self.assertTrue(
775
+ module.landings.filter(path="/ocpp/rfid/validator/").exists()
776
+ )
660
777
 
661
778
 
662
779
  class ScannerTemplateTests(TestCase):