arthexis 0.1.13__py3-none-any.whl → 0.1.15__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.
Files changed (108) hide show
  1. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/METADATA +224 -221
  2. arthexis-0.1.15.dist-info/RECORD +110 -0
  3. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +43 -43
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -32
  9. config/context_processors.py +67 -69
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +25 -25
  14. config/offline.py +49 -49
  15. config/settings.py +691 -682
  16. config/settings_helpers.py +109 -109
  17. config/urls.py +171 -166
  18. config/wsgi.py +17 -17
  19. core/admin.py +3795 -2809
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +151 -151
  22. core/apps.py +356 -272
  23. core/auto_upgrade.py +57 -57
  24. core/backends.py +265 -236
  25. core/changelog.py +342 -0
  26. core/entity.py +149 -133
  27. core/environment.py +61 -61
  28. core/fields.py +168 -168
  29. core/form_fields.py +75 -75
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +178 -172
  32. core/github_repos.py +72 -0
  33. core/lcd_screen.py +78 -78
  34. core/liveupdate.py +25 -25
  35. core/log_paths.py +114 -100
  36. core/mailer.py +85 -85
  37. core/middleware.py +91 -91
  38. core/models.py +3637 -2795
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +108 -108
  42. core/release.py +840 -368
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -149
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +315 -315
  47. core/system.py +952 -493
  48. core/tasks.py +408 -394
  49. core/temp_passwords.py +181 -181
  50. core/test_system_info.py +186 -139
  51. core/tests.py +2168 -1521
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +641 -633
  55. core/views.py +2201 -1417
  56. core/widgets.py +213 -94
  57. core/workgroup_urls.py +17 -17
  58. core/workgroup_views.py +94 -94
  59. nodes/admin.py +1720 -1161
  60. nodes/apps.py +87 -85
  61. nodes/backends.py +160 -160
  62. nodes/dns.py +203 -203
  63. nodes/feature_checks.py +133 -133
  64. nodes/lcd.py +165 -165
  65. nodes/models.py +1764 -1597
  66. nodes/reports.py +411 -411
  67. nodes/rfid_sync.py +195 -0
  68. nodes/signals.py +18 -0
  69. nodes/tasks.py +46 -46
  70. nodes/tests.py +3830 -3116
  71. nodes/urls.py +15 -14
  72. nodes/utils.py +121 -105
  73. nodes/views.py +683 -619
  74. ocpp/admin.py +948 -948
  75. ocpp/apps.py +25 -25
  76. ocpp/consumers.py +1565 -1459
  77. ocpp/evcs.py +844 -844
  78. ocpp/evcs_discovery.py +158 -158
  79. ocpp/models.py +917 -917
  80. ocpp/reference_utils.py +42 -42
  81. ocpp/routing.py +11 -11
  82. ocpp/simulator.py +745 -745
  83. ocpp/status_display.py +26 -26
  84. ocpp/store.py +601 -541
  85. ocpp/tasks.py +31 -31
  86. ocpp/test_export_import.py +130 -130
  87. ocpp/test_rfid.py +913 -702
  88. ocpp/tests.py +4445 -4094
  89. ocpp/transactions_io.py +189 -189
  90. ocpp/urls.py +50 -50
  91. ocpp/views.py +1479 -1251
  92. pages/admin.py +769 -539
  93. pages/apps.py +10 -10
  94. pages/checks.py +40 -40
  95. pages/context_processors.py +127 -119
  96. pages/defaults.py +13 -13
  97. pages/forms.py +198 -198
  98. pages/middleware.py +209 -153
  99. pages/models.py +643 -426
  100. pages/tasks.py +74 -0
  101. pages/tests.py +3025 -2200
  102. pages/urls.py +26 -25
  103. pages/utils.py +23 -12
  104. pages/views.py +1176 -1128
  105. arthexis-0.1.13.dist-info/RECORD +0 -105
  106. nodes/actions.py +0 -70
  107. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/WHEEL +0 -0
  108. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/top_level.txt +0 -0
