arthexis 0.1.16__py3-none-any.whl → 0.1.17__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.

core/tests.py CHANGED
@@ -20,6 +20,7 @@ import types
20
20
  from glob import glob
21
21
  from datetime import datetime, timedelta, timezone as datetime_timezone
22
22
  import tempfile
23
+ from io import StringIO
23
24
  from urllib.parse import quote
24
25
 
25
26
  from django.utils import timezone
@@ -57,6 +58,7 @@ from nodes.models import ContentSample
57
58
 
58
59
  from django.core.exceptions import ValidationError
59
60
  from django.core.management import call_command
61
+ from django.core.management.base import CommandError
60
62
  from django.db import IntegrityError
61
63
  from .backends import LocalhostAdminBackend
62
64
  from core.views import (
@@ -983,6 +985,40 @@ class RFIDImportExportCommandTests(TestCase):
983
985
  self.assertTrue(tag.energy_accounts.filter(pk=account.pk).exists())
984
986
 
985
987
 
988
+ class CheckRFIDCommandTests(TestCase):
989
+ def test_successful_validation_outputs_json(self):
990
+ out = StringIO()
991
+
992
+ call_command("check_rfid", "abcd1234", stdout=out)
993
+
994
+ payload = json.loads(out.getvalue())
995
+ self.assertEqual(payload["rfid"], "ABCD1234")
996
+ self.assertTrue(payload["created"])
997
+ self.assertTrue(RFID.objects.filter(rfid="ABCD1234").exists())
998
+
999
+ def test_invalid_value_raises_error(self):
1000
+ with self.assertRaises(CommandError):
1001
+ call_command("check_rfid", "invalid!")
1002
+
1003
+ def test_kind_option_updates_existing_tag(self):
1004
+ tag = RFID.objects.create(rfid="EXISTING", allowed=False, kind=RFID.CLASSIC)
1005
+ out = StringIO()
1006
+
1007
+ call_command(
1008
+ "check_rfid",
1009
+ "existing",
1010
+ "--kind",
1011
+ RFID.NTAG215,
1012
+ stdout=out,
1013
+ )
1014
+
1015
+ payload = json.loads(out.getvalue())
1016
+ tag.refresh_from_db()
1017
+ self.assertFalse(payload["created"])
1018
+ self.assertEqual(payload["kind"], RFID.NTAG215)
1019
+ self.assertEqual(tag.kind, RFID.NTAG215)
1020
+
1021
+
986
1022
  class RFIDKeyVerificationFlagTests(TestCase):
987
1023
  def test_flags_reset_on_key_change(self):
988
1024
  tag = RFID.objects.create(
@@ -1037,6 +1073,44 @@ class ReleaseProcessTests(TestCase):
1037
1073
  )
1038
1074
  sync_main.assert_called_once_with(Path("rel.log"))
1039
1075
 
1076
+ def test_step_check_todos_logs_instruction_when_pending(self):
1077
+ log_path = Path("rel.log")
1078
+ log_path.unlink(missing_ok=True)
1079
+ Todo.objects.create(request="Review checklist")
1080
+ ctx: dict[str, object] = {}
1081
+
1082
+ try:
1083
+ with self.assertRaises(core_views.PendingTodos):
1084
+ core_views._step_check_todos(self.release, ctx, log_path)
1085
+
1086
+ contents = log_path.read_text(encoding="utf-8")
1087
+ message = "Release checklist requires acknowledgment before continuing."
1088
+ self.assertIn(message, contents)
1089
+ self.assertIn("Review outstanding TODO items", contents)
1090
+
1091
+ with self.assertRaises(core_views.PendingTodos):
1092
+ core_views._step_check_todos(self.release, ctx, log_path)
1093
+
1094
+ contents = log_path.read_text(encoding="utf-8")
1095
+ self.assertEqual(contents.count(message), 1)
1096
+ finally:
1097
+ log_path.unlink(missing_ok=True)
1098
+
1099
+ def test_step_check_todos_auto_ack_when_no_pending(self):
1100
+ log_path = Path("rel.log")
1101
+ log_path.unlink(missing_ok=True)
1102
+ ctx: dict[str, object] = {}
1103
+
1104
+ try:
1105
+ with mock.patch("core.views._refresh_changelog_once"):
1106
+ core_views._step_check_todos(self.release, ctx, log_path)
1107
+ finally:
1108
+ log_path.unlink(missing_ok=True)
1109
+
1110
+ self.assertTrue(ctx.get("todos_ack"))
1111
+ self.assertNotIn("todos_required", ctx)
1112
+ self.assertIsNone(ctx.get("todos"))
1113
+
1040
1114
  @mock.patch("core.views._sync_with_origin_main")
1041
1115
  @mock.patch("core.views.release_utils._git_clean", return_value=True)
1042
1116
  @mock.patch("core.views.release_utils.network_available", return_value=False)
@@ -1527,6 +1601,54 @@ class ReleaseProcessTests(TestCase):
1527
1601
  ctx = session.get(f"release_publish_{self.release.pk}")
1528
1602
  self.assertTrue(ctx.get("dry_run"))
1529
1603
 
1604
+ def test_resume_button_shown_when_credentials_missing(self):
1605
+ user = User.objects.create_superuser("admin", "admin@example.com", "pw")
1606
+ url = reverse("release-progress", args=[self.release.pk, "publish"])
1607
+ self.client.force_login(user)
1608
+
1609
+ self.client.get(f"{url}?start=1")
1610
+
1611
+ session = self.client.session
1612
+ ctx = session.get(f"release_publish_{self.release.pk}") or {}
1613
+ ctx.update({"step": 7, "started": True, "paused": False})
1614
+ session[f"release_publish_{self.release.pk}"] = ctx
1615
+ session.save()
1616
+
1617
+ response = self.client.get(f"{url}?step=7")
1618
+ self.assertEqual(response.status_code, 200)
1619
+ context = response.context
1620
+ if isinstance(context, list):
1621
+ context = context[-1]
1622
+ self.assertTrue(context["resume_available"])
1623
+ self.assertIn(b"Resume Publish", response.content)
1624
+
1625
+ def test_resume_without_step_parameter_defaults_to_current_progress(self):
1626
+ run: list[str] = []
1627
+
1628
+ def step_fn(release, ctx, log_path):
1629
+ run.append("step")
1630
+
1631
+ steps = [("Only step", step_fn)]
1632
+ user = User.objects.create_superuser("admin", "admin@example.com", "pw")
1633
+ url = reverse("release-progress", args=[self.release.pk, "publish"])
1634
+ with mock.patch("core.views.PUBLISH_STEPS", steps):
1635
+ self.client.force_login(user)
1636
+ session = self.client.session
1637
+ session[f"release_publish_{self.release.pk}"] = {
1638
+ "step": 0,
1639
+ "started": True,
1640
+ "paused": False,
1641
+ }
1642
+ session.save()
1643
+
1644
+ response = self.client.get(f"{url}?resume=1")
1645
+ self.assertEqual(response.status_code, 200)
1646
+ self.assertEqual(run, ["step"])
1647
+
1648
+ session = self.client.session
1649
+ ctx = session.get(f"release_publish_{self.release.pk}")
1650
+ self.assertEqual(ctx.get("step"), 1)
1651
+
1530
1652
  def test_new_todo_does_not_reset_pending_flow(self):
1531
1653
  user = User.objects.create_superuser("admin", "admin@example.com", "pw")
1532
1654
  url = reverse("release-progress", args=[self.release.pk, "publish"])
core/views.py CHANGED
@@ -656,8 +656,8 @@ def _should_use_python_changelog(exc: OSError) -> bool:
656
656
  def _generate_changelog_with_python(log_path: Path) -> None:
657
657
  _append_log(log_path, "Falling back to Python changelog generator")
658
658
  changelog_path = Path("CHANGELOG.rst")
659
- range_spec = changelog_utils.determine_range_spec()
660
659
  previous = changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
660
+ range_spec = changelog_utils.determine_range_spec(previous_text=previous)
661
661
  sections = changelog_utils.collect_sections(range_spec=range_spec, previous_text=previous)
662
662
  content = changelog_utils.render_changelog(sections)
663
663
  if not content.endswith("\n"):
@@ -897,7 +897,18 @@ def _step_check_todos(release, ctx, log_path: Path) -> None:
897
897
  pending_values = list(
898
898
  pending_qs.values("id", "request", "url", "request_details")
899
899
  )
900
+ if not pending_values:
901
+ ctx["todos_ack"] = True
902
+
900
903
  if not ctx.get("todos_ack"):
904
+ if not ctx.get("todos_block_logged"):
905
+ _append_log(
906
+ log_path,
907
+ "Release checklist requires acknowledgment before continuing. "
908
+ "Review outstanding TODO items and confirm the checklist; "
909
+ "publishing will resume automatically afterward.",
910
+ )
911
+ ctx["todos_block_logged"] = True
901
912
  ctx["todos"] = pending_values
902
913
  ctx["todos_required"] = True
903
914
  raise PendingTodos()
@@ -1451,12 +1462,29 @@ def _step_publish(release, ctx, log_path: Path) -> None:
1451
1462
  )
