arthexis 0.1.9__py3-none-any.whl → 0.1.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/METADATA +76 -23
- arthexis-0.1.11.dist-info/RECORD +99 -0
- config/context_processors.py +1 -0
- config/settings.py +245 -26
- config/urls.py +11 -4
- core/admin.py +585 -57
- core/apps.py +29 -1
- core/auto_upgrade.py +57 -0
- core/backends.py +115 -3
- core/environment.py +23 -5
- core/fields.py +93 -0
- core/mailer.py +3 -1
- core/models.py +482 -38
- core/reference_utils.py +108 -0
- core/sigil_builder.py +23 -5
- core/sigil_resolver.py +35 -4
- core/system.py +400 -140
- core/tasks.py +151 -8
- core/temp_passwords.py +181 -0
- core/test_system_info.py +97 -1
- core/tests.py +393 -15
- core/user_data.py +154 -16
- core/views.py +499 -20
- nodes/admin.py +149 -6
- nodes/backends.py +125 -18
- nodes/dns.py +203 -0
- nodes/models.py +498 -9
- nodes/tests.py +682 -3
- nodes/views.py +154 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +255 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +52 -7
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +414 -8
- ocpp/views.py +109 -76
- pages/admin.py +9 -1
- pages/context_processors.py +24 -4
- pages/defaults.py +14 -0
- pages/forms.py +131 -0
- pages/models.py +53 -14
- pages/tests.py +450 -14
- pages/urls.py +4 -0
- pages/views.py +419 -110
- arthexis-0.1.9.dist-info/RECORD +0 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/top_level.txt +0 -0
ocpp/tests.py
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
4
|
+
|
|
5
|
+
import django
|
|
6
|
+
|
|
7
|
+
django.setup()
|
|
8
|
+
|
|
1
9
|
from asgiref.testing import ApplicationCommunicator
|
|
2
10
|
from channels.testing import WebsocketCommunicator
|
|
3
11
|
from channels.db import database_sync_to_async
|
|
4
12
|
from asgiref.sync import async_to_sync
|
|
5
13
|
from django.test import Client, TransactionTestCase, TestCase, override_settings
|
|
6
14
|
from unittest import skip
|
|
7
|
-
from
|
|
15
|
+
from contextlib import suppress
|
|
16
|
+
from types import SimpleNamespace
|
|
17
|
+
from unittest.mock import patch, Mock
|
|
8
18
|
from django.contrib.auth import get_user_model
|
|
9
19
|
from django.urls import reverse
|
|
10
20
|
from django.utils import timezone
|
|
@@ -17,8 +27,8 @@ from nodes.models import Node, NodeRole
|
|
|
17
27
|
from config.asgi import application
|
|
18
28
|
|
|
19
29
|
from .models import Transaction, Charger, Simulator, MeterReading, Location
|
|
20
|
-
from
|
|
21
|
-
from core.models import RFID
|
|
30
|
+
from .consumers import CSMSConsumer
|
|
31
|
+
from core.models import EnergyAccount, EnergyCredit, Reference, RFID
|
|
22
32
|
from . import store
|
|
23
33
|
from django.db.models.deletion import ProtectedError
|
|
24
34
|
from decimal import Decimal
|
|
@@ -131,6 +141,17 @@ class ChargerUrlFallbackTests(TestCase):
|
|
|
131
141
|
self.assertTrue(charger.reference.value.startswith("http://fallback.example"))
|
|
132
142
|
self.assertTrue(charger.reference.value.endswith("/c/NO_SITE/"))
|
|
133
143
|
|
|
144
|
+
def test_reference_not_created_for_loopback_domain(self):
|
|
145
|
+
site = Site.objects.get_current()
|
|
146
|
+
site.domain = "127.0.0.1"
|
|
147
|
+
site.save()
|
|
148
|
+
Site.objects.clear_cache()
|
|
149
|
+
|
|
150
|
+
charger = Charger.objects.create(charger_id="LOCAL_LOOP")
|
|
151
|
+
charger.refresh_from_db()
|
|
152
|
+
|
|
153
|
+
self.assertIsNone(charger.reference)
|
|
154
|
+
|
|
134
155
|
|
|
135
156
|
class SinkConsumerTests(TransactionTestCase):
|
|
136
157
|
async def test_sink_replies(self):
|
|
@@ -251,11 +272,83 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
251
272
|
|
|
252
273
|
mock_broadcast.assert_called_once()
|
|
253
274
|
_, kwargs = mock_broadcast.call_args
|
|
254
|
-
self.assertEqual(kwargs["subject"], "
|
|
255
|
-
|
|
256
|
-
self.
|
|
257
|
-
|
|
258
|
-
|
|
275
|
+
self.assertEqual(kwargs["subject"], "NETMSG")
|
|
276
|
+
body = kwargs["body"]
|
|
277
|
+
self.assertRegex(body, r"^\d+\.\d kWh \d{2}:\d{2}$")
|
|
278
|
+
|
|
279
|
+
async def test_consumption_message_updates_existing_entry(self):
|
|
280
|
+
original_interval = CSMSConsumer.consumption_update_interval
|
|
281
|
+
CSMSConsumer.consumption_update_interval = 0.01
|
|
282
|
+
await database_sync_to_async(Charger.objects.create)(charger_id="UPDATEMSG")
|
|
283
|
+
communicator = WebsocketCommunicator(application, "/UPDATEMSG/")
|
|
284
|
+
connected, _ = await communicator.connect()
|
|
285
|
+
self.assertTrue(connected)
|
|
286
|
+
|
|
287
|
+
message_mock = Mock()
|
|
288
|
+
message_mock.uuid = "mock-uuid"
|
|
289
|
+
message_mock.save = Mock()
|
|
290
|
+
message_mock.propagate = Mock()
|
|
291
|
+
|
|
292
|
+
filter_mock = Mock()
|
|
293
|
+
filter_mock.first.return_value = message_mock
|
|
294
|
+
|
|
295
|
+
broadcast_result = SimpleNamespace(uuid="mock-uuid")
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
with patch(
|
|
299
|
+
"nodes.models.NetMessage.broadcast", return_value=broadcast_result
|
|
300
|
+
) as mock_broadcast, patch(
|
|
301
|
+
"nodes.models.NetMessage.objects.filter", return_value=filter_mock
|
|
302
|
+
):
|
|
303
|
+
await communicator.send_json_to(
|
|
304
|
+
[2, "1", "StartTransaction", {"meterStart": 1}]
|
|
305
|
+
)
|
|
306
|
+
await communicator.receive_json_from()
|
|
307
|
+
mock_broadcast.assert_called_once()
|
|
308
|
+
await asyncio.sleep(0.05)
|
|
309
|
+
await communicator.disconnect()
|
|
310
|
+
finally:
|
|
311
|
+
CSMSConsumer.consumption_update_interval = original_interval
|
|
312
|
+
with suppress(Exception):
|
|
313
|
+
await communicator.disconnect()
|
|
314
|
+
|
|
315
|
+
self.assertTrue(message_mock.save.called)
|
|
316
|
+
self.assertTrue(message_mock.propagate.called)
|
|
317
|
+
|
|
318
|
+
async def test_consumption_message_final_update_on_disconnect(self):
|
|
319
|
+
await database_sync_to_async(Charger.objects.create)(charger_id="FINALMSG")
|
|
320
|
+
communicator = WebsocketCommunicator(application, "/FINALMSG/")
|
|
321
|
+
connected, _ = await communicator.connect()
|
|
322
|
+
self.assertTrue(connected)
|
|
323
|
+
|
|
324
|
+
message_mock = Mock()
|
|
325
|
+
message_mock.uuid = "mock-uuid"
|
|
326
|
+
message_mock.save = Mock()
|
|
327
|
+
message_mock.propagate = Mock()
|
|
328
|
+
|
|
329
|
+
filter_mock = Mock()
|
|
330
|
+
filter_mock.first.return_value = message_mock
|
|
331
|
+
|
|
332
|
+
broadcast_result = SimpleNamespace(uuid="mock-uuid")
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
with patch(
|
|
336
|
+
"nodes.models.NetMessage.broadcast", return_value=broadcast_result
|
|
337
|
+
) as mock_broadcast, patch(
|
|
338
|
+
"nodes.models.NetMessage.objects.filter", return_value=filter_mock
|
|
339
|
+
):
|
|
340
|
+
await communicator.send_json_to(
|
|
341
|
+
[2, "1", "StartTransaction", {"meterStart": 1}]
|
|
342
|
+
)
|
|
343
|
+
await communicator.receive_json_from()
|
|
344
|
+
mock_broadcast.assert_called_once()
|
|
345
|
+
await communicator.disconnect()
|
|
346
|
+
finally:
|
|
347
|
+
with suppress(Exception):
|
|
348
|
+
await communicator.disconnect()
|
|
349
|
+
|
|
350
|
+
self.assertTrue(message_mock.save.called)
|
|
351
|
+
self.assertTrue(message_mock.propagate.called)
|
|
259
352
|
|
|
260
353
|
async def test_rfid_unbound_instance_created(self):
|
|
261
354
|
await database_sync_to_async(Charger.objects.create)(charger_id="NEWRFID")
|
|
@@ -529,6 +622,81 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
529
622
|
self.assertIn(2, connectors)
|
|
530
623
|
self.assertIn(None, connectors)
|
|
531
624
|
|
|
625
|
+
async def test_console_reference_created_for_aggregate_connector(self):
|
|
626
|
+
communicator = ClientWebsocketCommunicator(
|
|
627
|
+
application,
|
|
628
|
+
"/CONREF/",
|
|
629
|
+
client=("203.0.113.5", 12345),
|
|
630
|
+
)
|
|
631
|
+
connected, _ = await communicator.connect()
|
|
632
|
+
self.assertTrue(connected)
|
|
633
|
+
|
|
634
|
+
await communicator.send_json_to([2, "1", "BootNotification", {}])
|
|
635
|
+
await communicator.receive_json_from()
|
|
636
|
+
|
|
637
|
+
reference = await database_sync_to_async(
|
|
638
|
+
lambda: Reference.objects.get(alt_text="CONREF Console")
|
|
639
|
+
)()
|
|
640
|
+
self.assertEqual(reference.value, "http://203.0.113.5:8900")
|
|
641
|
+
self.assertTrue(reference.show_in_header)
|
|
642
|
+
|
|
643
|
+
await communicator.send_json_to(
|
|
644
|
+
[
|
|
645
|
+
2,
|
|
646
|
+
"2",
|
|
647
|
+
"StatusNotification",
|
|
648
|
+
{"connectorId": 1, "status": "Available"},
|
|
649
|
+
]
|
|
650
|
+
)
|
|
651
|
+
await communicator.receive_json_from()
|
|
652
|
+
|
|
653
|
+
count = await database_sync_to_async(
|
|
654
|
+
lambda: Reference.objects.filter(alt_text="CONREF Console").count()
|
|
655
|
+
)()
|
|
656
|
+
self.assertEqual(count, 1)
|
|
657
|
+
|
|
658
|
+
await communicator.disconnect()
|
|
659
|
+
|
|
660
|
+
async def test_console_reference_uses_forwarded_for_header(self):
|
|
661
|
+
communicator = ClientWebsocketCommunicator(
|
|
662
|
+
application,
|
|
663
|
+
"/FORWARDED/",
|
|
664
|
+
client=("127.0.0.1", 23456),
|
|
665
|
+
headers=[(b"x-forwarded-for", b"198.51.100.75, 127.0.0.1")],
|
|
666
|
+
)
|
|
667
|
+
connected, _ = await communicator.connect()
|
|
668
|
+
self.assertTrue(connected)
|
|
669
|
+
self.assertIn("198.51.100.75", store.ip_connections)
|
|
670
|
+
|
|
671
|
+
await communicator.send_json_to([2, "1", "BootNotification", {}])
|
|
672
|
+
await communicator.receive_json_from()
|
|
673
|
+
|
|
674
|
+
reference = await database_sync_to_async(
|
|
675
|
+
lambda: Reference.objects.get(alt_text="FORWARDED Console")
|
|
676
|
+
)()
|
|
677
|
+
self.assertEqual(reference.value, "http://198.51.100.75:8900")
|
|
678
|
+
|
|
679
|
+
await communicator.disconnect()
|
|
680
|
+
|
|
681
|
+
async def test_console_reference_skips_loopback_ip(self):
|
|
682
|
+
communicator = ClientWebsocketCommunicator(
|
|
683
|
+
application,
|
|
684
|
+
"/LOCAL/",
|
|
685
|
+
client=("127.0.0.1", 34567),
|
|
686
|
+
)
|
|
687
|
+
connected, _ = await communicator.connect()
|
|
688
|
+
self.assertTrue(connected)
|
|
689
|
+
|
|
690
|
+
await communicator.send_json_to([2, "1", "BootNotification", {}])
|
|
691
|
+
await communicator.receive_json_from()
|
|
692
|
+
|
|
693
|
+
exists = await database_sync_to_async(
|
|
694
|
+
lambda: Reference.objects.filter(alt_text="LOCAL Console").exists()
|
|
695
|
+
)()
|
|
696
|
+
self.assertFalse(exists)
|
|
697
|
+
|
|
698
|
+
await communicator.disconnect()
|
|
699
|
+
|
|
532
700
|
async def test_transaction_created_from_meter_values(self):
|
|
533
701
|
communicator = WebsocketCommunicator(application, "/NOSTART/")
|
|
534
702
|
connected, _ = await communicator.connect()
|
|
@@ -914,6 +1082,78 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
914
1082
|
store.transactions.pop(key2, None)
|
|
915
1083
|
|
|
916
1084
|
|
|
1085
|
+
async def test_rate_limit_blocks_third_connection(self):
|
|
1086
|
+
store.ip_connections.clear()
|
|
1087
|
+
ip = "203.0.113.10"
|
|
1088
|
+
communicator1 = ClientWebsocketCommunicator(
|
|
1089
|
+
application, "/IPLIMIT1/", client=(ip, 1001)
|
|
1090
|
+
)
|
|
1091
|
+
communicator2 = ClientWebsocketCommunicator(
|
|
1092
|
+
application, "/IPLIMIT2/", client=(ip, 1002)
|
|
1093
|
+
)
|
|
1094
|
+
communicator3 = ClientWebsocketCommunicator(
|
|
1095
|
+
application, "/IPLIMIT3/", client=(ip, 1003)
|
|
1096
|
+
)
|
|
1097
|
+
other = ClientWebsocketCommunicator(
|
|
1098
|
+
application, "/OTHERIP/", client=("198.51.100.5", 2001)
|
|
1099
|
+
)
|
|
1100
|
+
connected1 = connected2 = connected_other = False
|
|
1101
|
+
try:
|
|
1102
|
+
connected1, _ = await communicator1.connect()
|
|
1103
|
+
self.assertTrue(connected1)
|
|
1104
|
+
connected2, _ = await communicator2.connect()
|
|
1105
|
+
self.assertTrue(connected2)
|
|
1106
|
+
connected3, code = await communicator3.connect()
|
|
1107
|
+
self.assertFalse(connected3)
|
|
1108
|
+
self.assertEqual(code, 4003)
|
|
1109
|
+
connected_other, _ = await other.connect()
|
|
1110
|
+
self.assertTrue(connected_other)
|
|
1111
|
+
finally:
|
|
1112
|
+
if connected1:
|
|
1113
|
+
await communicator1.disconnect()
|
|
1114
|
+
if connected2:
|
|
1115
|
+
await communicator2.disconnect()
|
|
1116
|
+
if connected_other:
|
|
1117
|
+
await other.disconnect()
|
|
1118
|
+
|
|
1119
|
+
async def test_rate_limit_allows_reconnect_after_disconnect(self):
|
|
1120
|
+
store.ip_connections.clear()
|
|
1121
|
+
ip = "203.0.113.20"
|
|
1122
|
+
communicator1 = ClientWebsocketCommunicator(
|
|
1123
|
+
application, "/LIMITRESET1/", client=(ip, 3001)
|
|
1124
|
+
)
|
|
1125
|
+
communicator2 = ClientWebsocketCommunicator(
|
|
1126
|
+
application, "/LIMITRESET2/", client=(ip, 3002)
|
|
1127
|
+
)
|
|
1128
|
+
communicator3 = ClientWebsocketCommunicator(
|
|
1129
|
+
application, "/LIMITRESET3/", client=(ip, 3003)
|
|
1130
|
+
)
|
|
1131
|
+
communicator3_retry = None
|
|
1132
|
+
connected1 = connected2 = connected3_retry = False
|
|
1133
|
+
try:
|
|
1134
|
+
connected1, _ = await communicator1.connect()
|
|
1135
|
+
self.assertTrue(connected1)
|
|
1136
|
+
connected2, _ = await communicator2.connect()
|
|
1137
|
+
self.assertTrue(connected2)
|
|
1138
|
+
connected3, code = await communicator3.connect()
|
|
1139
|
+
self.assertFalse(connected3)
|
|
1140
|
+
self.assertEqual(code, 4003)
|
|
1141
|
+
await communicator1.disconnect()
|
|
1142
|
+
connected1 = False
|
|
1143
|
+
communicator3_retry = ClientWebsocketCommunicator(
|
|
1144
|
+
application, "/LIMITRESET4/", client=(ip, 3004)
|
|
1145
|
+
)
|
|
1146
|
+
connected3_retry, _ = await communicator3_retry.connect()
|
|
1147
|
+
self.assertTrue(connected3_retry)
|
|
1148
|
+
finally:
|
|
1149
|
+
if connected1:
|
|
1150
|
+
await communicator1.disconnect()
|
|
1151
|
+
if connected2:
|
|
1152
|
+
await communicator2.disconnect()
|
|
1153
|
+
if connected3_retry and communicator3_retry is not None:
|
|
1154
|
+
await communicator3_retry.disconnect()
|
|
1155
|
+
|
|
1156
|
+
|
|
917
1157
|
class ChargerLandingTests(TestCase):
|
|
918
1158
|
def setUp(self):
|
|
919
1159
|
self.client = Client()
|
|
@@ -1281,6 +1521,42 @@ class SimulatorAdminTests(TransactionTestCase):
|
|
|
1281
1521
|
resp = self.client.get(url)
|
|
1282
1522
|
self.assertContains(resp, "ws://h:1111/SIMY/")
|
|
1283
1523
|
|
|
1524
|
+
def test_admin_ws_url_without_port(self):
|
|
1525
|
+
sim = Simulator.objects.create(
|
|
1526
|
+
name="SIMNP", cp_path="SIMNP", host="h", ws_port=None
|
|
1527
|
+
)
|
|
1528
|
+
url = reverse("admin:ocpp_simulator_changelist")
|
|
1529
|
+
resp = self.client.get(url)
|
|
1530
|
+
self.assertContains(resp, "ws://h/SIMNP/")
|
|
1531
|
+
|
|
1532
|
+
def test_send_open_door_action_requires_running_simulator(self):
|
|
1533
|
+
sim = Simulator.objects.create(name="SIMDO", cp_path="SIMDO")
|
|
1534
|
+
url = reverse("admin:ocpp_simulator_changelist")
|
|
1535
|
+
resp = self.client.post(
|
|
1536
|
+
url,
|
|
1537
|
+
{"action": "send_open_door", "_selected_action": [sim.pk]},
|
|
1538
|
+
follow=True,
|
|
1539
|
+
)
|
|
1540
|
+
self.assertEqual(resp.status_code, 200)
|
|
1541
|
+
self.assertContains(resp, "simulator is not running")
|
|
1542
|
+
self.assertFalse(Simulator.objects.get(pk=sim.pk).door_open)
|
|
1543
|
+
|
|
1544
|
+
def test_send_open_door_action_triggers_simulator(self):
|
|
1545
|
+
sim = Simulator.objects.create(name="SIMTRIG", cp_path="SIMTRIG")
|
|
1546
|
+
stub = SimpleNamespace(trigger_door_open=Mock())
|
|
1547
|
+
store.simulators[sim.pk] = stub
|
|
1548
|
+
url = reverse("admin:ocpp_simulator_changelist")
|
|
1549
|
+
resp = self.client.post(
|
|
1550
|
+
url,
|
|
1551
|
+
{"action": "send_open_door", "_selected_action": [sim.pk]},
|
|
1552
|
+
follow=True,
|
|
1553
|
+
)
|
|
1554
|
+
self.assertEqual(resp.status_code, 200)
|
|
1555
|
+
stub.trigger_door_open.assert_called_once()
|
|
1556
|
+
self.assertContains(resp, "DoorOpen status notification sent")
|
|
1557
|
+
self.assertFalse(Simulator.objects.get(pk=sim.pk).door_open)
|
|
1558
|
+
store.simulators.pop(sim.pk, None)
|
|
1559
|
+
|
|
1284
1560
|
def test_as_config_includes_custom_fields(self):
|
|
1285
1561
|
sim = Simulator.objects.create(
|
|
1286
1562
|
name="SIM3",
|
|
@@ -1298,6 +1574,45 @@ class SimulatorAdminTests(TransactionTestCase):
|
|
|
1298
1574
|
self.assertEqual(cfg.pre_charge_delay, 5)
|
|
1299
1575
|
self.assertEqual(cfg.vin, "WP0ZZZ99999999999")
|
|
1300
1576
|
|
|
1577
|
+
def _post_simulator_change(self, sim: Simulator, **overrides):
|
|
1578
|
+
url = reverse("admin:ocpp_simulator_change", args=[sim.pk])
|
|
1579
|
+
data = {
|
|
1580
|
+
"name": sim.name,
|
|
1581
|
+
"cp_path": sim.cp_path,
|
|
1582
|
+
"host": sim.host,
|
|
1583
|
+
"ws_port": sim.ws_port or "",
|
|
1584
|
+
"rfid": sim.rfid,
|
|
1585
|
+
"duration": sim.duration,
|
|
1586
|
+
"interval": sim.interval,
|
|
1587
|
+
"pre_charge_delay": sim.pre_charge_delay,
|
|
1588
|
+
"kw_max": sim.kw_max,
|
|
1589
|
+
"repeat": "on" if sim.repeat else "",
|
|
1590
|
+
"username": sim.username,
|
|
1591
|
+
"password": sim.password,
|
|
1592
|
+
"door_open": "on" if overrides.get("door_open", False) else "",
|
|
1593
|
+
"_save": "Save",
|
|
1594
|
+
}
|
|
1595
|
+
data.update(overrides)
|
|
1596
|
+
return self.client.post(url, data, follow=True)
|
|
1597
|
+
|
|
1598
|
+
def test_save_model_triggers_door_open(self):
|
|
1599
|
+
sim = Simulator.objects.create(name="SIMSAVE", cp_path="SIMSAVE")
|
|
1600
|
+
stub = SimpleNamespace(trigger_door_open=Mock())
|
|
1601
|
+
store.simulators[sim.pk] = stub
|
|
1602
|
+
resp = self._post_simulator_change(sim, door_open="on")
|
|
1603
|
+
self.assertEqual(resp.status_code, 200)
|
|
1604
|
+
stub.trigger_door_open.assert_called_once()
|
|
1605
|
+
self.assertContains(resp, "DoorOpen status notification sent")
|
|
1606
|
+
self.assertFalse(Simulator.objects.get(pk=sim.pk).door_open)
|
|
1607
|
+
store.simulators.pop(sim.pk, None)
|
|
1608
|
+
|
|
1609
|
+
def test_save_model_reports_error_when_not_running(self):
|
|
1610
|
+
sim = Simulator.objects.create(name="SIMERR", cp_path="SIMERR")
|
|
1611
|
+
resp = self._post_simulator_change(sim, door_open="on")
|
|
1612
|
+
self.assertEqual(resp.status_code, 200)
|
|
1613
|
+
self.assertContains(resp, "simulator is not running")
|
|
1614
|
+
self.assertFalse(Simulator.objects.get(pk=sim.pk).door_open)
|
|
1615
|
+
|
|
1301
1616
|
async def test_unknown_charger_auto_registered(self):
|
|
1302
1617
|
communicator = WebsocketCommunicator(application, "/NEWCHG/")
|
|
1303
1618
|
connected, _ = await communicator.connect()
|
|
@@ -1781,6 +2096,80 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
1781
2096
|
server.close()
|
|
1782
2097
|
await server.wait_closed()
|
|
1783
2098
|
|
|
2099
|
+
async def test_door_open_event_sends_notifications(self):
|
|
2100
|
+
status_payloads = []
|
|
2101
|
+
|
|
2102
|
+
async def handler(ws):
|
|
2103
|
+
async for msg in ws:
|
|
2104
|
+
data = json.loads(msg)
|
|
2105
|
+
action = data[2]
|
|
2106
|
+
if action == "BootNotification":
|
|
2107
|
+
await ws.send(
|
|
2108
|
+
json.dumps(
|
|
2109
|
+
[
|
|
2110
|
+
3,
|
|
2111
|
+
data[1],
|
|
2112
|
+
{"status": "Accepted", "currentTime": "2024-01-01T00:00:00Z"},
|
|
2113
|
+
]
|
|
2114
|
+
)
|
|
2115
|
+
)
|
|
2116
|
+
elif action == "Authorize":
|
|
2117
|
+
await ws.send(json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}]))
|
|
2118
|
+
elif action == "StatusNotification":
|
|
2119
|
+
status_payloads.append(data[3])
|
|
2120
|
+
await ws.send(json.dumps([3, data[1], {}]))
|
|
2121
|
+
elif action == "StartTransaction":
|
|
2122
|
+
await ws.send(
|
|
2123
|
+
json.dumps(
|
|
2124
|
+
[
|
|
2125
|
+
3,
|
|
2126
|
+
data[1],
|
|
2127
|
+
{"transactionId": 1, "idTagInfo": {"status": "Accepted"}},
|
|
2128
|
+
]
|
|
2129
|
+
)
|
|
2130
|
+
)
|
|
2131
|
+
elif action == "MeterValues":
|
|
2132
|
+
await ws.send(json.dumps([3, data[1], {}]))
|
|
2133
|
+
elif action == "StopTransaction":
|
|
2134
|
+
await ws.send(json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}]))
|
|
2135
|
+
break
|
|
2136
|
+
|
|
2137
|
+
server = await websockets.serve(
|
|
2138
|
+
handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"]
|
|
2139
|
+
)
|
|
2140
|
+
port = server.sockets[0].getsockname()[1]
|
|
2141
|
+
|
|
2142
|
+
cfg = SimulatorConfig(
|
|
2143
|
+
host="127.0.0.1",
|
|
2144
|
+
ws_port=port,
|
|
2145
|
+
cp_path="SIMDOOR/",
|
|
2146
|
+
duration=0.2,
|
|
2147
|
+
interval=0.05,
|
|
2148
|
+
pre_charge_delay=0.0,
|
|
2149
|
+
)
|
|
2150
|
+
sim = ChargePointSimulator(cfg)
|
|
2151
|
+
sim.trigger_door_open()
|
|
2152
|
+
try:
|
|
2153
|
+
await sim._run_session()
|
|
2154
|
+
finally:
|
|
2155
|
+
server.close()
|
|
2156
|
+
await server.wait_closed()
|
|
2157
|
+
store.clear_log(cfg.cp_path, log_type="simulator")
|
|
2158
|
+
|
|
2159
|
+
door_open_messages = [p for p in status_payloads if p.get("errorCode") == "DoorOpen"]
|
|
2160
|
+
door_closed_messages = [p for p in status_payloads if p.get("errorCode") == "NoError"]
|
|
2161
|
+
self.assertTrue(door_open_messages)
|
|
2162
|
+
self.assertTrue(door_closed_messages)
|
|
2163
|
+
first_open = next(
|
|
2164
|
+
idx for idx, payload in enumerate(status_payloads) if payload.get("errorCode") == "DoorOpen"
|
|
2165
|
+
)
|
|
2166
|
+
first_close = next(
|
|
2167
|
+
idx for idx, payload in enumerate(status_payloads) if payload.get("errorCode") == "NoError"
|
|
2168
|
+
)
|
|
2169
|
+
self.assertLess(first_open, first_close)
|
|
2170
|
+
self.assertEqual(door_open_messages[0].get("status"), "Faulted")
|
|
2171
|
+
self.assertEqual(door_closed_messages[0].get("status"), "Available")
|
|
2172
|
+
|
|
1784
2173
|
|
|
1785
2174
|
class PurgeMeterReadingsTaskTests(TestCase):
|
|
1786
2175
|
def test_purge_old_meter_readings(self):
|
|
@@ -2294,3 +2683,20 @@ class LiveUpdateViewTests(TestCase):
|
|
|
2294
2683
|
resp = self.client.get(reverse("cp-simulator"))
|
|
2295
2684
|
self.assertEqual(resp.context["request"].live_update_interval, 5)
|
|
2296
2685
|
self.assertContains(resp, "setInterval(() => location.reload()")
|
|
2686
|
+
|
|
2687
|
+
def test_dashboard_hides_private_chargers(self):
|
|
2688
|
+
public = Charger.objects.create(charger_id="PUBLICCP")
|
|
2689
|
+
private = Charger.objects.create(
|
|
2690
|
+
charger_id="PRIVATECP", public_display=False
|
|
2691
|
+
)
|
|
2692
|
+
|
|
2693
|
+
resp = self.client.get(reverse("ocpp-dashboard"))
|
|
2694
|
+
chargers = [item["charger"] for item in resp.context["chargers"]]
|
|
2695
|
+
self.assertIn(public, chargers)
|
|
2696
|
+
self.assertNotIn(private, chargers)
|
|
2697
|
+
|
|
2698
|
+
list_response = self.client.get(reverse("charger-list"))
|
|
2699
|
+
payload = list_response.json()
|
|
2700
|
+
ids = [item["charger_id"] for item in payload["chargers"]]
|
|
2701
|
+
self.assertIn(public.charger_id, ids)
|
|
2702
|
+
self.assertNotIn(private.charger_id, ids)
|