arthexis 0.1.3__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 (73) hide show
  1. arthexis-0.1.3.dist-info/METADATA +126 -0
  2. arthexis-0.1.3.dist-info/RECORD +73 -0
  3. arthexis-0.1.3.dist-info/WHEEL +5 -0
  4. arthexis-0.1.3.dist-info/licenses/LICENSE +21 -0
  5. arthexis-0.1.3.dist-info/top_level.txt +5 -0
  6. config/__init__.py +6 -0
  7. config/active_app.py +15 -0
  8. config/asgi.py +29 -0
  9. config/auth_app.py +8 -0
  10. config/celery.py +19 -0
  11. config/context_processors.py +68 -0
  12. config/loadenv.py +11 -0
  13. config/logging.py +43 -0
  14. config/middleware.py +25 -0
  15. config/offline.py +47 -0
  16. config/settings.py +374 -0
  17. config/urls.py +91 -0
  18. config/wsgi.py +17 -0
  19. core/__init__.py +0 -0
  20. core/admin.py +830 -0
  21. core/apps.py +67 -0
  22. core/backends.py +82 -0
  23. core/entity.py +97 -0
  24. core/environment.py +43 -0
  25. core/fields.py +70 -0
  26. core/lcd_screen.py +77 -0
  27. core/middleware.py +34 -0
  28. core/models.py +1277 -0
  29. core/notifications.py +95 -0
  30. core/release.py +451 -0
  31. core/system.py +111 -0
  32. core/tasks.py +100 -0
  33. core/tests.py +483 -0
  34. core/urls.py +11 -0
  35. core/user_data.py +333 -0
  36. core/views.py +431 -0
  37. nodes/__init__.py +0 -0
  38. nodes/actions.py +72 -0
  39. nodes/admin.py +347 -0
  40. nodes/apps.py +76 -0
  41. nodes/lcd.py +151 -0
  42. nodes/models.py +577 -0
  43. nodes/tasks.py +50 -0
  44. nodes/tests.py +1072 -0
  45. nodes/urls.py +13 -0
  46. nodes/utils.py +62 -0
  47. nodes/views.py +262 -0
  48. ocpp/__init__.py +0 -0
  49. ocpp/admin.py +392 -0
  50. ocpp/apps.py +24 -0
  51. ocpp/consumers.py +267 -0
  52. ocpp/evcs.py +911 -0
  53. ocpp/models.py +300 -0
  54. ocpp/routing.py +9 -0
  55. ocpp/simulator.py +357 -0
  56. ocpp/store.py +175 -0
  57. ocpp/tasks.py +27 -0
  58. ocpp/test_export_import.py +129 -0
  59. ocpp/test_rfid.py +345 -0
  60. ocpp/tests.py +1229 -0
  61. ocpp/transactions_io.py +119 -0
  62. ocpp/urls.py +17 -0
  63. ocpp/views.py +359 -0
  64. pages/__init__.py +0 -0
  65. pages/admin.py +231 -0
  66. pages/apps.py +10 -0
  67. pages/checks.py +41 -0
  68. pages/context_processors.py +72 -0
  69. pages/models.py +224 -0
  70. pages/tests.py +628 -0
  71. pages/urls.py +17 -0
  72. pages/utils.py +13 -0
  73. pages/views.py +191 -0