1452
1463
  else:
1453
1464
  _append_log(log_path, "Uploading distribution")
1454
- release_utils.publish(
1455
- package=release.to_package(),
1456
- version=release.version,
1457
- creds=release.to_credentials(),
1458
- repositories=targets,
1459
- )
1465
+ publish_warning: release_utils.PostPublishWarning | None = None
1466
+ try:
1467
+ release_utils.publish(
1468
+ package=release.to_package(),
1469
+ version=release.version,
1470
+ creds=release.to_credentials(),
1471
+ repositories=targets,
1472
+ )
1473
+ except release_utils.PostPublishWarning as warning:
1474
+ publish_warning = warning
1475
+
1476
+ if publish_warning is not None:
1477
+ message = str(publish_warning)
1478
+ followups = _dedupe_preserve_order(publish_warning.followups)
1479
+ warning_entries = ctx.setdefault("warnings", [])
1480
+ if not any(entry.get("message") == message for entry in warning_entries):
1481
+ entry: dict[str, object] = {"message": message}
1482
+ if followups:
1483
+ entry["followups"] = followups
1484
+ warning_entries.append(entry)
1485
+ _append_log(log_path, message)
1486
+ for note in followups:
1487
+ _append_log(log_path, f"Follow-up: {note}")
1460
1488
  release.pypi_url = (
1461
1489
  f"https://pypi.org/project/{release.package.name}/{release.version}/"
1462
1490
  )