ocpp/test_rfid.py CHANGED
@@ -1,702 +1,913 @@
1
- import io
2
- import json
3
- import os
4
- import sys
5
- import types
6
- from pathlib import Path
7
- from unittest.mock import patch, MagicMock, call
8
-
9
- import pytest
10
-
11
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
12
-
13
- import django
14
-
15
- django.setup()
16
-
17
- from django.test import SimpleTestCase, TestCase
18
- from django.urls import reverse
19
- from django.contrib.auth import get_user_model
20
- from django.contrib.sites.models import Site
21
- from django.utils import timezone
22
-
23
- from pages.models import Application, Module
24
- from nodes.models import Node, NodeRole
25
-
26
- from core.models import RFID
27
- from ocpp.rfid.reader import read_rfid, enable_deep_read, validate_rfid_value
28
- from ocpp.rfid.detect import detect_scanner, main as detect_main
29
- from ocpp.rfid import background_reader
30
- from ocpp.rfid.constants import (
31
- DEFAULT_IRQ_PIN,
32
- DEFAULT_RST_PIN,
33
- GPIO_PIN_MODE_BCM,
34
- MODULE_WIRING,
35
- SPI_BUS,
36
- SPI_DEVICE,
37
- )
38
-
39
-
40
- pytestmark = [pytest.mark.feature("rfid-scanner")]
41
-
42
-
43
- class BackgroundReaderConfigurationTests(SimpleTestCase):
44
- def setUp(self):
45
- background_reader._auto_detect_logged = False
46
-
47
- def tearDown(self):
48
- background_reader._auto_detect_logged = False
49
-
50
- def test_is_configured_auto_detects_without_lock(self):
51
- fake_lock = Path("/tmp/rfid-auto-detect.lock")
52
- with (
53
- patch("ocpp.rfid.background_reader._lock_path", return_value=fake_lock),
54
- patch("ocpp.rfid.background_reader._has_spi_device", return_value=True),
55
- patch(
56
- "ocpp.rfid.background_reader._dependencies_available",
57
- return_value=True,
58
- ),
59
- ):
60
- self.assertTrue(background_reader.is_configured())
61
-
62
- def test_is_configured_requires_dependencies(self):
63
- fake_lock = Path("/tmp/rfid-auto-detect.lock")
64
- with (
65
- patch("ocpp.rfid.background_reader._lock_path", return_value=fake_lock),
66
- patch("ocpp.rfid.background_reader._has_spi_device", return_value=True),
67
- patch(
68
- "ocpp.rfid.background_reader._dependencies_available",
69
- return_value=False,
70
- ),
71
- ):
72
- self.assertFalse(background_reader.is_configured())
73
-
74
-
75
- class ScanNextViewTests(TestCase):
76
- def setUp(self):
77
- User = get_user_model()
78
- self.user = User.objects.create_user("rfid-user", password="pwd")
79
- self.client.force_login(self.user)
80
-
81
- @patch("config.middleware.Node.get_local", return_value=None)
82
- @patch("config.middleware.get_site")
83
- @patch(
84
- "ocpp.rfid.views.scan_sources",
85
- return_value={
86
- "rfid": "ABCD1234",
87
- "label_id": 1,
88
- "created": False,
89
- "kind": RFID.CLASSIC,
90
- },
91
- )
92
- def test_scan_next_success(self, mock_scan, mock_site, mock_node):
93
- resp = self.client.get(reverse("rfid-scan-next"))
94
- self.assertEqual(resp.status_code, 200)
95
- self.assertEqual(
96
- resp.json(),
97
- {
98
- "rfid": "ABCD1234",
99
- "label_id": 1,
100
- "created": False,
101
- "kind": RFID.CLASSIC,
102
- },
103
- )
104
-
105
- @patch("config.middleware.Node.get_local", return_value=None)
106
- @patch("config.middleware.get_site")
107
- @patch("ocpp.rfid.views.scan_sources", return_value={"error": "boom"})
108
- def test_scan_next_error(self, mock_scan, mock_site, mock_node):
109
- resp = self.client.get(reverse("rfid-scan-next"))
110
- self.assertEqual(resp.status_code, 500)
111
- self.assertEqual(resp.json(), {"error": "boom"})
112
-
113
- @patch("config.middleware.Node.get_local", return_value=None)
114
- @patch("config.middleware.get_site")
115
- @patch(
116
- "ocpp.rfid.views.validate_rfid_value",
117
- return_value={"rfid": "ABCD1234", "label_id": 1, "created": False},
118
- )
119
- def test_scan_next_post_validates(self, mock_validate, mock_site, mock_node):
120
- User = get_user_model()
121
- user = User.objects.create_user("scanner", password="pwd")
122
- self.client.force_login(user)
123
- resp = self.client.post(
124
- reverse("rfid-scan-next"),
125
- data=json.dumps({"rfid": "ABCD1234"}),
126
- content_type="application/json",
127
- )
128
- self.assertEqual(resp.status_code, 200)
129
- self.assertEqual(
130
- resp.json(), {"rfid": "ABCD1234", "label_id": 1, "created": False}
131
- )
132
- mock_validate.assert_called_once_with("ABCD1234", kind=None)
133
-
134
- @patch("config.middleware.Node.get_local", return_value=None)
135
- @patch("config.middleware.get_site")
136
- @patch("ocpp.rfid.views.validate_rfid_value")
137
- def test_scan_next_post_requires_authentication(
138
- self, mock_validate, mock_site, mock_node
139
- ):
140
- resp = self.client.post(
141
- reverse("rfid-scan-next"),
142
- data=json.dumps({"rfid": "ABCD1234"}),
143
- content_type="application/json",
144
- )
145
- self.assertEqual(resp.status_code, 401)
146
- self.assertEqual(resp.json(), {"error": "Authentication required"})
147
- mock_validate.assert_not_called()
148
-
149
- @patch("config.middleware.Node.get_local", return_value=None)
150
- @patch("config.middleware.get_site")
151
- def test_scan_next_post_invalid_json(self, mock_site, mock_node):
152
- User = get_user_model()
153
- user = User.objects.create_user("invalid-json", password="pwd")
154
- self.client.force_login(user)
155
- resp = self.client.post(
156
- reverse("rfid-scan-next"),
157
- data="{",
158
- content_type="application/json",
159
- )
160
- self.assertEqual(resp.status_code, 400)
161
- self.assertEqual(resp.json(), {"error": "Invalid JSON payload"})
162
-
163
- def test_scan_next_requires_authentication(self):
164
- self.client.logout()
165
- resp = self.client.get(reverse("rfid-scan-next"))
166
- self.assertEqual(resp.status_code, 302)
167
- self.assertIn(reverse("pages:login"), resp.url)
168
-
169
-
170
- class ReaderNotificationTests(TestCase):
171
- def _mock_reader(self):
172
- class MockReader:
173
- MI_OK = 1
174
- PICC_REQIDL = 0
175
-
176
- def MFRC522_Request(self, _):
177
- return (self.MI_OK, None)
178
-
179
- def MFRC522_Anticoll(self):
180
- return (self.MI_OK, [0xAB, 0xCD, 0x12, 0x34, 0x56])
181
-
182
- def MFRC522_SelectTag(self, _uid):
183
- self.select_called = True
184
- return self.MI_OK
185
-
186
- def MFRC522_StopCrypto1(self):
187
- self.stop_called = True
188
-
189
- return MockReader()
190
-
191
- @patch("ocpp.rfid.reader.notify_async")
192
- @patch("core.models.RFID.objects.get_or_create")
193
- def test_notify_on_allowed_tag(self, mock_get, mock_notify):
194
- reference = MagicMock(value="https://example.com")
195
- tag = MagicMock(
196
- label_id=1,
197
- pk=1,
198
- allowed=True,
199
- color="B",
200
- released=False,
201
- reference=reference,
202
- )
203
- mock_get.return_value = (tag, False)
204
-
205
- reader = self._mock_reader()
206
- result = read_rfid(mfrc=reader, cleanup=False)
207
- self.assertEqual(result["label_id"], 1)
208
- self.assertEqual(result["kind"], RFID.CLASSIC)
209
- self.assertEqual(result["reference"], "https://example.com")
210
- self.assertEqual(mock_notify.call_count, 1)
211
- mock_notify.assert_has_calls([call("RFID 1 OK", f"{result['rfid']} B")])
212
- self.assertTrue(getattr(reader, "select_called", False))
213
- self.assertTrue(getattr(reader, "stop_called", False))
214
-
215
- @patch("ocpp.rfid.reader.notify_async")
216
- @patch("core.models.RFID.objects.get_or_create")
217
- def test_notify_on_disallowed_tag(self, mock_get, mock_notify):
218
- tag = MagicMock(
219
- label_id=2,
220
- pk=2,
221
- allowed=False,
222
- color="B",
223
- released=False,
224
- reference=None,
225
- )
226
- mock_get.return_value = (tag, False)
227
-
228
- reader = self._mock_reader()
229
- result = read_rfid(mfrc=reader, cleanup=False)
230
- self.assertEqual(result["kind"], RFID.CLASSIC)
231
- self.assertEqual(mock_notify.call_count, 1)
232
- mock_notify.assert_has_calls([call("RFID 2 BAD", f"{result['rfid']} B")])
233
- self.assertTrue(getattr(reader, "select_called", False))
234
- self.assertTrue(getattr(reader, "stop_called", False))
235
-
236
-
237
- class ValidateRfidValueTests(SimpleTestCase):
238
- @patch("ocpp.rfid.reader.timezone.now")
239
- @patch("ocpp.rfid.reader.notify_async")
240
- @patch("ocpp.rfid.reader.RFID.objects.get_or_create")
241
- def test_creates_new_tag(self, mock_get, mock_notify, mock_now):
242
- fake_now = object()
243
- mock_now.return_value = fake_now
244
- tag = MagicMock()
245
- tag.pk = 1
246
- tag.label_id = 1
247
- tag.allowed = True
248
- tag.color = "B"
249
- tag.released = False
250
- tag.reference = None
251
- tag.kind = RFID.CLASSIC
252
- mock_get.return_value = (tag, True)
253
-
254
- result = validate_rfid_value("abcd1234")
255
-
256
- mock_get.assert_called_once_with(rfid="ABCD1234", defaults={})
257
- tag.save.assert_called_once_with(update_fields=["last_seen_on"])
258
- self.assertIs(tag.last_seen_on, fake_now)
259
- mock_notify.assert_called_once_with("RFID 1 OK", "ABCD1234 B")
260
- self.assertTrue(result["created"])
261
- self.assertEqual(result["rfid"], "ABCD1234")
262
-
263
- @patch("ocpp.rfid.reader.timezone.now")
264
- @patch("ocpp.rfid.reader.notify_async")
265
- @patch("ocpp.rfid.reader.RFID.objects.get_or_create")
266
- def test_updates_existing_tag_kind(self, mock_get, mock_notify, mock_now):
267
- fake_now = object()
268
- mock_now.return_value = fake_now
269
- tag = MagicMock()
270
- tag.pk = 5
271
- tag.label_id = 5
272
- tag.allowed = False
273
- tag.color = "G"
274
- tag.released = True
275
- tag.reference = None
276
- tag.kind = RFID.CLASSIC
277
- mock_get.return_value = (tag, False)
278
-
279
- result = validate_rfid_value("abcd", kind=RFID.NTAG215)
280
-
281
- mock_get.assert_called_once_with(
282
- rfid="ABCD", defaults={"kind": RFID.NTAG215}
283
- )
284
- tag.save.assert_called_once_with(update_fields=["kind", "last_seen_on"])
285
- self.assertIs(tag.last_seen_on, fake_now)
286
- self.assertEqual(tag.kind, RFID.NTAG215)
287
- mock_notify.assert_called_once_with("RFID 5 BAD", "ABCD G")
288
- self.assertFalse(result["allowed"])
289
- self.assertFalse(result["created"])
290
- self.assertEqual(result["kind"], RFID.NTAG215)
291
-
292
- def test_rejects_invalid_value(self):
293
- result = validate_rfid_value("invalid!")
294
- self.assertEqual(result, {"error": "RFID must be hexadecimal digits"})
295
-
296
- def test_rejects_non_string_values(self):
297
- result = validate_rfid_value(12345)
298
- self.assertEqual(result, {"error": "RFID must be a string"})
299
-
300
- def test_rejects_missing_value(self):
301
- result = validate_rfid_value(None)
302
- self.assertEqual(result, {"error": "RFID value is required"})
303
-
304
-
305
- class CardTypeDetectionTests(TestCase):
306
- def _mock_ntag_reader(self):
307
- class MockReader:
308
- MI_OK = 1
309
- PICC_REQIDL = 0
310
-
311
- def MFRC522_Request(self, _):
312
- return (self.MI_OK, None)
313
-
314
- def MFRC522_Anticoll(self):
315
- return (
316
- self.MI_OK,
317
- [0x04, 0xD3, 0x2A, 0x1B, 0x5F, 0x23, 0x19],
318
- )
319
-
320
- def MFRC522_SelectTag(self, _uid):
321
- self.select_called = True
322
- return self.MI_OK
323
-
324
- def MFRC522_StopCrypto1(self):
325
- self.stop_called = True
326
-
327
- return MockReader()
328
-
329
- @patch("ocpp.rfid.reader.notify_async")
330
- @patch("core.models.RFID.objects.get_or_create")
331
- def test_detects_ntag215(self, mock_get, _mock_notify):
332
- tag = MagicMock(
333
- pk=1,
334
- label_id=1,
335
- allowed=True,
336
- color="B",
337
- released=False,
338
- reference=None,
339
- kind=RFID.NTAG215,
340
- )
341
- mock_get.return_value = (tag, True)
342
- reader = self._mock_ntag_reader()
343
- result = read_rfid(mfrc=reader, cleanup=False)
344
- self.assertEqual(result["kind"], RFID.NTAG215)
345
- self.assertTrue(getattr(reader, "select_called", False))
346
- self.assertTrue(getattr(reader, "stop_called", False))
347
-
348
-
349
- class RFIDLastSeenTests(TestCase):
350
- def _mock_reader(self):
351
- class MockReader:
352
- MI_OK = 1
353
- PICC_REQIDL = 0
354
-
355
- def MFRC522_Request(self, _):
356
- return (self.MI_OK, None)
357
-
358
- def MFRC522_Anticoll(self):
359
- return (self.MI_OK, [0xAB, 0xCD, 0x12, 0x34])
360
-
361
- def MFRC522_SelectTag(self, _uid):
362
- self.select_called = True
363
- return self.MI_OK
364
-
365
- def MFRC522_StopCrypto1(self):
366
- self.stop_called = True
367
-
368
- return MockReader()
369
-
370
- @patch("ocpp.rfid.reader.notify_async")
371
- def test_last_seen_updated_on_read(self, _mock_notify):
372
- tag = RFID.objects.create(rfid="ABCD1234")
373
- reader = self._mock_reader()
374
- result = read_rfid(mfrc=reader, cleanup=False)
375
- tag.refresh_from_db()
376
- self.assertIsNotNone(tag.last_seen_on)
377
- self.assertEqual(result["kind"], RFID.CLASSIC)
378
- self.assertTrue(getattr(reader, "select_called", False))
379
- self.assertTrue(getattr(reader, "stop_called", False))
380
-
381
-
382
- class RFIDDetectionScriptTests(SimpleTestCase):
383
- @patch("ocpp.rfid.detect._ensure_django")
384
- @patch(
385
- "ocpp.rfid.irq_wiring_check.check_irq_pin",
386
- return_value={"irq_pin": DEFAULT_IRQ_PIN},
387
- )
388
- def test_detect_scanner_success(self, mock_check, _mock_setup):
389
- result = detect_scanner()
390
- self.assertEqual(
391
- result,
392
- {
393
- "detected": True,
394
- "irq_pin": DEFAULT_IRQ_PIN,
395
- },
396
- )
397
- mock_check.assert_called_once()
398
-
399
- @patch("ocpp.rfid.detect._ensure_django")
400
- @patch(
401
- "ocpp.rfid.irq_wiring_check.check_irq_pin",
402
- return_value={"error": "no scanner detected"},
403
- )
404
- def test_detect_scanner_failure(self, mock_check, _mock_setup):
405
- result = detect_scanner()
406
- self.assertFalse(result["detected"])
407
- self.assertEqual(result["reason"], "no scanner detected")
408
- mock_check.assert_called_once()
409
-
410
- @patch(
411
- "ocpp.rfid.detect.detect_scanner",
412
- return_value={"detected": True, "irq_pin": DEFAULT_IRQ_PIN},
413
- )
414
- def test_detect_main_success_output(self, mock_detect):
415
- buffer = io.StringIO()
416
- with patch("sys.stdout", new=buffer):
417
- exit_code = detect_main([])
418
- self.assertEqual(exit_code, 0)
419
- self.assertIn("IRQ pin", buffer.getvalue())
420
- mock_detect.assert_called_once()
421
-
422
- @patch(
423
- "ocpp.rfid.detect.detect_scanner",
424
- return_value={"detected": False, "reason": "missing hardware"},
425
- )
426
- def test_detect_main_failure_output(self, mock_detect):
427
- buffer = io.StringIO()
428
- with patch("sys.stdout", new=buffer):
429
- exit_code = detect_main([])
430
- self.assertEqual(exit_code, 1)
431
- self.assertIn("missing hardware", buffer.getvalue())
432
- mock_detect.assert_called_once()
433
-
434
-
435
- class RestartViewTests(TestCase):
436
- def setUp(self):
437
- User = get_user_model()
438
- self.user = User.objects.create_user("restart-user", password="pwd")
439
- self.client.force_login(self.user)
440
-
441
- @patch("config.middleware.Node.get_local", return_value=None)
442
- @patch("config.middleware.get_site")
443
- @patch("ocpp.rfid.views.restart_sources", return_value={"status": "restarted"})
444
- def test_restart_endpoint(self, mock_restart, mock_site, mock_node):
445
- resp = self.client.post(reverse("rfid-scan-restart"))
446
- self.assertEqual(resp.status_code, 200)
447
- self.assertEqual(resp.json(), {"status": "restarted"})
448
- mock_restart.assert_called_once()
449
-
450
- def test_restart_requires_authentication(self):
451
- self.client.logout()
452
- resp = self.client.post(reverse("rfid-scan-restart"))
453
- self.assertEqual(resp.status_code, 302)
454
- self.assertIn(reverse("pages:login"), resp.url)
455
-
456
-
457
- class ScanTestViewTests(TestCase):
458
- def setUp(self):
459
- User = get_user_model()
460
- self.user = User.objects.create_user("scan-test-user", password="pwd")
461
- self.client.force_login(self.user)
462
-
463
- @patch("config.middleware.Node.get_local", return_value=None)
464
- @patch("config.middleware.get_site")
465
- @patch("ocpp.rfid.views.test_sources", return_value={"irq_pin": 7})
466
- def test_scan_test_success(self, mock_test, mock_site, mock_node):
467
- resp = self.client.get(reverse("rfid-scan-test"))
468
- self.assertEqual(resp.status_code, 200)
469
- self.assertEqual(resp.json(), {"irq_pin": 7})
470
-
471
- @patch("config.middleware.Node.get_local", return_value=None)
472
- @patch("config.middleware.get_site")
473
- @patch(
474
- "ocpp.rfid.views.test_sources",
475
- return_value={"error": "no scanner detected"},
476
- )
477
- def test_scan_test_error(self, mock_test, mock_site, mock_node):
478
- resp = self.client.get(reverse("rfid-scan-test"))
479
- self.assertEqual(resp.status_code, 500)
480
- self.assertEqual(resp.json(), {"error": "no scanner detected"})
481
-
482
- def test_scan_test_requires_authentication(self):
483
- self.client.logout()
484
- resp = self.client.get(reverse("rfid-scan-test"))
485
- self.assertEqual(resp.status_code, 302)
486
- self.assertIn(reverse("pages:login"), resp.url)
487
-
488
-
489
- class RFIDLandingTests(TestCase):
490
- def test_scanner_view_registered_as_landing(self):
491
- role, _ = NodeRole.objects.get_or_create(name="Terminal")
492
- Node.objects.update_or_create(
493
- mac_address=Node.get_current_mac(),
494
- defaults={"hostname": "localhost", "address": "127.0.0.1", "role": role},
495
- )
496
- Site.objects.update_or_create(
497
- id=1, defaults={"domain": "testserver", "name": ""}
498
- )
499
- app = Application.objects.create(name="Ocpp")
500
- module = Module.objects.create(node_role=role, application=app, path="/ocpp/")
501
- module.create_landings()
502
- self.assertTrue(module.landings.filter(path="/ocpp/rfid/").exists())
503
-
504
-
505
- class ScannerTemplateTests(TestCase):
506
- def setUp(self):
507
- self.url = reverse("rfid-reader")
508
- User = get_user_model()
509
- self.user = User.objects.create_user("scanner-user", password="pwd")
510
-
511
- def test_configure_link_for_staff(self):
512
- User = get_user_model()
513
- staff = User.objects.create_user("staff", password="pwd", is_staff=True)
514
- self.client.force_login(staff)
515
- resp = self.client.get(self.url)
516
- self.assertContains(resp, 'id="rfid-configure"')
517
-
518
- def test_redirect_for_anonymous(self):
519
- self.client.logout()
520
- resp = self.client.get(self.url)
521
- self.assertEqual(resp.status_code, 302)
522
- self.assertIn(reverse("pages:login"), resp.url)
523
-
524
- def test_advanced_fields_for_staff(self):
525
- User = get_user_model()
526
- staff = User.objects.create_user("staff2", password="pwd", is_staff=True)
527
- self.client.force_login(staff)
528
- resp = self.client.get(self.url)
529
- self.assertContains(resp, 'id="rfid-kind"')
530
- self.assertContains(resp, 'id="rfid-rfid"')
531
- self.assertContains(resp, 'id="rfid-released"')
532
- self.assertContains(resp, 'id="rfid-reference"')
533
-
534
- def test_basic_fields_for_authenticated_user(self):
535
- self.client.logout()
536
- self.client.force_login(self.user)
537
- resp = self.client.get(self.url)
538
- self.assertContains(resp, 'id="rfid-kind"')
539
- self.assertNotContains(resp, 'id="rfid-connect-local"')
540
- self.assertNotContains(resp, 'id="rfid-rfid"')
541
- self.assertNotContains(resp, 'id="rfid-released"')
542
- self.assertNotContains(resp, 'id="rfid-reference"')
543
-
544
- def test_deep_read_button_for_staff(self):
545
- User = get_user_model()
546
- staff = User.objects.create_user("staff3", password="pwd", is_staff=True)
547
- self.client.force_login(staff)
548
- resp = self.client.get(self.url)
549
- self.assertContains(resp, 'id="rfid-deep-read"')
550
-
551
- def test_no_deep_read_button_for_authenticated_user(self):
552
- self.client.logout()
553
- self.client.force_login(self.user)
554
- resp = self.client.get(self.url)
555
- self.assertNotContains(resp, 'id="rfid-deep-read"')
556
-
557
-
558
- class ReaderPollingTests(SimpleTestCase):
559
- def _mock_reader_no_tag(self):
560
- class MockReader:
561
- MI_OK = 1
562
- PICC_REQIDL = 0
563
-
564
- def MFRC522_Request(self, _):
565
- return (0, None)
566
-
567
- return MockReader()
568
-
569
- @patch("ocpp.rfid.reader.time.sleep")
570
- def test_poll_interval_used(self, mock_sleep):
571
- read_rfid(
572
- mfrc=self._mock_reader_no_tag(),
573
- cleanup=False,
574
- timeout=0.002,
575
- poll_interval=0.001,
576
- )
577
- mock_sleep.assert_called_with(0.001)
578
-
579
- @patch("ocpp.rfid.reader.time.sleep")
580
- def test_use_irq_skips_sleep(self, mock_sleep):
581
- read_rfid(
582
- mfrc=self._mock_reader_no_tag(),
583
- cleanup=False,
584
- timeout=0.002,
585
- use_irq=True,
586
- )
587
- mock_sleep.assert_not_called()
588
-
589
-
590
- class DeepReadViewTests(TestCase):
591
- @patch("config.middleware.Node.get_local", return_value=None)
592
- @patch("config.middleware.get_site")
593
- @patch(
594
- "ocpp.rfid.views.enable_deep_read_mode",
595
- return_value={"status": "deep", "timeout": 60},
596
- )
597
- def test_enable_deep_read(self, mock_enable, mock_site, mock_node):
598
- User = get_user_model()
599
- staff = User.objects.create_user("staff4", password="pwd", is_staff=True)
600
- self.client.force_login(staff)
601
- resp = self.client.post(reverse("rfid-scan-deep"))
602
- self.assertEqual(resp.status_code, 200)
603
- self.assertEqual(resp.json(), {"status": "deep", "timeout": 60})
604
- mock_enable.assert_called_once()
605
-
606
- def test_forbidden_for_anonymous(self):
607
- resp = self.client.post(reverse("rfid-scan-deep"))
608
- self.assertNotEqual(resp.status_code, 200)
609
-
610
-
611
- class DeepReadAuthTests(TestCase):
612
- class MockReader:
613
- MI_OK = 1
614
- MI_ERR = 2
615
- PICC_REQIDL = 0
616
- PICC_AUTHENT1A = 0x60
617
- PICC_AUTHENT1B = 0x61
618
-
619
- def __init__(self):
620
- self.auth_calls = []
621
-
622
- def MFRC522_Request(self, _):
623
- return (self.MI_OK, None)
624
-
625
- def MFRC522_Anticoll(self):
626
- return (self.MI_OK, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE])
627
-
628
- def MFRC522_Auth(self, mode, block, key, uid):
629
- self.auth_calls.append(mode)
630
- return self.MI_ERR if mode == self.PICC_AUTHENT1A else self.MI_OK
631
-
632
- def MFRC522_Read(self, block):
633
- return (self.MI_OK, [0] * 16)
634
-
635
- @patch("core.notifications.notify_async")
636
- @patch("core.models.RFID.objects.get_or_create")
637
- def test_auth_tries_key_a_then_b(self, mock_get, mock_notify):
638
- tag = MagicMock(
639
- label_id=1,
640
- pk=1,
641
- allowed=True,
642
- color="B",
643
- released=False,
644
- reference=None,
645
- )
646
- mock_get.return_value = (tag, False)
647
- reader = self.MockReader()
648
- enable_deep_read(60)
649
- read_rfid(mfrc=reader, cleanup=False)
650
- self.assertGreaterEqual(len(reader.auth_calls), 2)
651
- self.assertEqual(reader.auth_calls[0], reader.PICC_AUTHENT1A)
652
- self.assertEqual(reader.auth_calls[1], reader.PICC_AUTHENT1B)
653
-
654
-
655
- class RFIDWiringConfigTests(SimpleTestCase):
656
- def test_module_wiring_map(self):
657
- expected = [
658
- ("SDA", "CE0"),
659
- ("SCK", "SCLK"),
660
- ("MOSI", "MOSI"),
661
- ("MISO", "MISO"),
662
- ("IRQ", "IO4"),
663
- ("GND", "GND"),
664
- ("RST", "IO25"),
665
- ("3v3", "3v3"),
666
- ]
667
- self.assertEqual(list(MODULE_WIRING.items()), expected)
668
- self.assertEqual(DEFAULT_IRQ_PIN, 4)
669
- self.assertEqual(DEFAULT_RST_PIN, 25)
670
-
671
- def test_background_reader_uses_default_irq_pin(self):
672
- self.assertEqual(background_reader.IRQ_PIN, DEFAULT_IRQ_PIN)
673
-
674
- def test_reader_instantiation_uses_configured_pins(self):
675
- class DummyReader:
676
- init_args = None
677
- init_kwargs = None
678
-
679
- def __init__(self, *args, **kwargs):
680
- DummyReader.init_args = args
681
- DummyReader.init_kwargs = kwargs
682
- self.MI_OK = 1
683
- self.PICC_REQIDL = 0
684
-
685
- fake_mfrc = types.ModuleType("mfrc522")
686
- fake_mfrc.MFRC522 = DummyReader
687
- fake_gpio = types.ModuleType("RPi.GPIO")
688
- fake_rpi = types.ModuleType("RPi")
689
- fake_rpi.GPIO = fake_gpio
690
-
691
- with patch.dict(
692
- "sys.modules",
693
- {"mfrc522": fake_mfrc, "RPi": fake_rpi, "RPi.GPIO": fake_gpio},
694
- ):
695
- result = read_rfid(timeout=0, cleanup=False)
696
-
697
- self.assertEqual(result, {"rfid": None, "label_id": None})
698
- self.assertIsNotNone(DummyReader.init_kwargs)
699
- self.assertEqual(DummyReader.init_kwargs["bus"], SPI_BUS)
700
- self.assertEqual(DummyReader.init_kwargs["device"], SPI_DEVICE)
701
- self.assertEqual(DummyReader.init_kwargs["pin_mode"], GPIO_PIN_MODE_BCM)
702
- self.assertEqual(DummyReader.init_kwargs["pin_rst"], DEFAULT_RST_PIN)
1
+ import io
2
+ import json
3
+ import os
4
+ import sys
5
+ import types
6
+ from datetime import datetime, timezone as dt_timezone
7
+ from pathlib import Path
8
+ from unittest.mock import patch, MagicMock, call
9
+
10
+ import pytest
11
+
12
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
13
+
14
+ import django
15
+
16
+ django.setup()
17
+
18
+ from django.test import SimpleTestCase, TestCase
19
+ from django.urls import reverse
20
+ from django.contrib.auth import get_user_model
21
+ from django.contrib.sites.models import Site
22
+ from django.utils import timezone
23
+
24
+ from pages.models import Application, Module
25
+ from nodes.models import Node, NodeRole
26
+
27
+ from core.models import RFID
28
+ from ocpp.rfid.reader import read_rfid, enable_deep_read, validate_rfid_value
29
+ from ocpp.rfid.detect import detect_scanner, main as detect_main
30
+ from ocpp.rfid import background_reader, camera
31
+ from ocpp.rfid.constants import (
32
+ DEFAULT_IRQ_PIN,
33
+ DEFAULT_RST_PIN,
34
+ GPIO_PIN_MODE_BCM,
35
+ MODULE_WIRING,
36
+ SPI_BUS,
37
+ SPI_DEVICE,
38
+ )
39
+
40
+
41
+ pytestmark = [pytest.mark.feature("rfid-scanner")]
42
+
43
+
44
+ class BackgroundReaderConfigurationTests(SimpleTestCase):
45
+ def setUp(self):
46
+ background_reader._auto_detect_logged = False
47
+
48
+ def tearDown(self):
49
+ background_reader._auto_detect_logged = False
50
+
51
+ def test_is_configured_auto_detects_without_lock(self):
52
+ fake_lock = Path("/tmp/rfid-auto-detect.lock")
53
+ with (
54
+ patch("ocpp.rfid.background_reader._lock_path", return_value=fake_lock),
55
+ patch("ocpp.rfid.background_reader._has_spi_device", return_value=True),
56
+ patch(
57
+ "ocpp.rfid.background_reader._dependencies_available",
58
+ return_value=True,
59
+ ),
60
+ ):
61
+ self.assertTrue(background_reader.is_configured())
62
+
63
+ def test_is_configured_requires_dependencies(self):
64
+ fake_lock = Path("/tmp/rfid-auto-detect.lock")
65
+ with (
66
+ patch("ocpp.rfid.background_reader._lock_path", return_value=fake_lock),
67
+ patch("ocpp.rfid.background_reader._has_spi_device", return_value=True),
68
+ patch(
69
+ "ocpp.rfid.background_reader._dependencies_available",
70
+ return_value=False,
71
+ ),
72
+ ):
73
+ self.assertFalse(background_reader.is_configured())
74
+
75
+
76
+ class ScanNextViewTests(TestCase):
77
+ def setUp(self):
78
+ User = get_user_model()
79
+ self.user = User.objects.create_user("rfid-user", password="pwd")
80
+ self.client.force_login(self.user)
81
+
82
+ @patch("config.middleware.Node.get_local", return_value=None)
83
+ @patch("config.middleware.get_site")
84
+ @patch(
85
+ "ocpp.rfid.views.scan_sources",
86
+ return_value={
87
+ "rfid": "ABCD1234",
88
+ "label_id": 1,
89
+ "created": False,
90
+ "kind": RFID.CLASSIC,
91
+ },
92
+ )
93
+ def test_scan_next_success(self, mock_scan, mock_site, mock_node):
94
+ resp = self.client.get(reverse("rfid-scan-next"))
95
+ self.assertEqual(resp.status_code, 200)
96
+ self.assertEqual(
97
+ resp.json(),
98
+ {
99
+ "rfid": "ABCD1234",
100
+ "label_id": 1,
101
+ "created": False,
102
+ "kind": RFID.CLASSIC,
103
+ },
104
+ )
105
+
106
+ @patch("config.middleware.Node.get_local", return_value=None)
107
+ @patch("config.middleware.get_site")
108
+ @patch("ocpp.rfid.views.scan_sources", return_value={"error": "boom"})
109
+ def test_scan_next_error(self, mock_scan, mock_site, mock_node):
110
+ resp = self.client.get(reverse("rfid-scan-next"))
111
+ self.assertEqual(resp.status_code, 500)
112
+ self.assertEqual(resp.json(), {"error": "boom"})
113
+
114
+ @patch("config.middleware.Node.get_local", return_value=None)
115
+ @patch("config.middleware.get_site")
116
+ @patch(
117
+ "ocpp.rfid.views.validate_rfid_value",
118
+ return_value={"rfid": "ABCD1234", "label_id": 1, "created": False},
119
+ )
120
+ def test_scan_next_post_validates(self, mock_validate, mock_site, mock_node):
121
+ User = get_user_model()
122
+ user = User.objects.create_user("scanner", password="pwd")
123
+ self.client.force_login(user)
124
+ resp = self.client.post(
125
+ reverse("rfid-scan-next"),
126
+ data=json.dumps({"rfid": "ABCD1234"}),
127
+ content_type="application/json",
128
+ )
129
+ self.assertEqual(resp.status_code, 200)
130
+ self.assertEqual(
131
+ resp.json(), {"rfid": "ABCD1234", "label_id": 1, "created": False}
132
+ )
133
+ mock_validate.assert_called_once_with("ABCD1234", kind=None)
134
+
135
+ @patch("config.middleware.Node.get_local", return_value=None)
136
+ @patch("config.middleware.get_site")
137
+ @patch("ocpp.rfid.views.validate_rfid_value")
138
+ def test_scan_next_post_requires_authentication(
139
+ self, mock_validate, mock_site, mock_node
140
+ ):
141
+ self.client.logout()
142
+ resp = self.client.post(
143
+ reverse("rfid-scan-next"),
144
+ data=json.dumps({"rfid": "ABCD1234"}),
145
+ content_type="application/json",
146
+ )
147
+ self.assertEqual(resp.status_code, 401)
148
+ self.assertEqual(resp.json(), {"error": "Authentication required"})
149
+ mock_validate.assert_not_called()
150
+
151
+ @patch("config.middleware.Node.get_local", return_value=None)
152
+ @patch("config.middleware.get_site")
153
+ def test_scan_next_post_invalid_json(self, mock_site, mock_node):
154
+ User = get_user_model()
155
+ user = User.objects.create_user("invalid-json", password="pwd")
156
+ self.client.force_login(user)
157
+ resp = self.client.post(
158
+ reverse("rfid-scan-next"),
159
+ data="{",
160
+ content_type="application/json",
161
+ )
162
+ self.assertEqual(resp.status_code, 400)
163
+ self.assertEqual(resp.json(), {"error": "Invalid JSON payload"})
164
+
165
+ def test_scan_next_requires_authentication(self):
166
+ self.client.logout()
167
+ resp = self.client.get(reverse("rfid-scan-next"))
168
+ self.assertEqual(resp.status_code, 302)
169
+ self.assertIn(reverse("pages:login"), resp.url)
170
+
171
+
172
+ class ReaderNotificationTests(TestCase):
173
+ def setUp(self):
174
+ super().setUp()
175
+ self.queue_patcher = patch("ocpp.rfid.reader.queue_camera_snapshot")
176
+ self.mock_queue = self.queue_patcher.start()
177
+
178
+ def tearDown(self):
179
+ self.queue_patcher.stop()
180
+ super().tearDown()
181
+
182
+ def _mock_reader(self):
183
+ class MockReader:
184
+ MI_OK = 1
185
+ PICC_REQIDL = 0
186
+
187
+ def MFRC522_Request(self, _):
188
+ return (self.MI_OK, None)
189
+
190
+ def MFRC522_Anticoll(self):
191
+ return (self.MI_OK, [0xAB, 0xCD, 0x12, 0x34, 0x56])
192
+
193
+ def MFRC522_SelectTag(self, _uid):
194
+ self.select_called = True
195
+ return self.MI_OK
196
+
197
+ def MFRC522_StopCrypto1(self):
198
+ self.stop_called = True
199
+
200
+ return MockReader()
201
+
202
+ @patch("ocpp.rfid.reader.notify_async")
203
+ @patch("ocpp.rfid.reader.RFID.register_scan")
204
+ def test_notify_on_allowed_tag(self, mock_register, mock_notify):
205
+ reference = MagicMock(value="https://example.com")
206
+ tag = MagicMock(
207
+ label_id=1,
208
+ pk=1,
209
+ allowed=True,
210
+ color="B",
211
+ released=False,
212
+ reference=reference,
213
+ )
214
+ mock_register.return_value = (tag, False)
215
+
216
+ reader = self._mock_reader()
217
+ result = read_rfid(mfrc=reader, cleanup=False)
218
+ self.assertEqual(result["label_id"], 1)
219
+ self.assertEqual(result["kind"], RFID.CLASSIC)
220
+ self.assertEqual(result["reference"], "https://example.com")
221
+ self.assertEqual(mock_notify.call_count, 1)
222
+ mock_notify.assert_has_calls([call("RFID 1 OK", f"{result['rfid']} B")])
223
+ self.assertTrue(getattr(reader, "select_called", False))
224
+ self.assertTrue(getattr(reader, "stop_called", False))
225
+
226
+ @patch("ocpp.rfid.reader.notify_async")
227
+ @patch("ocpp.rfid.reader.RFID.register_scan")
228
+ def test_notify_on_disallowed_tag(self, mock_register, mock_notify):
229
+ tag = MagicMock(
230
+ label_id=2,
231
+ pk=2,
232
+ allowed=False,
233
+ color="B",
234
+ released=False,
235
+ reference=None,
236
+ )
237
+ mock_register.return_value = (tag, False)
238
+
239
+ reader = self._mock_reader()
240
+ result = read_rfid(mfrc=reader, cleanup=False)
241
+ self.assertEqual(result["kind"], RFID.CLASSIC)
242
+ self.assertEqual(mock_notify.call_count, 1)
243
+ mock_notify.assert_has_calls([call("RFID 2 BAD", f"{result['rfid']} B")])
244
+ self.assertTrue(getattr(reader, "select_called", False))
245
+ self.assertTrue(getattr(reader, "stop_called", False))
246
+
247
+ @patch("ocpp.rfid.reader.notify_async")
248
+ @patch("ocpp.rfid.reader.RFID.register_scan")
249
+ def test_snapshot_metadata_passed_to_queue(self, mock_register, mock_notify):
250
+ tag = MagicMock(
251
+ label_id=5,
252
+ pk=5,
253
+ allowed=True,
254
+ color="",
255
+ released=False,
256
+ reference=None,
257
+ )
258
+ mock_register.return_value = (tag, True)
259
+
260
+ reader = self._mock_reader()
261
+ result = read_rfid(mfrc=reader, cleanup=False)
262
+
263
+ self.assertTrue(self.mock_queue.called)
264
+ args, kwargs = self.mock_queue.call_args
265
+ self.assertEqual(args[0], result["rfid"])
266
+ self.assertEqual(args[1]["label_id"], tag.pk)
267
+ self.assertTrue(args[1]["created"])
268
+
269
+
270
+ class CameraSnapshotTriggerTests(SimpleTestCase):
271
+ @patch("ocpp.rfid.camera._camera_feature_enabled", return_value=False)
272
+ @patch("ocpp.rfid.camera.threading.Thread")
273
+ def test_queue_skips_without_feature(self, mock_thread, mock_enabled):
274
+ camera.queue_camera_snapshot("ABC123", {"label_id": 1})
275
+ mock_thread.assert_not_called()
276
+
277
+ @patch("ocpp.rfid.camera.timezone.now")
278
+ @patch("ocpp.rfid.camera.threading.Thread")
279
+ @patch("ocpp.rfid.camera._camera_feature_enabled", return_value=True)
280
+ def test_queue_spawns_thread_with_metadata(
281
+ self, mock_enabled, mock_thread, mock_now
282
+ ):
283
+ thread_instance = MagicMock()
284
+ mock_thread.return_value = thread_instance
285
+ mock_now.return_value = datetime(2023, 1, 1, tzinfo=dt_timezone.utc)
286
+
287
+ camera.queue_camera_snapshot("ABC123", {"label_id": 7})
288
+
289
+ mock_thread.assert_called_once()
290
+ kwargs = mock_thread.call_args.kwargs
291
+ self.assertTrue(kwargs.get("daemon"))
292
+ metadata = kwargs["args"][0]
293
+ self.assertEqual(metadata["rfid"], "ABC123")
294
+ self.assertEqual(metadata["label_id"], 7)
295
+ self.assertEqual(metadata["source"], "rfid-scan")
296
+ self.assertEqual(metadata["captured_at"], "2023-01-01T00:00:00+00:00")
297
+ thread_instance.start.assert_called_once()
298
+
299
+ @patch("ocpp.rfid.camera.close_old_connections")
300
+ @patch("ocpp.rfid.camera.save_screenshot")
301
+ @patch("ocpp.rfid.camera.capture_rpi_snapshot")
302
+ def test_worker_saves_snapshot(
303
+ self, mock_capture, mock_save, mock_close
304
+ ):
305
+ mock_capture.return_value = Path("/tmp/test.jpg")
306
+
307
+ camera._capture_snapshot_worker({"rfid": "ABC", "label_id": 3})
308
+
309
+ mock_capture.assert_called_once()
310
+ mock_save.assert_called_once()
311
+ _, kwargs = mock_save.call_args
312
+ self.assertEqual(kwargs["method"], "RFID_SCAN")
313
+ metadata = json.loads(kwargs["content"])
314
+ self.assertEqual(metadata["rfid"], "ABC")
315
+ self.assertEqual(metadata["label_id"], 3)
316
+ self.assertGreaterEqual(mock_close.call_count, 2)
317
+
318
+ @patch("ocpp.rfid.camera.close_old_connections")
319
+ @patch("ocpp.rfid.camera.save_screenshot")
320
+ @patch("ocpp.rfid.camera.capture_rpi_snapshot", side_effect=RuntimeError("boom"))
321
+ def test_worker_handles_capture_failure(
322
+ self, mock_capture, mock_save, mock_close
323
+ ):
324
+ camera._capture_snapshot_worker({"rfid": "XYZ"})
325
+ mock_save.assert_not_called()
326
+ self.assertGreaterEqual(mock_close.call_count, 2)
327
+
328
+
329
+ class ValidateRfidValueTests(SimpleTestCase):
330
+ @patch("ocpp.rfid.reader.timezone.now")
331
+ @patch("ocpp.rfid.reader.notify_async")
332
+ @patch("ocpp.rfid.reader.RFID.register_scan")
333
+ def test_creates_new_tag(self, mock_register, mock_notify, mock_now):
334
+ fake_now = object()
335
+ mock_now.return_value = fake_now
336
+ tag = MagicMock()
337
+ tag.pk = 1
338
+ tag.label_id = 1
339
+ tag.allowed = True
340
+ tag.color = "B"
341
+ tag.released = False
342
+ tag.reference = None
343
+ tag.kind = RFID.CLASSIC
344
+ mock_register.return_value = (tag, True)
345
+
346
+ result = validate_rfid_value("abcd1234")
347
+
348
+ mock_register.assert_called_once_with("ABCD1234", kind=None)
349
+ tag.save.assert_called_once_with(update_fields=["last_seen_on"])
350
+ self.assertIs(tag.last_seen_on, fake_now)
351
+ mock_notify.assert_called_once_with("RFID 1 OK", "ABCD1234 B")
352
+ self.assertTrue(result["created"])
353
+ self.assertEqual(result["rfid"], "ABCD1234")
354
+
355
+ @patch("ocpp.rfid.reader.timezone.now")
356
+ @patch("ocpp.rfid.reader.notify_async")
357
+ @patch("ocpp.rfid.reader.RFID.register_scan")
358
+ def test_updates_existing_tag_kind(self, mock_register, mock_notify, mock_now):
359
+ fake_now = object()
360
+ mock_now.return_value = fake_now
361
+ tag = MagicMock()
362
+ tag.pk = 5
363
+ tag.label_id = 5
364
+ tag.allowed = False
365
+ tag.color = "G"
366
+ tag.released = True
367
+ tag.reference = None
368
+ tag.kind = RFID.CLASSIC
369
+ mock_register.return_value = (tag, False)
370
+
371
+ result = validate_rfid_value("abcd", kind=RFID.NTAG215)
372
+
373
+ mock_register.assert_called_once_with("ABCD", kind=RFID.NTAG215)
374
+ tag.save.assert_called_once_with(update_fields=["kind", "last_seen_on"])
375
+ self.assertIs(tag.last_seen_on, fake_now)
376
+ self.assertEqual(tag.kind, RFID.NTAG215)
377
+ mock_notify.assert_called_once_with("RFID 5 BAD", "ABCD G")
378
+ self.assertFalse(result["allowed"])
379
+ self.assertFalse(result["created"])
380
+ self.assertEqual(result["kind"], RFID.NTAG215)
381
+
382
+ def test_rejects_invalid_value(self):
383
+ result = validate_rfid_value("invalid!")
384
+ self.assertEqual(result, {"error": "RFID must be hexadecimal digits"})
385
+
386
+ def test_rejects_non_string_values(self):
387
+ result = validate_rfid_value(12345)
388
+ self.assertEqual(result, {"error": "RFID must be a string"})
389
+
390
+ def test_rejects_missing_value(self):
391
+ result = validate_rfid_value(None)
392
+ self.assertEqual(result, {"error": "RFID value is required"})
393
+
394
+
395
+ @patch("ocpp.rfid.reader.timezone.now")
396
+ @patch("ocpp.rfid.reader.notify_async")
397
+ @patch("ocpp.rfid.reader.subprocess.run")
398
+ @patch("ocpp.rfid.reader.RFID.register_scan")
399
+ def test_external_command_success(
400
+ self, mock_register, mock_run, mock_notify, mock_now
401
+ ):
402
+ fake_now = object()
403
+ mock_now.return_value = fake_now
404
+ tag = MagicMock()
405
+ tag.pk = 1
406
+ tag.label_id = 1
407
+ tag.allowed = True
408
+ tag.external_command = "echo ok"
409
+ tag.color = "B"
410
+ tag.released = False
411
+ tag.reference = None
412
+ tag.kind = RFID.CLASSIC
413
+ mock_register.return_value = (tag, False)
414
+ mock_run.return_value = types.SimpleNamespace(
415
+ returncode=0, stdout="ok\n", stderr=""
416
+ )
417
+
418
+ result = validate_rfid_value("abcd1234")
419
+
420
+ mock_run.assert_called_once()
421
+ run_args, run_kwargs = mock_run.call_args
422
+ self.assertEqual(run_args[0], "echo ok")
423
+ self.assertTrue(run_kwargs.get("shell"))
424
+ env = run_kwargs.get("env", {})
425
+ self.assertEqual(env.get("RFID_VALUE"), "ABCD1234")
426
+ self.assertEqual(env.get("RFID_LABEL_ID"), "1")
427
+ mock_notify.assert_called_once_with("RFID 1 OK", "ABCD1234 B")
428
+ tag.save.assert_called_once_with(update_fields=["last_seen_on"])
429
+ self.assertTrue(result["allowed"])
430
+ output = result.get("command_output")
431
+ self.assertIsInstance(output, dict)
432
+ self.assertEqual(output.get("stdout"), "ok\n")
433
+ self.assertEqual(output.get("stderr"), "")
434
+ self.assertEqual(output.get("returncode"), 0)
435
+ self.assertEqual(output.get("error"), "")
436
+
437
+ @patch("ocpp.rfid.reader.timezone.now")
438
+ @patch("ocpp.rfid.reader.notify_async")
439
+ @patch("ocpp.rfid.reader.subprocess.run")
440
+ @patch("ocpp.rfid.reader.RFID.register_scan")
441
+ def test_external_command_failure_blocks_tag(
442
+ self, mock_register, mock_run, mock_notify, mock_now
443
+ ):
444
+ fake_now = object()
445
+ mock_now.return_value = fake_now
446
+ tag = MagicMock()
447
+ tag.pk = 2
448
+ tag.label_id = 2
449
+ tag.allowed = True
450
+ tag.external_command = "exit 1"
451
+ tag.color = "G"
452
+ tag.released = False
453
+ tag.reference = None
454
+ tag.kind = RFID.CLASSIC
455
+ mock_register.return_value = (tag, False)
456
+ mock_run.return_value = types.SimpleNamespace(
457
+ returncode=1, stdout="", stderr="failure"
458
+ )
459
+
460
+ result = validate_rfid_value("ffff")
461
+
462
+ mock_run.assert_called_once()
463
+ mock_notify.assert_called_once_with("RFID 2 BAD", "FFFF G")
464
+ tag.save.assert_called_once_with(update_fields=["last_seen_on"])
465
+ self.assertFalse(result["allowed"])
466
+ output = result.get("command_output")
467
+ self.assertIsInstance(output, dict)
468
+ self.assertEqual(output.get("returncode"), 1)
469
+ self.assertEqual(output.get("stdout"), "")
470
+ self.assertEqual(output.get("stderr"), "failure")
471
+ self.assertEqual(output.get("error"), "")
472
+
473
+
474
+ class CardTypeDetectionTests(TestCase):
475
+ def _mock_ntag_reader(self):
476
+ class MockReader:
477
+ MI_OK = 1
478
+ PICC_REQIDL = 0
479
+
480
+ def MFRC522_Request(self, _):
481
+ return (self.MI_OK, None)
482
+
483
+ def MFRC522_Anticoll(self):
484
+ return (
485
+ self.MI_OK,
486
+ [0x04, 0xD3, 0x2A, 0x1B, 0x5F, 0x23, 0x19],
487
+ )
488
+
489
+ def MFRC522_SelectTag(self, _uid):
490
+ self.select_called = True
491
+ return self.MI_OK
492
+
493
+ def MFRC522_StopCrypto1(self):
494
+ self.stop_called = True
495
+
496
+ return MockReader()
497
+
498
+ @patch("ocpp.rfid.reader.notify_async")
499
+ @patch("ocpp.rfid.reader.RFID.register_scan")
500
+ def test_detects_ntag215(self, mock_register, _mock_notify):
501
+ tag = MagicMock(
502
+ pk=1,
503
+ label_id=1,
504
+ allowed=True,
505
+ color="B",
506
+ released=False,
507
+ reference=None,
508
+ kind=RFID.NTAG215,
509
+ )
510
+ mock_register.return_value = (tag, True)
511
+ reader = self._mock_ntag_reader()
512
+ result = read_rfid(mfrc=reader, cleanup=False)
513
+ self.assertEqual(result["kind"], RFID.NTAG215)
514
+ self.assertTrue(getattr(reader, "select_called", False))
515
+ self.assertTrue(getattr(reader, "stop_called", False))
516
+
517
+
518
+ class RFIDLastSeenTests(TestCase):
519
+ def _mock_reader(self):
520
+ class MockReader:
521
+ MI_OK = 1
522
+ PICC_REQIDL = 0
523
+
524
+ def MFRC522_Request(self, _):
525
+ return (self.MI_OK, None)
526
+
527
+ def MFRC522_Anticoll(self):
528
+ return (self.MI_OK, [0xAB, 0xCD, 0x12, 0x34])
529
+
530
+ def MFRC522_SelectTag(self, _uid):
531
+ self.select_called = True
532
+ return self.MI_OK
533
+
534
+ def MFRC522_StopCrypto1(self):
535
+ self.stop_called = True
536
+
537
+ return MockReader()
538
+
539
+ @patch("ocpp.rfid.reader.notify_async")
540
+ def test_last_seen_updated_on_read(self, _mock_notify):
541
+ tag = RFID.objects.create(rfid="ABCD1234")
542
+ reader = self._mock_reader()
543
+ result = read_rfid(mfrc=reader, cleanup=False)
544
+ tag.refresh_from_db()
545
+ self.assertIsNotNone(tag.last_seen_on)
546
+ self.assertEqual(result["kind"], RFID.CLASSIC)
547
+ self.assertTrue(getattr(reader, "select_called", False))
548
+ self.assertTrue(getattr(reader, "stop_called", False))
549
+
550
+
551
+ class RFIDDetectionScriptTests(SimpleTestCase):
552
+ @patch("ocpp.rfid.detect._ensure_django")
553
+ @patch(
554
+ "ocpp.rfid.irq_wiring_check.check_irq_pin",
555
+ return_value={"irq_pin": DEFAULT_IRQ_PIN},
556
+ )
557
+ def test_detect_scanner_success(self, mock_check, _mock_setup):
558
+ result = detect_scanner()
559
+ self.assertEqual(
560
+ result,
561
+ {
562
+ "detected": True,
563
+ "irq_pin": DEFAULT_IRQ_PIN,
564
+ },
565
+ )
566
+ mock_check.assert_called_once()
567
+
568
+ @patch("ocpp.rfid.detect._ensure_django")
569
+ @patch(
570
+ "ocpp.rfid.irq_wiring_check.check_irq_pin",
571
+ return_value={"error": "no scanner detected"},
572
+ )
573
+ def test_detect_scanner_failure(self, mock_check, _mock_setup):
574
+ result = detect_scanner()
575
+ self.assertFalse(result["detected"])
576
+ self.assertEqual(result["reason"], "no scanner detected")
577
+ mock_check.assert_called_once()
578
+
579
+ @patch(
580
+ "ocpp.rfid.detect.detect_scanner",
581
+ return_value={"detected": True, "irq_pin": DEFAULT_IRQ_PIN},
582
+ )
583
+ def test_detect_main_success_output(self, mock_detect):
584
+ buffer = io.StringIO()
585
+ with patch("sys.stdout", new=buffer):
586
+ exit_code = detect_main([])
587
+ self.assertEqual(exit_code, 0)
588
+ self.assertIn("IRQ pin", buffer.getvalue())
589
+ mock_detect.assert_called_once()
590
+
591
+ @patch(
592
+ "ocpp.rfid.detect.detect_scanner",
593
+ return_value={"detected": False, "reason": "missing hardware"},
594
+ )
595
+ def test_detect_main_failure_output(self, mock_detect):
596
+ buffer = io.StringIO()
597
+ with patch("sys.stdout", new=buffer):
598
+ exit_code = detect_main([])
599
+ self.assertEqual(exit_code, 1)
600
+ self.assertIn("missing hardware", buffer.getvalue())
601
+ mock_detect.assert_called_once()
602
+
603
+
604
+ class RFIDLandingTests(TestCase):
605
+ def test_scanner_view_registered_as_landing(self):
606
+ role, _ = NodeRole.objects.get_or_create(name="Terminal")
607
+ Node.objects.update_or_create(
608
+ mac_address=Node.get_current_mac(),
609
+ defaults={"hostname": "localhost", "address": "127.0.0.1", "role": role},
610
+ )
611
+ Site.objects.update_or_create(
612
+ id=1, defaults={"domain": "testserver", "name": ""}
613
+ )
614
+ app = Application.objects.create(name="Ocpp")
615
+ module = Module.objects.create(node_role=role, application=app, path="/ocpp/")
616
+ module.create_landings()
617
+ self.assertTrue(module.landings.filter(path="/ocpp/rfid/").exists())
618
+
619
+
620
+ class ScannerTemplateTests(TestCase):
621
+ def setUp(self):
622
+ self.url = reverse("rfid-reader")
623
+ User = get_user_model()
624
+ self.user = User.objects.create_user("scanner-user", password="pwd")
625
+
626
+ def test_configure_link_for_staff(self):
627
+ User = get_user_model()
628
+ staff = User.objects.create_user("staff", password="pwd", is_staff=True)
629
+ self.client.force_login(staff)
630
+ resp = self.client.get(self.url)
631
+ self.assertContains(resp, 'id="rfid-configure"')
632
+ self.assertContains(resp, 'id="rfid-connect-local"')
633
+ self.assertNotContains(resp, 'Restart & Test Scanner')
634
+
635
+ def test_redirect_for_anonymous(self):
636
+ self.client.logout()
637
+ resp = self.client.get(self.url)
638
+ self.assertEqual(resp.status_code, 302)
639
+ self.assertIn(reverse("pages:login"), resp.url)
640
+
641
+ def test_advanced_fields_for_staff(self):
642
+ User = get_user_model()
643
+ staff = User.objects.create_user("staff2", password="pwd", is_staff=True)
644
+ self.client.force_login(staff)
645
+ resp = self.client.get(self.url)
646
+ self.assertContains(resp, 'id="rfid-kind"')
647
+ self.assertContains(resp, 'id="rfid-rfid"')
648
+ self.assertContains(resp, 'id="rfid-released"')
649
+ self.assertContains(resp, 'id="rfid-reference"')
650
+ self.assertContains(resp, 'id="rfid-deep-details"')
651
+
652
+ def test_basic_fields_for_authenticated_user(self):
653
+ self.client.logout()
654
+ self.client.force_login(self.user)
655
+ resp = self.client.get(self.url)
656
+ self.assertContains(resp, 'id="rfid-kind"')
657
+ self.assertNotContains(resp, 'id="rfid-connect-local"')
658
+ self.assertNotContains(resp, 'id="rfid-rfid"')
659
+ self.assertNotContains(resp, 'id="rfid-released"')
660
+ self.assertNotContains(resp, 'id="rfid-reference"')
661
+ self.assertNotContains(resp, 'id="rfid-deep-details"')
662
+ self.assertNotContains(resp, 'Restart & Test Scanner')
663
+
664
+ def test_deep_read_button_for_staff(self):
665
+ User = get_user_model()
666
+ staff = User.objects.create_user("staff3", password="pwd", is_staff=True)
667
+ self.client.force_login(staff)
668
+ resp = self.client.get(self.url)
669
+ self.assertContains(resp, 'id="rfid-deep-read"')
670
+
671
+ def test_no_deep_read_button_for_authenticated_user(self):
672
+ self.client.logout()
673
+ self.client.force_login(self.user)
674
+ resp = self.client.get(self.url)
675
+ self.assertNotContains(resp, 'id="rfid-deep-read"')
676
+
677
+
678
+ class ReaderPollingTests(SimpleTestCase):
679
+ def _mock_reader_no_tag(self):
680
+ class MockReader:
681
+ MI_OK = 1
682
+ PICC_REQIDL = 0
683
+
684
+ def MFRC522_Request(self, _):
685
+ return (0, None)
686
+
687
+ return MockReader()
688
+
689
+ @patch("ocpp.rfid.reader.time.sleep")
690
+ def test_poll_interval_used(self, mock_sleep):
691
+ read_rfid(
692
+ mfrc=self._mock_reader_no_tag(),
693
+ cleanup=False,
694
+ timeout=0.002,
695
+ poll_interval=0.001,
696
+ )
697
+ mock_sleep.assert_called_with(0.001)
698
+
699
+ @patch("ocpp.rfid.reader.time.sleep")
700
+ def test_use_irq_skips_sleep(self, mock_sleep):
701
+ read_rfid(
702
+ mfrc=self._mock_reader_no_tag(),
703
+ cleanup=False,
704
+ timeout=0.002,
705
+ use_irq=True,
706
+ )
707
+ mock_sleep.assert_not_called()
708
+
709
+
710
+ class DeepReadViewTests(TestCase):
711
+ @patch("config.middleware.Node.get_local", return_value=None)
712
+ @patch("config.middleware.get_site")
713
+ @patch(
714
+ "ocpp.rfid.views.enable_deep_read_mode",
715
+ return_value={"status": "deep read enabled", "enabled": True},
716
+ )
717
+ def test_enable_deep_read(self, mock_enable, mock_site, mock_node):
718
+ User = get_user_model()
719
+ staff = User.objects.create_user("staff4", password="pwd", is_staff=True)
720
+ self.client.force_login(staff)
721
+ resp = self.client.post(reverse("rfid-scan-deep"))
722
+ self.assertEqual(resp.status_code, 200)
723
+ self.assertEqual(
724
+ resp.json(), {"status": "deep read enabled", "enabled": True}
725
+ )
726
+ mock_enable.assert_called_once()
727
+
728
+ def test_forbidden_for_anonymous(self):
729
+ resp = self.client.post(reverse("rfid-scan-deep"))
730
+ self.assertNotEqual(resp.status_code, 200)
731
+
732
+
733
+ class DeepReadAuthTests(TestCase):
734
+ def setUp(self):
735
+ super().setUp()
736
+ self.queue_patcher = patch("ocpp.rfid.reader.queue_camera_snapshot")
737
+ self.queue_patcher.start()
738
+
739
+ def tearDown(self):
740
+ self.queue_patcher.stop()
741
+ super().tearDown()
742
+
743
+ class MockReader:
744
+ MI_OK = 1
745
+ MI_ERR = 2
746
+ PICC_REQIDL = 0
747
+ PICC_AUTHENT1A = 0x60
748
+ PICC_AUTHENT1B = 0x61
749
+
750
+ def __init__(self):
751
+ self.auth_calls = []
752
+
753
+ def MFRC522_Request(self, _):
754
+ return (self.MI_OK, None)
755
+
756
+ def MFRC522_Anticoll(self):
757
+ return (self.MI_OK, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE])
758
+
759
+ def MFRC522_Auth(self, mode, block, key, uid):
760
+ self.auth_calls.append(mode)
761
+ return self.MI_ERR if mode == self.PICC_AUTHENT1A else self.MI_OK
762
+
763
+ def MFRC522_Read(self, block):
764
+ return (self.MI_OK, [0] * 16)
765
+
766
+ @patch("core.notifications.notify_async")
767
+ @patch("ocpp.rfid.reader.RFID.register_scan")
768
+ def test_auth_tries_key_a_then_b(self, mock_register, mock_notify):
769
+ tag = MagicMock(
770
+ label_id=1,
771
+ pk=1,
772
+ allowed=True,
773
+ color="B",
774
+ released=False,
775
+ reference=None,
776
+ )
777
+ tag.key_a = "A1A2A3A4A5A6"
778
+ tag.key_b = "B1B2B3B4B5B6"
779
+ tag.key_a_verified = True
780
+ tag.key_b_verified = False
781
+ tag.data = []
782
+ tag.save = MagicMock()
783
+ mock_register.return_value = (tag, False)
784
+ reader = self.MockReader()
785
+ enable_deep_read(60)
786
+ result = read_rfid(mfrc=reader, cleanup=False)
787
+ self.assertGreaterEqual(len(reader.auth_calls), 2)
788
+ self.assertEqual(reader.auth_calls[0], reader.PICC_AUTHENT1A)
789
+ self.assertEqual(reader.auth_calls[1], reader.PICC_AUTHENT1B)
790
+ self.assertTrue(result.get("deep_read"))
791
+ self.assertIn("dump", result)
792
+ self.assertIn("keys", result)
793
+ self.assertEqual(result["keys"].get("a"), "A1A2A3A4A5A6")
794
+ self.assertTrue(result["keys"].get("b_verified"))
795
+ self.assertTrue(tag.key_b_verified)
796
+ self.assertTrue(any(entry.get("key") == "B" for entry in result["dump"]))
797
+ self.assertEqual(tag.data, result["dump"])
798
+ self.assertTrue(
799
+ any(
800
+ "data" in set(kwargs.get("update_fields", []) or [])
801
+ for _args, kwargs in tag.save.call_args_list
802
+ )
803
+ )
804
+
805
+ @patch("core.notifications.notify_async")
806
+ @patch("ocpp.rfid.reader.RFID.register_scan")
807
+ def test_heuristic_verifies_unverified_keys(
808
+ self, mock_register, mock_notify
809
+ ):
810
+ tag = MagicMock(
811
+ label_id=1,
812
+ pk=1,
813
+ allowed=True,
814
+ color="B",
815
+ released=False,
816
+ reference=None,
817
+ )
818
+ tag.key_a = "111111111111"
819
+ tag.key_b = "FFFFFFFFFFFF"
820
+ tag.key_a_verified = False
821
+ tag.key_b_verified = True
822
+ tag.save = MagicMock()
823
+ mock_register.return_value = (tag, False)
824
+
825
+ class HeuristicReader:
826
+ MI_OK = 1
827
+ MI_ERR = 2
828
+ PICC_REQIDL = 0
829
+ PICC_AUTHENT1A = 0x60
830
+ PICC_AUTHENT1B = 0x61
831
+
832
+ def __init__(self):
833
+ self.auth_calls: list[tuple[int, str]] = []
834
+
835
+ def MFRC522_Request(self, _):
836
+ return (self.MI_OK, None)
837
+
838
+ def MFRC522_Anticoll(self):
839
+ return (self.MI_OK, [0x01, 0x02, 0x03, 0x04])
840
+
841
+ def MFRC522_Auth(self, mode, block, key, uid):
842
+ key_hex = "".join(f"{value:02X}" for value in key)
843
+ self.auth_calls.append((mode, key_hex))
844
+ if mode == self.PICC_AUTHENT1A and key_hex == "A0A1A2A3A4A5":
845
+ return self.MI_OK
846
+ return self.MI_ERR
847
+
848
+ def MFRC522_Read(self, block):
849
+ return (self.MI_OK, list(range(16)))
850
+
851
+ reader = HeuristicReader()
852
+ enable_deep_read(60)
853
+ result = read_rfid(mfrc=reader, cleanup=False)
854
+
855
+ self.assertIn((reader.PICC_AUTHENT1A, "A0A1A2A3A4A5"), reader.auth_calls)
856
+ self.assertEqual(result["keys"].get("a"), "A0A1A2A3A4A5")
857
+ self.assertTrue(result["keys"].get("a_verified"))
858
+ self.assertEqual(tag.key_a, "A0A1A2A3A4A5")
859
+ self.assertTrue(tag.key_a_verified)
860
+ self.assertIn(
861
+ call(update_fields=["key_a", "key_a_verified"]),
862
+ tag.save.call_args_list,
863
+ )
864
+
865
+
866
+ class RFIDWiringConfigTests(SimpleTestCase):
867
+ def test_module_wiring_map(self):
868
+ expected = [
869
+ ("SDA", "CE0"),
870
+ ("SCK", "SCLK"),
871
+ ("MOSI", "MOSI"),
872
+ ("MISO", "MISO"),
873
+ ("IRQ", "IO4"),
874
+ ("GND", "GND"),
875
+ ("RST", "IO25"),
876
+ ("3v3", "3v3"),
877
+ ]
878
+ self.assertEqual(list(MODULE_WIRING.items()), expected)
879
+ self.assertEqual(DEFAULT_IRQ_PIN, 4)
880
+ self.assertEqual(DEFAULT_RST_PIN, 25)
881
+
882
+ def test_background_reader_uses_default_irq_pin(self):
883
+ self.assertEqual(background_reader.IRQ_PIN, DEFAULT_IRQ_PIN)
884
+
885
+ def test_reader_instantiation_uses_configured_pins(self):
886
+ class DummyReader:
887
+ init_args = None
888
+ init_kwargs = None
889
+
890
+ def __init__(self, *args, **kwargs):
891
+ DummyReader.init_args = args
892
+ DummyReader.init_kwargs = kwargs
893
+ self.MI_OK = 1
894
+ self.PICC_REQIDL = 0
895
+
896
+ fake_mfrc = types.ModuleType("mfrc522")
897
+ fake_mfrc.MFRC522 = DummyReader
898
+ fake_gpio = types.ModuleType("RPi.GPIO")
899
+ fake_rpi = types.ModuleType("RPi")
900
+ fake_rpi.GPIO = fake_gpio
901
+
902
+ with patch.dict(
903
+ "sys.modules",
904
+ {"mfrc522": fake_mfrc, "RPi": fake_rpi, "RPi.GPIO": fake_gpio},
905
+ ):
906
+ result = read_rfid(timeout=0, cleanup=False)
907
+
908
+ self.assertEqual(result, {"rfid": None, "label_id": None})
909
+ self.assertIsNotNone(DummyReader.init_kwargs)
910
+ self.assertEqual(DummyReader.init_kwargs["bus"], SPI_BUS)
911
+ self.assertEqual(DummyReader.init_kwargs["device"], SPI_DEVICE)
912
+ self.assertEqual(DummyReader.init_kwargs["pin_mode"], GPIO_PIN_MODE_BCM)
913
+ self.assertEqual(DummyReader.init_kwargs["pin_rst"], DEFAULT_RST_PIN)