ocpp/tests.py ADDED
@@ -0,0 +1,1229 @@
1
+
2
+ from channels.testing import WebsocketCommunicator
3
+ from channels.db import database_sync_to_async
4
+ from django.test import Client, TransactionTestCase, TestCase
5
+ from unittest import skip
6
+ from unittest.mock import patch
7
+ from django.contrib.auth import get_user_model
8
+ from django.urls import reverse
9
+ from django.utils import timezone
10
+ from django.contrib.sites.models import Site
11
+ from pages.models import Application, Module
12
+ from nodes.models import Node, NodeRole
13
+
14
+ from config.asgi import application
15
+
16
+ from .models import Transaction, Charger, Simulator, MeterReading, Location
17
+ from core.models import EnergyAccount, EnergyCredit
18
+ from core.models import RFID
19
+ from . import store
20
+ from django.db.models.deletion import ProtectedError
21
+ from decimal import Decimal
22
+ import json
23
+ import websockets
24
+ import asyncio
25
+ from pathlib import Path
26
+ from .simulator import SimulatorConfig, ChargePointSimulator
27
+ import re
28
+ from datetime import timedelta
29
+ from .tasks import purge_meter_readings
30
+
31
+
32
+
33
+ class ChargerFixtureTests(TestCase):
34
+ fixtures = ["initial_data.json"]
35
+
36
+ def test_cp2_requires_rfid(self):
37
+ cp2 = Charger.objects.get(charger_id="CP2")
38
+ self.assertTrue(cp2.require_rfid)
39
+
40
+ def test_cp1_does_not_require_rfid(self):
41
+ cp1 = Charger.objects.get(charger_id="CP1")
42
+ self.assertFalse(cp1.require_rfid)
43
+
44
+ def test_charger_connector_ids(self):
45
+ cp1 = Charger.objects.get(charger_id="CP1")
46
+ cp2 = Charger.objects.get(charger_id="CP2")
47
+ self.assertEqual(cp1.connector_id, "1")
48
+ self.assertEqual(cp2.connector_id, "2")
49
+ self.assertEqual(cp1.name, "Simulator #1")
50
+ self.assertEqual(cp2.name, "Simulator #2")
51
+
52
+
53
+ class SinkConsumerTests(TransactionTestCase):
54
+ async def test_sink_replies(self):
55
+ communicator = WebsocketCommunicator(application, "/ws/sink/")
56
+ connected, _ = await communicator.connect()
57
+ self.assertTrue(connected)
58
+
59
+ await communicator.send_json_to([2, "1", "Foo", {}])
60
+ response = await communicator.receive_json_from()
61
+ self.assertEqual(response, [3, "1", {}])
62
+
63
+ await communicator.disconnect()
64
+
65
+
66
+ class CSMSConsumerTests(TransactionTestCase):
67
+ async def test_transaction_saved(self):
68
+ communicator = WebsocketCommunicator(application, "/TEST/")
69
+ connected, _ = await communicator.connect()
70
+ self.assertTrue(connected)
71
+
72
+ await communicator.send_json_to([
73
+ 2,
74
+ "1",
75
+ "StartTransaction",
76
+ {"meterStart": 10},
77
+ ])
78
+ response = await communicator.receive_json_from()
79
+ tx_id = response[2]["transactionId"]
80
+
81
+ tx = await database_sync_to_async(Transaction.objects.get)(
82
+ pk=tx_id, charger__charger_id="TEST"
83
+ )
84
+ self.assertEqual(tx.meter_start, 10)
85
+ self.assertIsNone(tx.stop_time)
86
+
87
+ await communicator.send_json_to([
88
+ 2,
89
+ "2",
90
+ "StopTransaction",
91
+ {"transactionId": tx_id, "meterStop": 20},
92
+ ])
93
+ await communicator.receive_json_from()
94
+
95
+ await database_sync_to_async(tx.refresh_from_db)()
96
+ self.assertEqual(tx.meter_stop, 20)
97
+ self.assertIsNotNone(tx.stop_time)
98
+
99
+ await communicator.disconnect()
100
+
101
+ async def test_rfid_recorded(self):
102
+ await database_sync_to_async(Charger.objects.create)(charger_id="RFIDREC")
103
+ communicator = WebsocketCommunicator(application, "/RFIDREC/")
104
+ connected, _ = await communicator.connect()
105
+ self.assertTrue(connected)
106
+
107
+ await communicator.send_json_to(
108
+ [2, "1", "StartTransaction", {"meterStart": 1, "idTag": "TAG123"}]
109
+ )
110
+ response = await communicator.receive_json_from()
111
+ tx_id = response[2]["transactionId"]
112
+
113
+ tx = await database_sync_to_async(Transaction.objects.get)(
114
+ pk=tx_id, charger__charger_id="RFIDREC"
115
+ )
116
+ self.assertEqual(tx.rfid, "TAG123")
117
+
118
+ await communicator.disconnect()
119
+
120
+ async def test_vin_recorded(self):
121
+ await database_sync_to_async(Charger.objects.create)(charger_id="VINREC")
122
+ communicator = WebsocketCommunicator(application, "/VINREC/")
123
+ connected, _ = await communicator.connect()
124
+ self.assertTrue(connected)
125
+
126
+ await communicator.send_json_to(
127
+ [2, "1", "StartTransaction", {"meterStart": 1, "vin": "WP0ZZZ11111111111"}]
128
+ )
129
+ response = await communicator.receive_json_from()
130
+ tx_id = response[2]["transactionId"]
131
+
132
+ tx = await database_sync_to_async(Transaction.objects.get)(
133
+ pk=tx_id, charger__charger_id="VINREC"
134
+ )
135
+ self.assertEqual(tx.vin, "WP0ZZZ11111111111")
136
+
137
+ await communicator.disconnect()
138
+
139
+ async def test_connector_id_set_from_meter_values(self):
140
+ communicator = WebsocketCommunicator(application, "/NEWCID/")
141
+ connected, _ = await communicator.connect()
142
+ self.assertTrue(connected)
143
+
144
+ payload = {
145
+ "connectorId": 7,
146
+ "meterValue": [
147
+ {
148
+ "timestamp": timezone.now().isoformat(),
149
+ "sampledValue": [{"value": "1"}],
150
+ }
151
+ ],
152
+ }
153
+ await communicator.send_json_to([2, "1", "MeterValues", payload])
154
+ await communicator.receive_json_from()
155
+
156
+ charger = await database_sync_to_async(Charger.objects.get)(charger_id="NEWCID")
157
+ self.assertEqual(charger.connector_id, "7")
158
+
159
+ await communicator.disconnect()
160
+
161
+ async def test_transaction_created_from_meter_values(self):
162
+ communicator = WebsocketCommunicator(application, "/NOSTART/")
163
+ connected, _ = await communicator.connect()
164
+ self.assertTrue(connected)
165
+
166
+ await communicator.send_json_to(
167
+ [
168
+ 2,
169
+ "1",
170
+ "MeterValues",
171
+ {
172
+ "transactionId": 99,
173
+ "meterValue": [
174
+ {
175
+ "timestamp": "2025-01-01T00:00:00Z",
176
+ "sampledValue": [
177
+ {
178
+ "value": "1000",
179
+ "measurand": "Energy.Active.Import.Register",
180
+ "unit": "W",
181
+ }
182
+ ],
183
+ }
184
+ ],
185
+ },
186
+ ]
187
+ )
188
+ await communicator.receive_json_from()
189
+
190
+ tx = await database_sync_to_async(Transaction.objects.get)(
191
+ pk=99, charger__charger_id="NOSTART"
192
+ )
193
+ self.assertEqual(tx.meter_start, 1000)
194
+ self.assertIsNone(tx.meter_stop)
195
+
196
+ await communicator.send_json_to(
197
+ [
198
+ 2,
199
+ "2",
200
+ "StopTransaction",
201
+ {"transactionId": 99, "meterStop": 1500},
202
+ ]
203
+ )
204
+ await communicator.receive_json_from()
205
+ await database_sync_to_async(tx.refresh_from_db)()
206
+ self.assertEqual(tx.meter_stop, 1500)
207
+ self.assertIsNotNone(tx.stop_time)
208
+
209
+ await communicator.disconnect()
210
+
211
+ async def test_temperature_recorded(self):
212
+ charger = await database_sync_to_async(Charger.objects.create)(
213
+ charger_id="TEMP1"
214
+ )
215
+ communicator = WebsocketCommunicator(application, "/TEMP1/")
216
+ connected, _ = await communicator.connect()
217
+ self.assertTrue(connected)
218
+
219
+ await communicator.send_json_to(
220
+ [
221
+ 2,
222
+ "1",
223
+ "MeterValues",
224
+ {
225
+ "meterValue": [
226
+ {
227
+ "timestamp": "2025-01-01T00:00:00Z",
228
+ "sampledValue": [
229
+ {
230
+ "value": "42",
231
+ "measurand": "Temperature",
232
+ "unit": "Celsius",
233
+ }
234
+ ],
235
+ }
236
+ ]
237
+ },
238
+ ]
239
+ )
240
+ await communicator.receive_json_from()
241
+
242
+ await database_sync_to_async(charger.refresh_from_db)()
243
+ self.assertEqual(charger.temperature, Decimal("42"))
244
+ self.assertEqual(charger.temperature_unit, "Celsius")
245
+
246
+ await communicator.disconnect()
247
+
248
+ async def test_message_logged_and_session_file_created(self):
249
+ cid = "LOGTEST1"
250
+ log_path = Path("logs") / f"charger.{cid}.log"
251
+ if log_path.exists():
252
+ log_path.unlink()
253
+ session_dir = Path("logs") / "sessions" / cid
254
+ if session_dir.exists():
255
+ for f in session_dir.glob("*.json"):
256
+ f.unlink()
257
+ communicator = WebsocketCommunicator(application, f"/{cid}/")
258
+ connected, _ = await communicator.connect()
259
+ self.assertTrue(connected)
260
+
261
+ await communicator.send_json_to([
262
+ 2,
263
+ "1",
264
+ "StartTransaction",
265
+ {"meterStart": 1},
266
+ ])
267
+ response = await communicator.receive_json_from()
268
+ tx_id = response[2]["transactionId"]
269
+
270
+ await communicator.send_json_to([
271
+ 2,
272
+ "2",
273
+ "StopTransaction",
274
+ {"transactionId": tx_id, "meterStop": 2},
275
+ ])
276
+ await communicator.receive_json_from()
277
+ await communicator.disconnect()
278
+
279
+ content = log_path.read_text()
280
+ self.assertIn("StartTransaction", content)
281
+ self.assertNotIn(">", content)
282
+
283
+ files = list(session_dir.glob(f"*_{tx_id}.json"))
284
+ self.assertEqual(len(files), 1)
285
+ data = json.loads(files[0].read_text())
286
+ self.assertTrue(any("StartTransaction" in m["message"] for m in data))
287
+
288
+ async def test_binary_message_logged(self):
289
+ cid = "BINARY1"
290
+ log_path = Path("logs") / f"charger.{cid}.log"
291
+ if log_path.exists():
292
+ log_path.unlink()
293
+ communicator = WebsocketCommunicator(application, f"/{cid}/")
294
+ connected, _ = await communicator.connect()
295
+ self.assertTrue(connected)
296
+
297
+ await communicator.send_to(bytes_data=b"\x01\x02\x03")
298
+ await communicator.disconnect()
299
+
300
+ content = log_path.read_text()
301
+ self.assertIn("AQID", content)
302
+
303
+ async def test_session_file_written_on_disconnect(self):
304
+ cid = "LOGTEST2"
305
+ log_path = Path("logs") / f"charger.{cid}.log"
306
+ if log_path.exists():
307
+ log_path.unlink()
308
+ session_dir = Path("logs") / "sessions" / cid
309
+ if session_dir.exists():
310
+ for f in session_dir.glob("*.json"):
311
+ f.unlink()
312
+ communicator = WebsocketCommunicator(application, f"/{cid}/")
313
+ connected, _ = await communicator.connect()
314
+ self.assertTrue(connected)
315
+
316
+ await communicator.send_json_to([
317
+ 2,
318
+ "1",
319
+ "StartTransaction",
320
+ {"meterStart": 5},
321
+ ])
322
+ await communicator.receive_json_from()
323
+
324
+ await communicator.disconnect()
325
+
326
+ session_dir = Path("logs") / "sessions" / cid
327
+ files = list(session_dir.glob("*.json"))
328
+ self.assertEqual(len(files), 1)
329
+ data = json.loads(files[0].read_text())
330
+ self.assertTrue(any("StartTransaction" in m["message"] for m in data))
331
+
332
+ async def test_second_connection_closes_first(self):
333
+ communicator1 = WebsocketCommunicator(application, "/DUPLICATE/")
334
+ connected, _ = await communicator1.connect()
335
+ self.assertTrue(connected)
336
+ first_consumer = store.connections.get("DUPLICATE")
337
+
338
+ communicator2 = WebsocketCommunicator(application, "/DUPLICATE/")
339
+ connected2, _ = await communicator2.connect()
340
+ self.assertTrue(connected2)
341
+
342
+ # The first communicator should be closed when the second connects.
343
+ await communicator1.wait()
344
+ self.assertIsNot(store.connections.get("DUPLICATE"), first_consumer)
345
+
346
+ await communicator2.disconnect()
347
+
348
+
349
+ class ChargerLandingTests(TestCase):
350
+ def setUp(self):
351
+ self.client = Client()
352
+ User = get_user_model()
353
+ self.user = User.objects.create_user(username="u", password="pwd")
354
+ self.client.force_login(self.user)
355
+
356
+ def test_reference_created_and_page_renders(self):
357
+ charger = Charger.objects.create(charger_id="PAGE1")
358
+ self.assertIsNotNone(charger.reference)
359
+
360
+ response = self.client.get(reverse("charger-page", args=["PAGE1"]))
361
+ self.assertEqual(response.status_code, 200)
362
+ self.assertContains(
363
+ response,
364
+ "Plug in your vehicle and slide your RFID card over the reader to begin charging.",
365
+ )
366
+
367
+ def test_status_page_renders(self):
368
+ charger = Charger.objects.create(charger_id="PAGE2")
369
+ resp = self.client.get(reverse("charger-status", args=["PAGE2"]))
370
+ self.assertEqual(resp.status_code, 200)
371
+ self.assertContains(resp, "PAGE2")
372
+
373
+ def test_charger_page_shows_progress(self):
374
+ charger = Charger.objects.create(charger_id="STATS")
375
+ tx = Transaction.objects.create(
376
+ charger=charger,
377
+ meter_start=1000,
378
+ start_time=timezone.now(),
379
+ )
380
+ store.transactions[charger.charger_id] = tx
381
+ resp = self.client.get(reverse("charger-page", args=["STATS"]))
382
+ self.assertContains(resp, "progress")
383
+ store.transactions.pop(charger.charger_id, None)
384
+
385
+ def test_total_includes_ongoing_transaction(self):
386
+ charger = Charger.objects.create(charger_id="ONGOING")
387
+ tx = Transaction.objects.create(
388
+ charger=charger,
389
+ meter_start=1000,
390
+ start_time=timezone.now(),
391
+ )
392
+ store.transactions[charger.charger_id] = tx
393
+ MeterReading.objects.create(
394
+ charger=charger,
395
+ transaction=tx,
396
+ timestamp=timezone.now(),
397
+ measurand="Energy.Active.Import.Register",
398
+ value=Decimal("2500"),
399
+ unit="W",
400
+ )
401
+ resp = self.client.get(reverse("charger-status", args=["ONGOING"]))
402
+ self.assertContains(
403
+ resp, 'Total Energy: <span id="total-kw">1.50</span> kW'
404
+ )
405
+ store.transactions.pop(charger.charger_id, None)
406
+
407
+ def test_temperature_displayed(self):
408
+ charger = Charger.objects.create(
409
+ charger_id="TEMP2", temperature=Decimal("21.5"), temperature_unit="Celsius"
410
+ )
411
+ resp = self.client.get(reverse("charger-status", args=["TEMP2"]))
412
+ self.assertContains(resp, "Temperature")
413
+ self.assertContains(resp, "21.5")
414
+
415
+ def test_log_page_renders_without_charger(self):
416
+ store.add_log("LOG1", "hello", log_type="charger")
417
+ entry = store.get_logs("LOG1", log_type="charger")[0]
418
+ self.assertRegex(entry, r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} hello$")
419
+ resp = self.client.get(reverse("charger-log", args=["LOG1"]) + "?type=charger")
420
+ self.assertEqual(resp.status_code, 200)
421
+ self.assertContains(resp, "hello")
422
+ store.clear_log("LOG1", log_type="charger")
423
+
424
+ def test_log_page_is_case_insensitive(self):
425
+ store.add_log("cp2", "entry", log_type="charger")
426
+ resp = self.client.get(reverse("charger-log", args=["CP2"]) + "?type=charger")
427
+ self.assertEqual(resp.status_code, 200)
428
+ self.assertContains(resp, "entry")
429
+ store.clear_log("cp2", log_type="charger")
430
+
431
+
432
+ class SimulatorLandingTests(TestCase):
433
+ def setUp(self):
434
+ role, _ = NodeRole.objects.get_or_create(name="Terminal")
435
+ Node.objects.update_or_create(
436
+ mac_address=Node.get_current_mac(),
437
+ defaults={"hostname": "localhost", "address": "127.0.0.1", "role": role},
438
+ )
439
+ Site.objects.update_or_create(
440
+ id=1, defaults={"domain": "testserver", "name": ""}
441
+ )
442
+ app = Application.objects.create(name="Ocpp")
443
+ module = Module.objects.create(node_role=role, application=app, path="/ocpp/")
444
+ module.create_landings()
445
+ User = get_user_model()
446
+ self.user = User.objects.create_user(username="nav", password="pwd")
447
+ self.client = Client()
448
+
449
+ @skip("Navigation links unavailable in test environment")
450
+ def test_simulator_app_link_in_nav(self):
451
+ resp = self.client.get(reverse("pages:index"))
452
+ self.assertContains(resp, "/ocpp/")
453
+ self.assertNotContains(resp, "/ocpp/simulator/")
454
+ self.client.force_login(self.user)
455
+ resp = self.client.get(reverse("pages:index"))
456
+ self.assertContains(resp, "/ocpp/")
457
+ self.assertContains(resp, "/ocpp/simulator/")
458
+
459
+
460
+ class ChargerAdminTests(TestCase):
461
+ def setUp(self):
462
+ self.client = Client()
463
+ User = get_user_model()
464
+ self.admin = User.objects.create_superuser(
465
+ username="ocpp-admin", password="secret", email="admin@example.com"
466
+ )
467
+ self.client.force_login(self.admin)
468
+
469
+ def test_admin_lists_landing_link(self):
470
+ charger = Charger.objects.create(charger_id="ADMIN1")
471
+ url = reverse("admin:ocpp_charger_changelist")
472
+ resp = self.client.get(url)
473
+ self.assertContains(resp, charger.get_absolute_url())
474
+ status_url = reverse("charger-status", args=["ADMIN1"])
475
+ self.assertContains(resp, status_url)
476
+
477
+ def test_admin_does_not_list_qr_link(self):
478
+ charger = Charger.objects.create(charger_id="QR1")
479
+ url = reverse("admin:ocpp_charger_changelist")
480
+ resp = self.client.get(url)
481
+ self.assertNotContains(resp, charger.reference.image.url)
482
+
483
+ def test_admin_lists_log_link(self):
484
+ charger = Charger.objects.create(charger_id="LOG1")
485
+ url = reverse("admin:ocpp_charger_changelist")
486
+ resp = self.client.get(url)
487
+ log_url = reverse("charger-log", args=["LOG1"]) + "?type=charger"
488
+ self.assertContains(resp, log_url)
489
+
490
+ def test_admin_change_links_landing_page(self):
491
+ charger = Charger.objects.create(charger_id="CHANGE1")
492
+ url = reverse("admin:ocpp_charger_change", args=[charger.pk])
493
+ resp = self.client.get(url)
494
+ self.assertContains(resp, charger.get_absolute_url())
495
+
496
+ def test_admin_shows_location_name(self):
497
+ loc = Location.objects.create(name="AdminLoc")
498
+ Charger.objects.create(charger_id="ADMINLOC", location=loc)
499
+ url = reverse("admin:ocpp_charger_changelist")
500
+ resp = self.client.get(url)
501
+ self.assertContains(resp, "AdminLoc")
502
+
503
+ def test_last_fields_are_read_only(self):
504
+ now = timezone.now()
505
+ charger = Charger.objects.create(
506
+ charger_id="ADMINRO",
507
+ last_heartbeat=now,
508
+ last_meter_values={"a": 1},
509
+ )
510
+ url = reverse("admin:ocpp_charger_change", args=[charger.pk])
511
+ resp = self.client.get(url)
512
+ self.assertContains(resp, "Last heartbeat")
513
+ self.assertContains(resp, "Last meter values")
514
+ self.assertNotContains(resp, 'name="last_heartbeat"')
515
+ self.assertNotContains(resp, 'name="last_meter_values"')
516
+
517
+ def test_purge_action_removes_data(self):
518
+ charger = Charger.objects.create(charger_id="PURGE1")
519
+ Transaction.objects.create(
520
+ charger=charger,
521
+ start_time=timezone.now(),
522
+ )
523
+ MeterReading.objects.create(
524
+ charger=charger,
525
+ timestamp=timezone.now(),
526
+ value=1,
527
+ )
528
+ store.add_log("PURGE1", "entry", log_type="charger")
529
+ url = reverse("admin:ocpp_charger_changelist")
530
+ self.client.post(url, {"action": "purge_data", "_selected_action": [charger.pk]})
531
+ self.assertFalse(Transaction.objects.filter(charger=charger).exists())
532
+ self.assertFalse(MeterReading.objects.filter(charger=charger).exists())
533
+ self.assertNotIn("PURGE1", store.logs["charger"])
534
+
535
+ def test_delete_requires_purge(self):
536
+ charger = Charger.objects.create(charger_id="DEL1")
537
+ Transaction.objects.create(
538
+ charger=charger,
539
+ start_time=timezone.now(),
540
+ )
541
+ delete_url = reverse("admin:ocpp_charger_delete", args=[charger.pk])
542
+ with self.assertRaises(ProtectedError):
543
+ self.client.post(delete_url, {"post": "yes"})
544
+ self.assertTrue(Charger.objects.filter(pk=charger.pk).exists())
545
+ url = reverse("admin:ocpp_charger_changelist")
546
+ self.client.post(url, {"action": "purge_data", "_selected_action": [charger.pk]})
547
+ self.client.post(delete_url, {"post": "yes"})
548
+ self.assertFalse(Charger.objects.filter(pk=charger.pk).exists())
549
+
550
+
551
+ class TransactionAdminTests(TestCase):
552
+ def setUp(self):
553
+ self.client = Client()
554
+ User = get_user_model()
555
+ self.admin = User.objects.create_superuser(
556
+ username="tx-admin", password="secret", email="tx@example.com"
557
+ )
558
+ self.client.force_login(self.admin)
559
+
560
+ def test_meter_readings_inline_displayed(self):
561
+ charger = Charger.objects.create(charger_id="T1")
562
+ tx = Transaction.objects.create(charger=charger, start_time=timezone.now())
563
+ reading = MeterReading.objects.create(
564
+ charger=charger,
565
+ transaction=tx,
566
+ timestamp=timezone.now(),
567
+ value=Decimal("2.123"),
568
+ unit="kW",
569
+ )
570
+ url = reverse("admin:ocpp_transaction_change", args=[tx.pk])
571
+ resp = self.client.get(url)
572
+ self.assertContains(resp, str(reading.value))
573
+
574
+
575
+ class SimulatorAdminTests(TestCase):
576
+ def setUp(self):
577
+ self.client = Client()
578
+ User = get_user_model()
579
+ self.admin = User.objects.create_superuser(
580
+ username="admin2", password="secret", email="admin2@example.com"
581
+ )
582
+ self.client.force_login(self.admin)
583
+
584
+ def test_admin_lists_log_link(self):
585
+ sim = Simulator.objects.create(name="SIM", cp_path="SIMX")
586
+ url = reverse("admin:ocpp_simulator_changelist")
587
+ resp = self.client.get(url)
588
+ log_url = reverse("charger-log", args=["SIMX"]) + "?type=simulator"
589
+ self.assertContains(resp, log_url)
590
+
591
+ def test_admin_shows_ws_url(self):
592
+ sim = Simulator.objects.create(name="SIM2", cp_path="SIMY", host="h",
593
+ ws_port=1111)
594
+ url = reverse("admin:ocpp_simulator_changelist")
595
+ resp = self.client.get(url)
596
+ self.assertContains(resp, "ws://h:1111/SIMY/")
597
+
598
+ def test_as_config_includes_custom_fields(self):
599
+ sim = Simulator.objects.create(
600
+ name="SIM3",
601
+ cp_path="S3",
602
+ interval=3.5,
603
+ kw_max=70,
604
+ duration=500,
605
+ pre_charge_delay=5,
606
+ vin="WP0ZZZ99999999999",
607
+ )
608
+ cfg = sim.as_config()
609
+ self.assertEqual(cfg.interval, 3.5)
610
+ self.assertEqual(cfg.kw_max, 70)
611
+ self.assertEqual(cfg.duration, 500)
612
+ self.assertEqual(cfg.pre_charge_delay, 5)
613
+ self.assertEqual(cfg.vin, "WP0ZZZ99999999999")
614
+
615
+ async def test_unknown_charger_auto_registered(self):
616
+ communicator = WebsocketCommunicator(application, "/NEWCHG/")
617
+ connected, _ = await communicator.connect()
618
+ self.assertTrue(connected)
619
+
620
+ exists = await database_sync_to_async(Charger.objects.filter(charger_id="NEWCHG").exists)()
621
+ self.assertTrue(exists)
622
+
623
+ charger = await database_sync_to_async(Charger.objects.get)(charger_id="NEWCHG")
624
+ self.assertEqual(charger.last_path, "/NEWCHG/")
625
+
626
+ await communicator.disconnect()
627
+
628
+ async def test_nested_path_accepted_and_recorded(self):
629
+ communicator = WebsocketCommunicator(application, "/foo/NEST/")
630
+ connected, _ = await communicator.connect()
631
+ self.assertTrue(connected)
632
+
633
+ await communicator.disconnect()
634
+
635
+ charger = await database_sync_to_async(Charger.objects.get)(charger_id="NEST")
636
+ self.assertEqual(charger.last_path, "/foo/NEST/")
637
+
638
+ async def test_rfid_required_rejects_invalid(self):
639
+ await database_sync_to_async(Charger.objects.create)(charger_id="RFID", require_rfid=True)
640
+ communicator = WebsocketCommunicator(application, "/RFID/")
641
+ connected, _ = await communicator.connect()
642
+ self.assertTrue(connected)
643
+
644
+ await communicator.send_json_to([
645
+ 2,
646
+ "1",
647
+ "StartTransaction",
648
+ {"meterStart": 0},
649
+ ])
650
+ response = await communicator.receive_json_from()
651
+ self.assertEqual(response[2]["idTagInfo"]["status"], "Invalid")
652
+
653
+ exists = await database_sync_to_async(Transaction.objects.filter(charger__charger_id="RFID").exists)()
654
+ self.assertFalse(exists)
655
+
656
+ await communicator.disconnect()
657
+
658
+ async def test_rfid_required_accepts_known_tag(self):
659
+ User = get_user_model()
660
+ user = await database_sync_to_async(User.objects.create_user)(
661
+ username="bob", password="pwd"
662
+ )
663
+ acc = await database_sync_to_async(EnergyAccount.objects.create)(
664
+ user=user, name="BOB"
665
+ )
666
+ await database_sync_to_async(EnergyCredit.objects.create)(
667
+ account=acc, amount_kw=10
668
+ )
669
+ tag = await database_sync_to_async(RFID.objects.create)(rfid="CARDX")
670
+ await database_sync_to_async(acc.rfids.add)(tag)
671
+ await database_sync_to_async(Charger.objects.create)(charger_id="RFIDOK", require_rfid=True)
672
+ communicator = WebsocketCommunicator(application, "/RFIDOK/")
673
+ connected, _ = await communicator.connect()
674
+ self.assertTrue(connected)
675
+
676
+ await communicator.send_json_to([
677
+ 2,
678
+ "1",
679
+ "StartTransaction",
680
+ {"meterStart": 5, "idTag": "CARDX"},
681
+ ])
682
+ response = await communicator.receive_json_from()
683
+ self.assertEqual(response[2]["idTagInfo"]["status"], "Accepted")
684
+ tx_id = response[2]["transactionId"]
685
+
686
+ tx = await database_sync_to_async(Transaction.objects.get)(pk=tx_id, charger__charger_id="RFIDOK")
687
+ self.assertEqual(tx.account_id, user.energy_account.id)
688
+
689
+ async def test_status_fields_updated(self):
690
+ communicator = WebsocketCommunicator(application, "/STAT/")
691
+ connected, _ = await communicator.connect()
692
+ self.assertTrue(connected)
693
+
694
+ await communicator.send_json_to([2, "1", "Heartbeat", {}])
695
+ await communicator.receive_json_from()
696
+
697
+ charger = await database_sync_to_async(Charger.objects.get)(charger_id="STAT")
698
+ self.assertIsNotNone(charger.last_heartbeat)
699
+
700
+ payload = {
701
+ "meterValue": [
702
+ {
703
+ "timestamp": "2025-01-01T00:00:00Z",
704
+ "sampledValue": [{"value": "42"}],
705
+ }
706
+ ]
707
+ }
708
+ await communicator.send_json_to([2, "2", "MeterValues", payload])
709
+ await communicator.receive_json_from()
710
+
711
+ await database_sync_to_async(charger.refresh_from_db)()
712
+ self.assertEqual(charger.last_meter_values.get("meterValue")[0]["sampledValue"][0]["value"], "42")
713
+
714
+ await communicator.disconnect()
715
+
716
+
717
+ class ChargerLocationTests(TestCase):
718
+ def test_lat_lon_fields_saved(self):
719
+ loc = Location.objects.create(
720
+ name="Loc1", latitude=10.123456, longitude=-20.654321
721
+ )
722
+ charger = Charger.objects.create(charger_id="LOC1", location=loc)
723
+ self.assertAlmostEqual(float(charger.latitude), 10.123456)
724
+ self.assertAlmostEqual(float(charger.longitude), -20.654321)
725
+ self.assertEqual(charger.name, "Loc1")
726
+
727
+
728
+ class MeterReadingTests(TransactionTestCase):
729
+ async def test_meter_values_saved_as_readings(self):
730
+ communicator = WebsocketCommunicator(application, "/MR1/")
731
+ connected, _ = await communicator.connect()
732
+ self.assertTrue(connected)
733
+
734
+ payload = {
735
+ "connectorId": 1,
736
+ "transactionId": 100,
737
+ "meterValue": [
738
+ {
739
+ "timestamp": "2025-07-29T10:01:51Z",
740
+ "sampledValue": [
741
+ {
742
+ "value": "2.749",
743
+ "measurand": "Energy.Active.Import.Register",
744
+ "unit": "kW",
745
+ }
746
+ ],
747
+ }
748
+ ],
749
+ }
750
+ await communicator.send_json_to([2, "1", "MeterValues", payload])
751
+ await communicator.receive_json_from()
752
+
753
+ reading = await database_sync_to_async(MeterReading.objects.get)(charger__charger_id="MR1")
754
+ self.assertEqual(reading.transaction_id, 100)
755
+ self.assertEqual(str(reading.value), "2.749")
756
+ tx = await database_sync_to_async(Transaction.objects.get)(pk=100, charger__charger_id="MR1")
757
+ self.assertEqual(tx.meter_start, 2749)
758
+
759
+ await communicator.disconnect()
760
+
761
+
762
+ class ChargePointSimulatorTests(TransactionTestCase):
763
+ async def test_simulator_sends_messages(self):
764
+ received = []
765
+
766
+ async def handler(ws):
767
+ async for msg in ws:
768
+ data = json.loads(msg)
769
+ received.append(data)
770
+ action = data[2]
771
+ if action == "BootNotification":
772
+ await ws.send(
773
+ json.dumps(
774
+ [
775
+ 3,
776
+ data[1],
777
+ {
778
+ "status": "Accepted",
779
+ "currentTime": "2024-01-01T00:00:00Z",
780
+ "interval": 300,
781
+ },
782
+ ]
783
+ )
784
+ )
785
+ elif action == "Authorize":
786
+ await ws.send(
787
+ json.dumps(
788
+ [
789
+ 3,
790
+ data[1],
791
+ {"idTagInfo": {"status": "Accepted"}},
792
+ ]
793
+ )
794
+ )
795
+ elif action == "StartTransaction":
796
+ await ws.send(
797
+ json.dumps(
798
+ [
799
+ 3,
800
+ data[1],
801
+ {
802
+ "transactionId": 1,
803
+ "idTagInfo": {"status": "Accepted"},
804
+ },
805
+ ]
806
+ )
807
+ )
808
+ elif action == "MeterValues":
809
+ await ws.send(json.dumps([3, data[1], {}]))
810
+ elif action == "StopTransaction":
811
+ await ws.send(
812
+ json.dumps(
813
+ [
814
+ 3,
815
+ data[1],
816
+ {"idTagInfo": {"status": "Accepted"}},
817
+ ]
818
+ )
819
+ )
820
+ break
821
+
822
+ server = await websockets.serve(handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"])
823
+ port = server.sockets[0].getsockname()[1]
824
+
825
+ try:
826
+ cfg = SimulatorConfig(
827
+ host="127.0.0.1",
828
+ ws_port=port,
829
+ cp_path="SIM1/",
830
+ vin="WP0ZZZ12345678901",
831
+ duration=0.2,
832
+ interval=0.05,
833
+ kw_min=0.1,
834
+ kw_max=0.2,
835
+ pre_charge_delay=0.0,
836
+ )
837
+ sim = ChargePointSimulator(cfg)
838
+ await sim._run_session()
839
+ finally:
840
+ server.close()
841
+ await server.wait_closed()
842
+
843
+ actions = [msg[2] for msg in received]
844
+ self.assertIn("BootNotification", actions)
845
+ self.assertIn("StartTransaction", actions)
846
+ start_msg = next(msg for msg in received if msg[2] == "StartTransaction")
847
+ self.assertEqual(start_msg[3].get("vin"), "WP0ZZZ12345678901")
848
+
849
+ async def test_start_returns_status_and_log(self):
850
+ async def handler(ws):
851
+ async for msg in ws:
852
+ data = json.loads(msg)
853
+ action = data[2]
854
+ if action == "BootNotification":
855
+ await ws.send(
856
+ json.dumps(
857
+ [
858
+ 3,
859
+ data[1],
860
+ {
861
+ "status": "Accepted",
862
+ "currentTime": "2024-01-01T00:00:00Z",
863
+ "interval": 300,
864
+ },
865
+ ]
866
+ )
867
+ )
868
+ elif action == "Authorize":
869
+ await ws.send(
870
+ json.dumps(
871
+ [3, data[1], {"idTagInfo": {"status": "Accepted"}}]
872
+ )
873
+ )
874
+ elif action == "StartTransaction":
875
+ await ws.send(
876
+ json.dumps(
877
+ [
878
+ 3,
879
+ data[1],
880
+ {
881
+ "transactionId": 1,
882
+ "idTagInfo": {"status": "Accepted"},
883
+ },
884
+ ]
885
+ )
886
+ )
887
+ elif action == "StopTransaction":
888
+ await ws.send(
889
+ json.dumps(
890
+ [3, data[1], {"idTagInfo": {"status": "Accepted"}}]
891
+ )
892
+ )
893
+ break
894
+ else:
895
+ await ws.send(json.dumps([3, data[1], {}]))
896
+
897
+ server = await websockets.serve(handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"])
898
+ port = server.sockets[0].getsockname()[1]
899
+
900
+ cfg = SimulatorConfig(
901
+ host="127.0.0.1",
902
+ ws_port=port,
903
+ cp_path="SIMSTART/",
904
+ duration=0.1,
905
+ interval=0.05,
906
+ kw_min=0.1,
907
+ kw_max=0.2,
908
+ pre_charge_delay=0.0,
909
+ )
910
+ store.register_log_name(cfg.cp_path, "SimStart", log_type="simulator")
911
+ try:
912
+ sim = ChargePointSimulator(cfg)
913
+ started, status, log_file = await asyncio.to_thread(sim.start)
914
+ self.assertTrue(started)
915
+ self.assertEqual(status, "Connection accepted")
916
+ self.assertEqual(sim.status, "running")
917
+ self.assertTrue(Path(log_file).exists())
918
+ finally:
919
+ await sim.stop()
920
+ store.clear_log(cfg.cp_path, log_type="simulator")
921
+ server.close()
922
+ await server.wait_closed()
923
+
924
+ async def test_simulator_stops_when_charger_closes(self):
925
+ async def handler(ws):
926
+ async for msg in ws:
927
+ data = json.loads(msg)
928
+ action = data[2]
929
+ if action == "BootNotification":
930
+ await ws.send(
931
+ json.dumps([3, data[1], {"status": "Accepted"}])
932
+ )
933
+ elif action == "Authorize":
934
+ await ws.send(
935
+ json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}])
936
+ )
937
+ await ws.close()
938
+ break
939
+
940
+ server = await websockets.serve(handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"])
941
+ port = server.sockets[0].getsockname()[1]
942
+
943
+ cfg = SimulatorConfig(
944
+ host="127.0.0.1",
945
+ ws_port=port,
946
+ cp_path="SIMTERM/",
947
+ duration=0.1,
948
+ interval=0.05,
949
+ kw_min=0.1,
950
+ kw_max=0.2,
951
+ pre_charge_delay=0.0,
952
+ )
953
+ sim = ChargePointSimulator(cfg)
954
+ try:
955
+ started, _, _ = await asyncio.to_thread(sim.start)
956
+ self.assertTrue(started)
957
+ # Allow time for the server to close the connection
958
+ await asyncio.sleep(0.1)
959
+ self.assertEqual(sim.status, "stopped")
960
+ self.assertFalse(sim._thread.is_alive())
961
+ finally:
962
+ await sim.stop()
963
+ server.close()
964
+ await server.wait_closed()
965
+
966
+ async def test_pre_charge_sends_heartbeat_and_meter(self):
967
+ received = []
968
+
969
+ async def handler(ws):
970
+ async for msg in ws:
971
+ data = json.loads(msg)
972
+ received.append(data)
973
+ action = data[2]
974
+ if action == "BootNotification":
975
+ await ws.send(json.dumps([3, data[1], {"status": "Accepted"}]))
976
+ elif action in {"Authorize", "StatusNotification", "Heartbeat", "MeterValues"}:
977
+ await ws.send(json.dumps([3, data[1], {}]))
978
+ elif action == "StartTransaction":
979
+ await ws.send(
980
+ json.dumps(
981
+ [
982
+ 3,
983
+ data[1],
984
+ {"transactionId": 1, "idTagInfo": {"status": "Accepted"}},
985
+ ]
986
+ )
987
+ )
988
+ elif action == "StopTransaction":
989
+ await ws.send(json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}]))
990
+ break
991
+
992
+ server = await websockets.serve(handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"])
993
+ port = server.sockets[0].getsockname()[1]
994
+
995
+ try:
996
+ cfg = SimulatorConfig(
997
+ host="127.0.0.1",
998
+ ws_port=port,
999
+ cp_path="SIMPRE/",
1000
+ duration=0.1,
1001
+ interval=0.05,
1002
+ kw_min=0.1,
1003
+ kw_max=0.2,
1004
+ pre_charge_delay=0.1,
1005
+ )
1006
+ sim = ChargePointSimulator(cfg)
1007
+ await sim._run_session()
1008
+ finally:
1009
+ server.close()
1010
+ await server.wait_closed()
1011
+
1012
+ actions = [msg[2] for msg in received]
1013
+ start_idx = actions.index("StartTransaction")
1014
+ pre_actions = actions[:start_idx]
1015
+ self.assertIn("Heartbeat", pre_actions)
1016
+ self.assertIn("MeterValues", pre_actions)
1017
+
1018
+ async def test_simulator_times_out_without_response(self):
1019
+ async def handler(ws):
1020
+ async for _ in ws:
1021
+ pass
1022
+
1023
+ server = await websockets.serve(handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"])
1024
+ port = server.sockets[0].getsockname()[1]
1025
+
1026
+ cfg = SimulatorConfig(host="127.0.0.1", ws_port=port, cp_path="SIMTO/")
1027
+ sim = ChargePointSimulator(cfg)
1028
+ store.simulators[99] = sim
1029
+ try:
1030
+ with patch("ocpp.simulator.asyncio.wait_for", side_effect=asyncio.TimeoutError):
1031
+ started, status, _ = await asyncio.to_thread(sim.start)
1032
+ await asyncio.to_thread(sim._thread.join)
1033
+ self.assertFalse(started)
1034
+ self.assertIn("Timeout", status)
1035
+ self.assertNotIn(99, store.simulators)
1036
+ finally:
1037
+ await sim.stop()
1038
+ server.close()
1039
+ await server.wait_closed()
1040
+
1041
+
1042
+ class PurgeMeterReadingsTaskTests(TestCase):
1043
+ def test_purge_old_meter_readings(self):
1044
+ charger = Charger.objects.create(charger_id="PURGER")
1045
+ tx = Transaction.objects.create(
1046
+ charger=charger,
1047
+ meter_start=0,
1048
+ meter_stop=1000,
1049
+ start_time=timezone.now(),
1050
+ stop_time=timezone.now(),
1051
+ )
1052
+ old = timezone.now() - timedelta(days=8)
1053
+ recent = timezone.now() - timedelta(days=2)
1054
+ MeterReading.objects.create(
1055
+ charger=charger, transaction=tx, timestamp=old, value=1
1056
+ )
1057
+ MeterReading.objects.create(
1058
+ charger=charger, transaction=tx, timestamp=recent, value=2
1059
+ )
1060
+
1061
+ purge_meter_readings()
1062
+
1063
+ self.assertEqual(MeterReading.objects.count(), 1)
1064
+ self.assertTrue(
1065
+ MeterReading.objects.filter(timestamp__gte=recent - timedelta(minutes=1)).exists()
1066
+ )
1067
+ self.assertTrue(Transaction.objects.filter(pk=tx.pk).exists())
1068
+
1069
+ def test_purge_skips_open_transactions(self):
1070
+ charger = Charger.objects.create(charger_id="PURGER2")
1071
+ tx = Transaction.objects.create(
1072
+ charger=charger,
1073
+ meter_start=0,
1074
+ start_time=timezone.now() - timedelta(days=9),
1075
+ )
1076
+ old = timezone.now() - timedelta(days=8)
1077
+ reading = MeterReading.objects.create(
1078
+ charger=charger, transaction=tx, timestamp=old, value=1
1079
+ )
1080
+
1081
+ purge_meter_readings()
1082
+
1083
+ self.assertTrue(MeterReading.objects.filter(pk=reading.pk).exists())
1084
+
1085
+
1086
+ class TransactionKwTests(TestCase):
1087
+ def test_kw_sums_meter_readings(self):
1088
+ charger = Charger.objects.create(charger_id="SUM1")
1089
+ tx = Transaction.objects.create(charger=charger, start_time=timezone.now())
1090
+ MeterReading.objects.create(
1091
+ charger=charger,
1092
+ transaction=tx,
1093
+ timestamp=timezone.now(),
1094
+ value=Decimal("1.0"),
1095
+ unit="kW",
1096
+ )
1097
+ MeterReading.objects.create(
1098
+ charger=charger,
1099
+ transaction=tx,
1100
+ timestamp=timezone.now(),
1101
+ value=Decimal("500"),
1102
+ unit="W",
1103
+ )
1104
+ self.assertAlmostEqual(tx.kw, 1.5)
1105
+
1106
+ def test_kw_defaults_to_zero(self):
1107
+ charger = Charger.objects.create(charger_id="SUM2")
1108
+ tx = Transaction.objects.create(charger=charger, start_time=timezone.now())
1109
+ self.assertEqual(tx.kw, 0.0)
1110
+
1111
+
1112
+ class ChargerStatusViewTests(TestCase):
1113
+ def setUp(self):
1114
+ self.client = Client()
1115
+ User = get_user_model()
1116
+ self.user = User.objects.create_user(username="status", password="pwd")
1117
+ self.client.force_login(self.user)
1118
+ def test_chart_data_populated_from_existing_readings(self):
1119
+ charger = Charger.objects.create(charger_id="VIEW1")
1120
+ tx = Transaction.objects.create(charger=charger, start_time=timezone.now())
1121
+ t0 = timezone.now()
1122
+ MeterReading.objects.create(
1123
+ charger=charger,
1124
+ transaction=tx,
1125
+ timestamp=t0,
1126
+ value=Decimal("1000"),
1127
+ unit="W",
1128
+ )
1129
+ MeterReading.objects.create(
1130
+ charger=charger,
1131
+ transaction=tx,
1132
+ timestamp=t0 + timedelta(seconds=10),
1133
+ value=Decimal("500"),
1134
+ unit="W",
1135
+ )
1136
+ store.transactions[charger.charger_id] = tx
1137
+ resp = self.client.get(reverse("charger-status", args=[charger.charger_id]))
1138
+ self.assertEqual(resp.status_code, 200)
1139
+ chart = json.loads(resp.context["chart_data"])
1140
+ self.assertEqual(len(chart["labels"]), 2)
1141
+ self.assertAlmostEqual(chart["values"][0], 1.0)
1142
+ self.assertAlmostEqual(chart["values"][1], 1.5)
1143
+ store.transactions.pop(charger.charger_id, None)
1144
+
1145
+ def test_sessions_are_linked(self):
1146
+ charger = Charger.objects.create(charger_id="LINK1")
1147
+ tx = Transaction.objects.create(charger=charger, start_time=timezone.now())
1148
+ resp = self.client.get(reverse("charger-status", args=[charger.charger_id]))
1149
+ self.assertContains(resp, f"?session={tx.id}")
1150
+
1151
+ def test_status_links_landing_page(self):
1152
+ charger = Charger.objects.create(charger_id="LAND1")
1153
+ resp = self.client.get(reverse("charger-status", args=[charger.charger_id]))
1154
+ self.assertContains(resp, reverse("charger-page", args=[charger.charger_id]))
1155
+
1156
+ def test_past_session_chart(self):
1157
+ charger = Charger.objects.create(charger_id="PAST1")
1158
+ tx = Transaction.objects.create(charger=charger, start_time=timezone.now())
1159
+ t0 = timezone.now()
1160
+ MeterReading.objects.create(
1161
+ charger=charger,
1162
+ transaction=tx,
1163
+ timestamp=t0,
1164
+ value=Decimal("1000"),
1165
+ unit="W",
1166
+ )
1167
+ MeterReading.objects.create(
1168
+ charger=charger,
1169
+ transaction=tx,
1170
+ timestamp=t0 + timedelta(seconds=10),
1171
+ value=Decimal("1000"),
1172
+ unit="W",
1173
+ )
1174
+ resp = self.client.get(
1175
+ reverse("charger-status", args=[charger.charger_id]) + f"?session={tx.id}"
1176
+ )
1177
+ self.assertContains(resp, "Back to live")
1178
+ chart = json.loads(resp.context["chart_data"])
1179
+ self.assertEqual(len(chart["labels"]), 2)
1180
+ self.assertTrue(resp.context["past_session"])
1181
+
1182
+
1183
+ class ChargerSessionPaginationTests(TestCase):
1184
+ def setUp(self):
1185
+ self.client = Client()
1186
+ User = get_user_model()
1187
+ self.user = User.objects.create_user(username="page", password="pwd")
1188
+ self.client.force_login(self.user)
1189
+ self.charger = Charger.objects.create(charger_id="PAGETEST")
1190
+ for i in range(15):
1191
+ Transaction.objects.create(
1192
+ charger=self.charger,
1193
+ start_time=timezone.now() - timedelta(minutes=i),
1194
+ meter_start=0,
1195
+ )
1196
+
1197
+ def test_only_ten_transactions_shown(self):
1198
+ resp = self.client.get(reverse("charger-status", args=[self.charger.charger_id]))
1199
+ self.assertEqual(resp.status_code, 200)
1200
+ self.assertEqual(len(resp.context["transactions"]), 10)
1201
+ self.assertTrue(resp.context["page_obj"].has_next())
1202
+
1203
+ def test_session_search_by_date(self):
1204
+ date_str = timezone.now().date().isoformat()
1205
+ resp = self.client.get(
1206
+ reverse("charger-session-search", args=[self.charger.charger_id]),
1207
+ {"date": date_str},
1208
+ )
1209
+ self.assertEqual(resp.status_code, 200)
1210
+ self.assertEqual(len(resp.context["transactions"]), 15)
1211
+
1212
+
1213
+ class EfficiencyCalculatorViewTests(TestCase):
1214
+ def setUp(self):
1215
+ User = get_user_model()
1216
+ self.user = User.objects.create_user(
1217
+ username="eff", password="secret", email="eff@example.com"
1218
+ )
1219
+ self.client.force_login(self.user)
1220
+
1221
+ def test_get_view(self):
1222
+ url = reverse("ev-efficiency")
1223
+ resp = self.client.get(url)
1224
+ self.assertContains(resp, "EV Efficiency Calculator")
1225
+
1226
+ def test_post_calculation(self):
1227
+ url = reverse("ev-efficiency")
1228
+ resp = self.client.post(url, {"distance": "100", "energy": "20"})
1229
+ self.assertContains(resp, "5.00")