@@ -1804,8 +1832,16 @@ def release_progress(request, pk: int, action: str):
1804
1832
  ctx["release_approval"] = "approved"
1805
1833
  if request.GET.get("reject"):
1806
1834
  ctx["release_approval"] = "rejected"
1835
+ resume_requested = bool(request.GET.get("resume"))
1836
+
1807
1837
  if request.GET.get("pause") and ctx.get("started"):
1808
1838
  ctx["paused"] = True
1839
+
1840
+ if resume_requested:
1841
+ if not ctx.get("started"):
1842
+ ctx["started"] = True
1843
+ if ctx.get("paused"):
1844
+ ctx["paused"] = False
1809
1845
  restart_count = 0
1810
1846
  if restart_path.exists():
1811
1847
  try:
@@ -1814,6 +1850,8 @@ def release_progress(request, pk: int, action: str):
1814
1850
  restart_count = 0
1815
1851
  step_count = ctx.get("step", 0)
1816
1852
  step_param = request.GET.get("step")
1853
+ if resume_requested and step_param is None:
1854
+ step_param = str(step_count)
1817
1855
 
1818
1856
  pending_qs = Todo.objects.filter(is_deleted=False, done_on__isnull=True)
1819
1857
  pending_items = list(pending_qs)
@@ -1833,6 +1871,9 @@ def release_progress(request, pk: int, action: str):
1833
1871
  else:
1834
1872
  ctx["todos_ack"] = True
1835
1873
 
1874
+ if ctx.get("todos_ack"):
1875
+ ctx.pop("todos_block_logged", None)
1876
+
1836
1877
  if not ctx.get("todos_ack"):
1837
1878
  ctx["todos"] = [
1838
1879
  {
@@ -2049,6 +2090,14 @@ def release_progress(request, pk: int, action: str):
2049
2090
  )
2050
2091
 
2051
2092
  is_running = ctx.get("started") and not paused and not done and not ctx.get("error")
2093
+ resume_available = (
2094
+ ctx.get("started")
2095
+ and not paused
2096
+ and not done
2097
+ and not ctx.get("error")
2098
+ and step_count < len(steps)
2099
+ and next_step is None
2100
+ )
2052
2101
  can_resume = ctx.get("started") and paused and not done and not ctx.get("error")
2053
2102
  release_manager_owner = manager.owner_display() if manager else ""
2054
2103
  try:
@@ -2103,9 +2152,11 @@ def release_progress(request, pk: int, action: str):
2103
2152
  "has_release_manager": bool(manager),
2104
2153
  "current_user_admin_url": current_user_admin_url,
2105
2154
  "is_running": is_running,
2155
+ "resume_available": resume_available,
2106
2156
  "can_resume": can_resume,
2107
2157
  "dry_run": dry_run_active,
2108
2158
  "dry_run_toggle_enabled": dry_run_toggle_enabled,
2159
+ "warnings": ctx.get("warnings", []),
2109
2160
  }
2110
2161
  request.session[session_key] = ctx
2111
2162
  if done or ctx.get("error"):
ocpp/admin.py CHANGED
@@ -29,6 +29,7 @@ from .transactions_io import (
29
29
  import_transactions as import_transactions_data,
30
30
  )
31
31
  from .status_display import STATUS_BADGE_MAP, ERROR_OK_VALUES
32
+ from .views import _charger_state, _live_sessions
32
33
  from core.admin import SaveBeforeChangeAction
33
34
  from core.user_data import EntityModelAdmin
34
35
 
@@ -253,6 +254,8 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
253
254
  actions = [
254
255
  "purge_data",
255
256
  "fetch_cp_configuration",
257
+ "toggle_rfid_authentication",
258
+ "recheck_charger_status",
256
259
  "change_availability_operative",
257
260
  "change_availability_inoperative",
258
261
  "set_availability_state_operative",
@@ -317,16 +320,14 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
317
320
  "charger-status-connector",
318
321
  args=[obj.charger_id, obj.connector_slug],
319
322
  )
320
- label = (obj.last_status or "status").strip() or "status"
321
- status_key = label.lower()
322
- error_code = (obj.last_error_code or "").strip().lower()
323
- if (
324
- self._has_active_session(obj)
325
- and error_code in ERROR_OK_VALUES
326
- and (status_key not in STATUS_BADGE_MAP or status_key == "available")
327
- ):
328
- label = STATUS_BADGE_MAP["charging"][0]
329
- return format_html('<a href="{}" target="_blank">{}</a>', url, label)
323
+ tx_obj = store.get_transaction(obj.charger_id, obj.connector_id)
324
+ state, _ = _charger_state(
325
+ obj,
326
+ tx_obj
327
+ if obj.connector_id is not None
328
+ else (_live_sessions(obj) or None),
329
+ )
330
+ return format_html('<a href="{}" target="_blank">{}</a>', url, state)
330
331
 
331
332
  status_link.short_description = "Status"
332
333
 
@@ -359,6 +360,63 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
359
360
 
360
361
  purge_data.short_description = "Purge data"
361
362
 
363
+ @admin.action(description="Re-check Charger Status")
364
+ def recheck_charger_status(self, request, queryset):
365
+ requested = 0
366
+ for charger in queryset:
367
+ connector_value = charger.connector_id
368
+ ws = store.get_connection(charger.charger_id, connector_value)
369
+ if ws is None:
370
+ self.message_user(
371
+ request,
372
+ f"{charger}: no active connection",
373
+ level=messages.ERROR,
374
+ )
375
+ continue
376
+ payload: dict[str, object] = {"requestedMessage": "StatusNotification"}
377
+ trigger_connector: int | None = None
378
+ if connector_value is not None:
379
+ payload["connectorId"] = connector_value
380
+ trigger_connector = connector_value
381
+ message_id = uuid.uuid4().hex
382
+ msg = json.dumps([2, message_id, "TriggerMessage", payload])
383
+ try:
384
+ async_to_sync(ws.send)(msg)
385
+ except Exception as exc: # pragma: no cover - network error
386
+ self.message_user(
387
+ request,
388
+ f"{charger}: failed to send TriggerMessage ({exc})",
389
+ level=messages.ERROR,
390
+ )
391
+ continue
392
+ log_key = store.identity_key(charger.charger_id, connector_value)
393
+ store.add_log(log_key, f"< {msg}", log_type="charger")
394
+ store.register_pending_call(
395
+ message_id,
396
+ {
397
+ "action": "TriggerMessage",
398
+ "charger_id": charger.charger_id,
399
+ "connector_id": connector_value,
400
+ "log_key": log_key,
401
+ "trigger_target": "StatusNotification",
402
+ "trigger_connector": trigger_connector,
403
+ "requested_at": timezone.now(),
404
+ },
405
+ )
406
+ store.schedule_call_timeout(
407
+ message_id,
408
+ timeout=5.0,
409
+ action="TriggerMessage",
410
+ log_key=log_key,
411
+ message="TriggerMessage StatusNotification timed out",
412
+ )
413
+ requested += 1
414
+ if requested:
415
+ self.message_user(
416
+ request,
417
+ f"Requested status update from {requested} charger(s)",
418
+ )
419
+
362
420
  @admin.action(description="Fetch CP configuration")
363
421
  def fetch_cp_configuration(self, request, queryset):
364
422
  fetched = 0
@@ -413,6 +471,30 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
413
471
  f"Requested configuration from {fetched} charger(s)",
414
472
  )
415
473
 
474
+ @admin.action(description="Toggle RFID Authentication")
475
+ def toggle_rfid_authentication(self, request, queryset):
476
+ enabled = 0
477
+ disabled = 0
478
+ for charger in queryset:
479
+ new_value = not charger.require_rfid
480
+ Charger.objects.filter(pk=charger.pk).update(require_rfid=new_value)
481
+ charger.require_rfid = new_value
482
+ if new_value:
483
+ enabled += 1
484
+ else:
485
+ disabled += 1
486
+ if enabled or disabled:
487
+ changes = []
488
+ if enabled:
489
+ changes.append(f"enabled for {enabled} charger(s)")
490
+ if disabled:
491
+ changes.append(f"disabled for {disabled} charger(s)")
492
+ summary = "; ".join(changes)
493
+ self.message_user(
494
+ request,
495
+ f"Updated RFID authentication: {summary}",
496
+ )
497
+
416
498
  def _dispatch_change_availability(self, request, queryset, availability_type: str):
417
499
  sent = 0
418
500
  for charger in queryset:
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,7 @@ 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)
479
521
 
480
522
  @patch("ocpp.rfid.reader.timezone.now")
481
523
  @patch("ocpp.rfid.reader.notify_async")
@@ -497,6 +539,7 @@ class ValidateRfidValueTests(SimpleTestCase):
497
539
  tag.released = False
498
540
  tag.reference = None
499
541
  tag.kind = RFID.CLASSIC
542
+ tag.endianness = RFID.BIG_ENDIAN
500
543
  mock_register.return_value = (tag, False)
501
544
  result = validate_rfid_value("abcdef")
502
545
 
@@ -507,10 +550,12 @@ class ValidateRfidValueTests(SimpleTestCase):
507
550
  env = kwargs.get("env", {})
508
551
  self.assertEqual(env.get("RFID_VALUE"), "ABCDEF")
509
552
  self.assertEqual(env.get("RFID_LABEL_ID"), "3")
553
+ self.assertEqual(env.get("RFID_ENDIANNESS"), RFID.BIG_ENDIAN)
510
554
  self.assertIs(kwargs.get("stdout"), subprocess.DEVNULL)
511
555
  self.assertIs(kwargs.get("stderr"), subprocess.DEVNULL)
512
556
  self.assertTrue(result["allowed"])
513
557
  mock_notify.assert_called_once_with("RFID 3 OK", "ABCDEF B")
558
+ self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
514
559
 
515
560
 
516
561
  class CardTypeDetectionTests(TestCase):