arthexis 0.1.7__py3-none-any.whl → 0.1.9__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/METADATA +168 -0
- arthexis-0.1.9.dist-info/RECORD +92 -0
- arthexis-0.1.9.dist-info/licenses/LICENSE +674 -0
- config/__init__.py +0 -1
- config/auth_app.py +0 -1
- config/celery.py +1 -2
- config/context_processors.py +1 -1
- config/offline.py +2 -0
- config/settings.py +134 -16
- config/urls.py +71 -3
- core/admin.py +1331 -165
- core/admin_history.py +50 -0
- core/admindocs.py +151 -0
- core/apps.py +158 -3
- core/backends.py +46 -4
- core/entity.py +62 -48
- core/fields.py +6 -1
- core/github_helper.py +25 -0
- core/github_issues.py +172 -0
- core/lcd_screen.py +1 -0
- core/liveupdate.py +25 -0
- core/log_paths.py +100 -0
- core/mailer.py +83 -0
- core/middleware.py +57 -0
- core/models.py +1136 -259
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/release.py +27 -20
- core/sigil_builder.py +131 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +129 -10
- core/tasks.py +118 -19
- core/test_system_info.py +22 -0
- core/tests.py +445 -58
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +329 -167
- core/views.py +383 -57
- core/widgets.py +51 -0
- core/workgroup_urls.py +17 -0
- core/workgroup_views.py +94 -0
- nodes/actions.py +0 -2
- nodes/admin.py +159 -284
- nodes/apps.py +9 -15
- nodes/backends.py +53 -0
- nodes/lcd.py +24 -10
- nodes/models.py +375 -178
- nodes/tasks.py +1 -5
- nodes/tests.py +524 -129
- nodes/utils.py +13 -2
- nodes/views.py +66 -23
- ocpp/admin.py +150 -61
- ocpp/apps.py +4 -3
- ocpp/consumers.py +432 -69
- ocpp/evcs.py +25 -8
- ocpp/models.py +408 -68
- ocpp/simulator.py +13 -6
- ocpp/store.py +258 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1198 -135
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +654 -101
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +19 -6
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +759 -40
- pages/urls.py +3 -0
- pages/utils.py +0 -1
- pages/views.py +576 -25
- arthexis-0.1.7.dist-info/METADATA +0 -126
- arthexis-0.1.7.dist-info/RECORD +0 -77
- arthexis-0.1.7.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.7.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
- {arthexis-0.1.7.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
ocpp/tests.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
from asgiref.testing import ApplicationCommunicator
|
|
2
2
|
from channels.testing import WebsocketCommunicator
|
|
3
3
|
from channels.db import database_sync_to_async
|
|
4
|
-
from
|
|
4
|
+
from asgiref.sync import async_to_sync
|
|
5
|
+
from django.test import Client, TransactionTestCase, TestCase, override_settings
|
|
5
6
|
from unittest import skip
|
|
6
7
|
from unittest.mock import patch
|
|
7
8
|
from django.contrib.auth import get_user_model
|
|
8
9
|
from django.urls import reverse
|
|
9
10
|
from django.utils import timezone
|
|
11
|
+
from django.utils.dateparse import parse_datetime
|
|
12
|
+
from django.utils.translation import override, gettext as _
|
|
10
13
|
from django.contrib.sites.models import Site
|
|
11
14
|
from pages.models import Application, Module
|
|
12
15
|
from nodes.models import Node, NodeRole
|
|
@@ -25,13 +28,78 @@ import asyncio
|
|
|
25
28
|
from pathlib import Path
|
|
26
29
|
from .simulator import SimulatorConfig, ChargePointSimulator
|
|
27
30
|
import re
|
|
28
|
-
from datetime import timedelta
|
|
31
|
+
from datetime import datetime, timedelta
|
|
29
32
|
from .tasks import purge_meter_readings
|
|
33
|
+
from django.db import close_old_connections
|
|
34
|
+
from django.db.utils import OperationalError
|
|
35
|
+
from urllib.parse import unquote, urlparse
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ClientWebsocketCommunicator(WebsocketCommunicator):
|
|
39
|
+
"""WebsocketCommunicator that injects a client address into the scope."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
application,
|
|
44
|
+
path,
|
|
45
|
+
*,
|
|
46
|
+
client=None,
|
|
47
|
+
headers=None,
|
|
48
|
+
subprotocols=None,
|
|
49
|
+
spec_version=None,
|
|
50
|
+
):
|
|
51
|
+
if not isinstance(path, str):
|
|
52
|
+
raise TypeError(f"Expected str, got {type(path)}")
|
|
53
|
+
parsed = urlparse(path)
|
|
54
|
+
scope = {
|
|
55
|
+
"type": "websocket",
|
|
56
|
+
"path": unquote(parsed.path),
|
|
57
|
+
"query_string": parsed.query.encode("utf-8"),
|
|
58
|
+
"headers": headers or [],
|
|
59
|
+
"subprotocols": subprotocols or [],
|
|
60
|
+
}
|
|
61
|
+
if client is not None:
|
|
62
|
+
scope["client"] = client
|
|
63
|
+
if spec_version:
|
|
64
|
+
scope["spec_version"] = spec_version
|
|
65
|
+
self.scope = scope
|
|
66
|
+
ApplicationCommunicator.__init__(self, application, self.scope)
|
|
67
|
+
self.response_headers = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DummyWebSocket:
|
|
71
|
+
"""Simple websocket stub that records payloads sent by the view."""
|
|
30
72
|
|
|
73
|
+
def __init__(self):
|
|
74
|
+
self.sent: list[str] = []
|
|
75
|
+
|
|
76
|
+
async def send(self, message):
|
|
77
|
+
self.sent.append(message)
|
|
31
78
|
|
|
32
79
|
|
|
33
80
|
class ChargerFixtureTests(TestCase):
|
|
34
|
-
fixtures = [
|
|
81
|
+
fixtures = [
|
|
82
|
+
p.name
|
|
83
|
+
for p in (Path(__file__).resolve().parent / "fixtures").glob(
|
|
84
|
+
"initial_data__*.json"
|
|
85
|
+
)
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def setUpTestData(cls):
|
|
90
|
+
location = Location.objects.create(name="Simulator")
|
|
91
|
+
Charger.objects.create(
|
|
92
|
+
charger_id="CP1",
|
|
93
|
+
connector_id=1,
|
|
94
|
+
location=location,
|
|
95
|
+
require_rfid=False,
|
|
96
|
+
)
|
|
97
|
+
Charger.objects.create(
|
|
98
|
+
charger_id="CP2",
|
|
99
|
+
connector_id=2,
|
|
100
|
+
location=location,
|
|
101
|
+
require_rfid=True,
|
|
102
|
+
)
|
|
35
103
|
|
|
36
104
|
def test_cp2_requires_rfid(self):
|
|
37
105
|
cp2 = Charger.objects.get(charger_id="CP2")
|
|
@@ -44,12 +112,26 @@ class ChargerFixtureTests(TestCase):
|
|
|
44
112
|
def test_charger_connector_ids(self):
|
|
45
113
|
cp1 = Charger.objects.get(charger_id="CP1")
|
|
46
114
|
cp2 = Charger.objects.get(charger_id="CP2")
|
|
47
|
-
self.assertEqual(cp1.connector_id,
|
|
48
|
-
self.assertEqual(cp2.connector_id,
|
|
115
|
+
self.assertEqual(cp1.connector_id, 1)
|
|
116
|
+
self.assertEqual(cp2.connector_id, 2)
|
|
49
117
|
self.assertEqual(cp1.name, "Simulator #1")
|
|
50
118
|
self.assertEqual(cp2.name, "Simulator #2")
|
|
51
119
|
|
|
52
120
|
|
|
121
|
+
class ChargerUrlFallbackTests(TestCase):
|
|
122
|
+
@override_settings(ALLOWED_HOSTS=["fallback.example", "10.0.0.0/8"])
|
|
123
|
+
def test_reference_created_when_site_missing(self):
|
|
124
|
+
Site.objects.all().delete()
|
|
125
|
+
Site.objects.clear_cache()
|
|
126
|
+
|
|
127
|
+
charger = Charger.objects.create(charger_id="NO_SITE")
|
|
128
|
+
charger.refresh_from_db()
|
|
129
|
+
|
|
130
|
+
self.assertIsNotNone(charger.reference)
|
|
131
|
+
self.assertTrue(charger.reference.value.startswith("http://fallback.example"))
|
|
132
|
+
self.assertTrue(charger.reference.value.endswith("/c/NO_SITE/"))
|
|
133
|
+
|
|
134
|
+
|
|
53
135
|
class SinkConsumerTests(TransactionTestCase):
|
|
54
136
|
async def test_sink_replies(self):
|
|
55
137
|
communicator = WebsocketCommunicator(application, "/ws/sink/")
|
|
@@ -64,17 +146,40 @@ class SinkConsumerTests(TransactionTestCase):
|
|
|
64
146
|
|
|
65
147
|
|
|
66
148
|
class CSMSConsumerTests(TransactionTestCase):
|
|
149
|
+
async def _retry_db(self, func, attempts: int = 5, delay: float = 0.1):
|
|
150
|
+
"""Run a database function, retrying if the database is locked."""
|
|
151
|
+
for _ in range(attempts):
|
|
152
|
+
try:
|
|
153
|
+
return await database_sync_to_async(func)()
|
|
154
|
+
except OperationalError:
|
|
155
|
+
await database_sync_to_async(close_old_connections)()
|
|
156
|
+
await asyncio.sleep(delay)
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
async def _send_status_notification(self, serial: str, payload: dict):
|
|
160
|
+
communicator = WebsocketCommunicator(application, f"/{serial}/")
|
|
161
|
+
connected, _ = await communicator.connect()
|
|
162
|
+
self.assertTrue(connected)
|
|
163
|
+
|
|
164
|
+
await communicator.send_json_to([2, "1", "StatusNotification", payload])
|
|
165
|
+
response = await communicator.receive_json_from()
|
|
166
|
+
self.assertEqual(response, [3, "1", {}])
|
|
167
|
+
|
|
168
|
+
await communicator.disconnect()
|
|
169
|
+
|
|
67
170
|
async def test_transaction_saved(self):
|
|
68
171
|
communicator = WebsocketCommunicator(application, "/TEST/")
|
|
69
172
|
connected, _ = await communicator.connect()
|
|
70
173
|
self.assertTrue(connected)
|
|
71
174
|
|
|
72
|
-
await communicator.send_json_to(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
175
|
+
await communicator.send_json_to(
|
|
176
|
+
[
|
|
177
|
+
2,
|
|
178
|
+
"1",
|
|
179
|
+
"StartTransaction",
|
|
180
|
+
{"meterStart": 10, "connectorId": 3},
|
|
181
|
+
]
|
|
182
|
+
)
|
|
78
183
|
response = await communicator.receive_json_from()
|
|
79
184
|
tx_id = response[2]["transactionId"]
|
|
80
185
|
|
|
@@ -82,14 +187,17 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
82
187
|
pk=tx_id, charger__charger_id="TEST"
|
|
83
188
|
)
|
|
84
189
|
self.assertEqual(tx.meter_start, 10)
|
|
190
|
+
self.assertEqual(tx.connector_id, 3)
|
|
85
191
|
self.assertIsNone(tx.stop_time)
|
|
86
192
|
|
|
87
|
-
await communicator.send_json_to(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
193
|
+
await communicator.send_json_to(
|
|
194
|
+
[
|
|
195
|
+
2,
|
|
196
|
+
"2",
|
|
197
|
+
"StopTransaction",
|
|
198
|
+
{"transactionId": tx_id, "meterStop": 20},
|
|
199
|
+
]
|
|
200
|
+
)
|
|
93
201
|
await communicator.receive_json_from()
|
|
94
202
|
|
|
95
203
|
await database_sync_to_async(tx.refresh_from_db)()
|
|
@@ -117,6 +225,214 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
117
225
|
|
|
118
226
|
await communicator.disconnect()
|
|
119
227
|
|
|
228
|
+
async def test_start_transaction_sends_net_message(self):
|
|
229
|
+
location = await database_sync_to_async(Location.objects.create)(
|
|
230
|
+
name="Test Location"
|
|
231
|
+
)
|
|
232
|
+
await database_sync_to_async(Charger.objects.create)(
|
|
233
|
+
charger_id="NETMSG", location=location
|
|
234
|
+
)
|
|
235
|
+
communicator = WebsocketCommunicator(application, "/NETMSG/")
|
|
236
|
+
connected, _ = await communicator.connect()
|
|
237
|
+
self.assertTrue(connected)
|
|
238
|
+
|
|
239
|
+
with patch("nodes.models.NetMessage.broadcast") as mock_broadcast:
|
|
240
|
+
await communicator.send_json_to(
|
|
241
|
+
[
|
|
242
|
+
2,
|
|
243
|
+
"1",
|
|
244
|
+
"StartTransaction",
|
|
245
|
+
{"meterStart": 1, "connectorId": 1},
|
|
246
|
+
]
|
|
247
|
+
)
|
|
248
|
+
await communicator.receive_json_from()
|
|
249
|
+
|
|
250
|
+
await communicator.disconnect()
|
|
251
|
+
|
|
252
|
+
mock_broadcast.assert_called_once()
|
|
253
|
+
_, kwargs = mock_broadcast.call_args
|
|
254
|
+
self.assertEqual(kwargs["subject"], "charging-started")
|
|
255
|
+
payload = json.loads(kwargs["body"])
|
|
256
|
+
self.assertEqual(payload["location"], "Test Location")
|
|
257
|
+
self.assertEqual(payload["sn"], "NETMSG")
|
|
258
|
+
self.assertEqual(payload["cid"], "1")
|
|
259
|
+
|
|
260
|
+
async def test_rfid_unbound_instance_created(self):
|
|
261
|
+
await database_sync_to_async(Charger.objects.create)(charger_id="NEWRFID")
|
|
262
|
+
communicator = WebsocketCommunicator(application, "/NEWRFID/")
|
|
263
|
+
connected, _ = await communicator.connect()
|
|
264
|
+
self.assertTrue(connected)
|
|
265
|
+
|
|
266
|
+
await communicator.send_json_to(
|
|
267
|
+
[2, "1", "StartTransaction", {"meterStart": 1, "idTag": "TAG456"}]
|
|
268
|
+
)
|
|
269
|
+
await communicator.receive_json_from()
|
|
270
|
+
|
|
271
|
+
tag = await database_sync_to_async(RFID.objects.get)(rfid="TAG456")
|
|
272
|
+
count = await database_sync_to_async(tag.energy_accounts.count)()
|
|
273
|
+
self.assertEqual(count, 0)
|
|
274
|
+
|
|
275
|
+
await communicator.disconnect()
|
|
276
|
+
|
|
277
|
+
async def test_firmware_status_notification_updates_database_and_views(self):
|
|
278
|
+
communicator = WebsocketCommunicator(application, "/FWSTAT/")
|
|
279
|
+
connected, _ = await communicator.connect()
|
|
280
|
+
self.assertTrue(connected)
|
|
281
|
+
|
|
282
|
+
ts = timezone.now().replace(microsecond=0)
|
|
283
|
+
payload = {
|
|
284
|
+
"status": "Installing",
|
|
285
|
+
"statusInfo": "Applying patch",
|
|
286
|
+
"timestamp": ts.isoformat(),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
await communicator.send_json_to(
|
|
290
|
+
[2, "1", "FirmwareStatusNotification", payload]
|
|
291
|
+
)
|
|
292
|
+
response = await communicator.receive_json_from()
|
|
293
|
+
self.assertEqual(response, [3, "1", {}])
|
|
294
|
+
|
|
295
|
+
def _fetch_status():
|
|
296
|
+
charger = Charger.objects.get(charger_id="FWSTAT", connector_id=None)
|
|
297
|
+
return (
|
|
298
|
+
charger.firmware_status,
|
|
299
|
+
charger.firmware_status_info,
|
|
300
|
+
charger.firmware_timestamp,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
status, info, recorded_ts = await database_sync_to_async(_fetch_status)()
|
|
304
|
+
self.assertEqual(status, "Installing")
|
|
305
|
+
self.assertEqual(info, "Applying patch")
|
|
306
|
+
self.assertIsNotNone(recorded_ts)
|
|
307
|
+
self.assertEqual(recorded_ts.replace(microsecond=0), ts)
|
|
308
|
+
|
|
309
|
+
log_entries = store.get_logs(store.identity_key("FWSTAT", None), log_type="charger")
|
|
310
|
+
self.assertTrue(
|
|
311
|
+
any("FirmwareStatusNotification" in entry for entry in log_entries)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _fetch_views():
|
|
315
|
+
User = get_user_model()
|
|
316
|
+
user = User.objects.create_user(username="fwstatus", password="pw")
|
|
317
|
+
client = Client()
|
|
318
|
+
client.force_login(user)
|
|
319
|
+
detail = client.get(reverse("charger-detail", args=["FWSTAT"]))
|
|
320
|
+
status_page = client.get(reverse("charger-status", args=["FWSTAT"]))
|
|
321
|
+
list_response = client.get(reverse("charger-list"))
|
|
322
|
+
return (
|
|
323
|
+
detail.status_code,
|
|
324
|
+
json.loads(detail.content.decode()),
|
|
325
|
+
status_page.status_code,
|
|
326
|
+
status_page.content.decode(),
|
|
327
|
+
list_response.status_code,
|
|
328
|
+
json.loads(list_response.content.decode()),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
(
|
|
332
|
+
detail_code,
|
|
333
|
+
detail_payload,
|
|
334
|
+
status_code,
|
|
335
|
+
html,
|
|
336
|
+
list_code,
|
|
337
|
+
list_payload,
|
|
338
|
+
) = await database_sync_to_async(_fetch_views)()
|
|
339
|
+
self.assertEqual(detail_code, 200)
|
|
340
|
+
self.assertEqual(status_code, 200)
|
|
341
|
+
self.assertEqual(list_code, 200)
|
|
342
|
+
self.assertEqual(detail_payload["firmwareStatus"], "Installing")
|
|
343
|
+
self.assertEqual(detail_payload["firmwareStatusInfo"], "Applying patch")
|
|
344
|
+
self.assertEqual(detail_payload["firmwareTimestamp"], ts.isoformat())
|
|
345
|
+
self.assertIn('id="firmware-status">Installing<', html)
|
|
346
|
+
self.assertIn('id="firmware-status-info">Applying patch<', html)
|
|
347
|
+
match = re.search(
|
|
348
|
+
r'id="firmware-timestamp"[^>]*data-iso="([^"]+)"', html
|
|
349
|
+
)
|
|
350
|
+
self.assertIsNotNone(match)
|
|
351
|
+
parsed_iso = datetime.fromisoformat(match.group(1))
|
|
352
|
+
self.assertAlmostEqual(parsed_iso.timestamp(), ts.timestamp(), places=3)
|
|
353
|
+
|
|
354
|
+
matching = [
|
|
355
|
+
item
|
|
356
|
+
for item in list_payload.get("chargers", [])
|
|
357
|
+
if item["charger_id"] == "FWSTAT" and item["connector_id"] is None
|
|
358
|
+
]
|
|
359
|
+
self.assertTrue(matching)
|
|
360
|
+
self.assertEqual(matching[0]["firmwareStatus"], "Installing")
|
|
361
|
+
self.assertEqual(matching[0]["firmwareStatusInfo"], "Applying patch")
|
|
362
|
+
list_ts = datetime.fromisoformat(matching[0]["firmwareTimestamp"])
|
|
363
|
+
self.assertAlmostEqual(list_ts.timestamp(), ts.timestamp(), places=3)
|
|
364
|
+
|
|
365
|
+
store.clear_log(store.identity_key("FWSTAT", None), log_type="charger")
|
|
366
|
+
|
|
367
|
+
await communicator.disconnect()
|
|
368
|
+
|
|
369
|
+
async def test_firmware_status_notification_updates_connector_and_aggregate(
|
|
370
|
+
self,
|
|
371
|
+
):
|
|
372
|
+
communicator = WebsocketCommunicator(application, "/FWCONN/")
|
|
373
|
+
connected, _ = await communicator.connect()
|
|
374
|
+
self.assertTrue(connected)
|
|
375
|
+
|
|
376
|
+
await communicator.send_json_to(
|
|
377
|
+
[
|
|
378
|
+
2,
|
|
379
|
+
"1",
|
|
380
|
+
"FirmwareStatusNotification",
|
|
381
|
+
{"connectorId": 2, "status": "Downloaded"},
|
|
382
|
+
]
|
|
383
|
+
)
|
|
384
|
+
response = await communicator.receive_json_from()
|
|
385
|
+
self.assertEqual(response, [3, "1", {}])
|
|
386
|
+
|
|
387
|
+
def _fetch_chargers():
|
|
388
|
+
aggregate = Charger.objects.get(charger_id="FWCONN", connector_id=None)
|
|
389
|
+
connector = Charger.objects.get(charger_id="FWCONN", connector_id=2)
|
|
390
|
+
return (
|
|
391
|
+
aggregate.firmware_status,
|
|
392
|
+
aggregate.firmware_status_info,
|
|
393
|
+
aggregate.firmware_timestamp,
|
|
394
|
+
connector.firmware_status,
|
|
395
|
+
connector.firmware_status_info,
|
|
396
|
+
connector.firmware_timestamp,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
(
|
|
400
|
+
aggregate_status,
|
|
401
|
+
aggregate_info,
|
|
402
|
+
aggregate_ts,
|
|
403
|
+
connector_status,
|
|
404
|
+
connector_info,
|
|
405
|
+
connector_ts,
|
|
406
|
+
) = await database_sync_to_async(_fetch_chargers)()
|
|
407
|
+
|
|
408
|
+
self.assertEqual(aggregate_status, "Downloaded")
|
|
409
|
+
self.assertEqual(connector_status, "Downloaded")
|
|
410
|
+
self.assertEqual(aggregate_info, "")
|
|
411
|
+
self.assertEqual(connector_info, "")
|
|
412
|
+
self.assertIsNotNone(aggregate_ts)
|
|
413
|
+
self.assertIsNotNone(connector_ts)
|
|
414
|
+
self.assertAlmostEqual(
|
|
415
|
+
(connector_ts - aggregate_ts).total_seconds(), 0, delta=1.0
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
log_entries = store.get_logs(
|
|
419
|
+
store.identity_key("FWCONN", 2), log_type="charger"
|
|
420
|
+
)
|
|
421
|
+
self.assertTrue(
|
|
422
|
+
any("FirmwareStatusNotification" in entry for entry in log_entries)
|
|
423
|
+
)
|
|
424
|
+
log_entries_agg = store.get_logs(
|
|
425
|
+
store.identity_key("FWCONN", None), log_type="charger"
|
|
426
|
+
)
|
|
427
|
+
self.assertTrue(
|
|
428
|
+
any("FirmwareStatusNotification" in entry for entry in log_entries_agg)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
store.clear_log(store.identity_key("FWCONN", 2), log_type="charger")
|
|
432
|
+
store.clear_log(store.identity_key("FWCONN", None), log_type="charger")
|
|
433
|
+
|
|
434
|
+
await communicator.disconnect()
|
|
435
|
+
|
|
120
436
|
async def test_vin_recorded(self):
|
|
121
437
|
await database_sync_to_async(Charger.objects.create)(charger_id="VINREC")
|
|
122
438
|
communicator = WebsocketCommunicator(application, "/VINREC/")
|
|
@@ -153,10 +469,65 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
153
469
|
await communicator.send_json_to([2, "1", "MeterValues", payload])
|
|
154
470
|
await communicator.receive_json_from()
|
|
155
471
|
|
|
156
|
-
charger = await database_sync_to_async(Charger.objects.get)(
|
|
157
|
-
|
|
472
|
+
charger = await database_sync_to_async(Charger.objects.get)(
|
|
473
|
+
charger_id="NEWCID", connector_id=7
|
|
474
|
+
)
|
|
475
|
+
self.assertEqual(charger.connector_id, 7)
|
|
476
|
+
|
|
477
|
+
await communicator.disconnect()
|
|
158
478
|
|
|
479
|
+
async def test_new_charger_created_for_different_connector(self):
|
|
480
|
+
communicator = WebsocketCommunicator(application, "/DUPC/")
|
|
481
|
+
connected, _ = await communicator.connect()
|
|
482
|
+
self.assertTrue(connected)
|
|
483
|
+
|
|
484
|
+
payload1 = {
|
|
485
|
+
"connectorId": 1,
|
|
486
|
+
"meterValue": [
|
|
487
|
+
{
|
|
488
|
+
"timestamp": timezone.now().isoformat(),
|
|
489
|
+
"sampledValue": [{"value": "1"}],
|
|
490
|
+
}
|
|
491
|
+
],
|
|
492
|
+
}
|
|
493
|
+
await communicator.send_json_to([2, "1", "MeterValues", payload1])
|
|
494
|
+
await communicator.receive_json_from()
|
|
159
495
|
await communicator.disconnect()
|
|
496
|
+
await communicator.wait()
|
|
497
|
+
await database_sync_to_async(close_old_connections)()
|
|
498
|
+
|
|
499
|
+
communicator = WebsocketCommunicator(application, "/DUPC/")
|
|
500
|
+
connected, _ = await communicator.connect()
|
|
501
|
+
self.assertTrue(connected)
|
|
502
|
+
payload2 = {
|
|
503
|
+
"connectorId": 2,
|
|
504
|
+
"meterValue": [
|
|
505
|
+
{
|
|
506
|
+
"timestamp": timezone.now().isoformat(),
|
|
507
|
+
"sampledValue": [{"value": "1"}],
|
|
508
|
+
}
|
|
509
|
+
],
|
|
510
|
+
}
|
|
511
|
+
await communicator.send_json_to([2, "1", "MeterValues", payload2])
|
|
512
|
+
await communicator.receive_json_from()
|
|
513
|
+
await communicator.disconnect()
|
|
514
|
+
await communicator.wait()
|
|
515
|
+
await database_sync_to_async(close_old_connections)()
|
|
516
|
+
|
|
517
|
+
count = await self._retry_db(
|
|
518
|
+
lambda: Charger.objects.filter(charger_id="DUPC").count()
|
|
519
|
+
)
|
|
520
|
+
self.assertEqual(count, 3)
|
|
521
|
+
connectors = await self._retry_db(
|
|
522
|
+
lambda: list(
|
|
523
|
+
Charger.objects.filter(charger_id="DUPC").values_list(
|
|
524
|
+
"connector_id", flat=True
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
self.assertIn(1, connectors)
|
|
529
|
+
self.assertIn(2, connectors)
|
|
530
|
+
self.assertIn(None, connectors)
|
|
160
531
|
|
|
161
532
|
async def test_transaction_created_from_meter_values(self):
|
|
162
533
|
communicator = WebsocketCommunicator(application, "/NOSTART/")
|
|
@@ -208,6 +579,58 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
208
579
|
|
|
209
580
|
await communicator.disconnect()
|
|
210
581
|
|
|
582
|
+
async def test_diagnostics_status_notification_updates_records(self):
|
|
583
|
+
communicator = WebsocketCommunicator(application, "/DIAGCP/")
|
|
584
|
+
connected, _ = await communicator.connect()
|
|
585
|
+
self.assertTrue(connected)
|
|
586
|
+
|
|
587
|
+
reported_at = timezone.now().replace(microsecond=0)
|
|
588
|
+
payload = {
|
|
589
|
+
"status": "Uploaded",
|
|
590
|
+
"connectorId": 5,
|
|
591
|
+
"uploadLocation": "https://example.com/diag.tar",
|
|
592
|
+
"timestamp": reported_at.isoformat(),
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
await communicator.send_json_to(
|
|
596
|
+
[2, "1", "DiagnosticsStatusNotification", payload]
|
|
597
|
+
)
|
|
598
|
+
response = await communicator.receive_json_from()
|
|
599
|
+
self.assertEqual(response[0], 3)
|
|
600
|
+
self.assertEqual(response[2], {})
|
|
601
|
+
|
|
602
|
+
def _fetch():
|
|
603
|
+
aggregate = Charger.objects.get(charger_id="DIAGCP", connector_id=None)
|
|
604
|
+
connector = Charger.objects.get(charger_id="DIAGCP", connector_id=5)
|
|
605
|
+
return aggregate, connector
|
|
606
|
+
|
|
607
|
+
aggregate, connector = await database_sync_to_async(_fetch)()
|
|
608
|
+
self.assertEqual(aggregate.diagnostics_status, "Uploaded")
|
|
609
|
+
self.assertEqual(connector.diagnostics_status, "Uploaded")
|
|
610
|
+
self.assertEqual(
|
|
611
|
+
aggregate.diagnostics_location, "https://example.com/diag.tar"
|
|
612
|
+
)
|
|
613
|
+
self.assertEqual(
|
|
614
|
+
connector.diagnostics_location, "https://example.com/diag.tar"
|
|
615
|
+
)
|
|
616
|
+
self.assertEqual(aggregate.diagnostics_timestamp, reported_at)
|
|
617
|
+
self.assertEqual(connector.diagnostics_timestamp, reported_at)
|
|
618
|
+
|
|
619
|
+
connector_logs = store.get_logs(
|
|
620
|
+
store.identity_key("DIAGCP", 5), log_type="charger"
|
|
621
|
+
)
|
|
622
|
+
aggregate_logs = store.get_logs(
|
|
623
|
+
store.identity_key("DIAGCP", None), log_type="charger"
|
|
624
|
+
)
|
|
625
|
+
self.assertTrue(
|
|
626
|
+
any("DiagnosticsStatusNotification" in entry for entry in connector_logs)
|
|
627
|
+
)
|
|
628
|
+
self.assertTrue(
|
|
629
|
+
any("DiagnosticsStatusNotification" in entry for entry in aggregate_logs)
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
await communicator.disconnect()
|
|
633
|
+
|
|
211
634
|
async def test_temperature_recorded(self):
|
|
212
635
|
charger = await database_sync_to_async(Charger.objects.create)(
|
|
213
636
|
charger_id="TEMP1"
|
|
@@ -245,6 +668,106 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
245
668
|
|
|
246
669
|
await communicator.disconnect()
|
|
247
670
|
|
|
671
|
+
def test_status_notification_updates_models_and_views(self):
|
|
672
|
+
serial = "STATUS-CP"
|
|
673
|
+
payload = {
|
|
674
|
+
"connectorId": 1,
|
|
675
|
+
"status": "Faulted",
|
|
676
|
+
"errorCode": "GroundFailure",
|
|
677
|
+
"info": "Relay malfunction",
|
|
678
|
+
"vendorId": "ACME",
|
|
679
|
+
"timestamp": "2024-01-01T12:34:56Z",
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async_to_sync(self._send_status_notification)(serial, payload)
|
|
683
|
+
|
|
684
|
+
expected_ts = parse_datetime(payload["timestamp"])
|
|
685
|
+
aggregate = Charger.objects.get(charger_id=serial, connector_id=None)
|
|
686
|
+
connector = Charger.objects.get(charger_id=serial, connector_id=1)
|
|
687
|
+
|
|
688
|
+
vendor_data = {"info": payload["info"], "vendorId": payload["vendorId"]}
|
|
689
|
+
self.assertEqual(aggregate.last_status, payload["status"])
|
|
690
|
+
self.assertEqual(aggregate.last_error_code, payload["errorCode"])
|
|
691
|
+
self.assertEqual(aggregate.last_status_vendor_info, vendor_data)
|
|
692
|
+
self.assertEqual(aggregate.last_status_timestamp, expected_ts)
|
|
693
|
+
self.assertEqual(connector.last_status, payload["status"])
|
|
694
|
+
self.assertEqual(connector.last_error_code, payload["errorCode"])
|
|
695
|
+
self.assertEqual(connector.last_status_vendor_info, vendor_data)
|
|
696
|
+
self.assertEqual(connector.last_status_timestamp, expected_ts)
|
|
697
|
+
|
|
698
|
+
connector_log = store.get_logs(
|
|
699
|
+
store.identity_key(serial, 1), log_type="charger"
|
|
700
|
+
)
|
|
701
|
+
self.assertTrue(
|
|
702
|
+
any("StatusNotification processed" in entry for entry in connector_log)
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
user = get_user_model().objects.create_user(
|
|
706
|
+
username="status", email="status@example.com", password="pwd"
|
|
707
|
+
)
|
|
708
|
+
self.client.force_login(user)
|
|
709
|
+
|
|
710
|
+
list_response = self.client.get(reverse("charger-list"))
|
|
711
|
+
self.assertEqual(list_response.status_code, 200)
|
|
712
|
+
chargers = list_response.json()["chargers"]
|
|
713
|
+
aggregate_entry = next(
|
|
714
|
+
item
|
|
715
|
+
for item in chargers
|
|
716
|
+
if item["charger_id"] == serial and item["connector_id"] is None
|
|
717
|
+
)
|
|
718
|
+
connector_entry = next(
|
|
719
|
+
item
|
|
720
|
+
for item in chargers
|
|
721
|
+
if item["charger_id"] == serial and item["connector_id"] == 1
|
|
722
|
+
)
|
|
723
|
+
expected_iso = expected_ts.isoformat()
|
|
724
|
+
self.assertEqual(aggregate_entry["lastStatus"], payload["status"])
|
|
725
|
+
self.assertEqual(aggregate_entry["lastErrorCode"], payload["errorCode"])
|
|
726
|
+
self.assertEqual(aggregate_entry["lastStatusVendorInfo"], vendor_data)
|
|
727
|
+
self.assertEqual(aggregate_entry["lastStatusTimestamp"], expected_iso)
|
|
728
|
+
self.assertEqual(aggregate_entry["status"], "Faulted (GroundFailure)")
|
|
729
|
+
self.assertEqual(aggregate_entry["statusColor"], "#dc3545")
|
|
730
|
+
self.assertEqual(connector_entry["lastStatus"], payload["status"])
|
|
731
|
+
self.assertEqual(connector_entry["lastErrorCode"], payload["errorCode"])
|
|
732
|
+
self.assertEqual(connector_entry["lastStatusVendorInfo"], vendor_data)
|
|
733
|
+
self.assertEqual(connector_entry["lastStatusTimestamp"], expected_iso)
|
|
734
|
+
self.assertEqual(connector_entry["status"], "Faulted (GroundFailure)")
|
|
735
|
+
self.assertEqual(connector_entry["statusColor"], "#dc3545")
|
|
736
|
+
|
|
737
|
+
detail_response = self.client.get(
|
|
738
|
+
reverse("charger-detail-connector", args=[serial, 1])
|
|
739
|
+
)
|
|
740
|
+
self.assertEqual(detail_response.status_code, 200)
|
|
741
|
+
detail_payload = detail_response.json()
|
|
742
|
+
self.assertEqual(detail_payload["lastStatus"], payload["status"])
|
|
743
|
+
self.assertEqual(detail_payload["lastErrorCode"], payload["errorCode"])
|
|
744
|
+
self.assertEqual(detail_payload["lastStatusVendorInfo"], vendor_data)
|
|
745
|
+
self.assertEqual(detail_payload["lastStatusTimestamp"], expected_iso)
|
|
746
|
+
self.assertEqual(detail_payload["status"], "Faulted (GroundFailure)")
|
|
747
|
+
self.assertEqual(detail_payload["statusColor"], "#dc3545")
|
|
748
|
+
|
|
749
|
+
status_resp = self.client.get(
|
|
750
|
+
reverse("charger-status-connector", args=[serial, "1"])
|
|
751
|
+
)
|
|
752
|
+
self.assertContains(status_resp, "Faulted (GroundFailure)")
|
|
753
|
+
self.assertContains(status_resp, "Error code: GroundFailure")
|
|
754
|
+
self.assertContains(status_resp, "Vendor: ACME")
|
|
755
|
+
self.assertContains(status_resp, "Info: Relay malfunction")
|
|
756
|
+
self.assertContains(status_resp, "background-color: #dc3545")
|
|
757
|
+
|
|
758
|
+
aggregate_status = self.client.get(reverse("charger-status", args=[serial]))
|
|
759
|
+
self.assertContains(aggregate_status, "Reported status")
|
|
760
|
+
self.assertContains(aggregate_status, "Info: Relay malfunction")
|
|
761
|
+
|
|
762
|
+
page_resp = self.client.get(reverse("charger-page", args=[serial]))
|
|
763
|
+
self.assertContains(page_resp, "Faulted (GroundFailure)")
|
|
764
|
+
self.assertContains(page_resp, "Vendor")
|
|
765
|
+
self.assertContains(page_resp, "Relay malfunction")
|
|
766
|
+
self.assertContains(page_resp, "background-color: #dc3545")
|
|
767
|
+
|
|
768
|
+
store.clear_log(store.identity_key(serial, 1), log_type="charger")
|
|
769
|
+
store.clear_log(store.identity_key(serial, None), log_type="charger")
|
|
770
|
+
|
|
248
771
|
async def test_message_logged_and_session_file_created(self):
|
|
249
772
|
cid = "LOGTEST1"
|
|
250
773
|
log_path = Path("logs") / f"charger.{cid}.log"
|
|
@@ -258,21 +781,25 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
258
781
|
connected, _ = await communicator.connect()
|
|
259
782
|
self.assertTrue(connected)
|
|
260
783
|
|
|
261
|
-
await communicator.send_json_to(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
784
|
+
await communicator.send_json_to(
|
|
785
|
+
[
|
|
786
|
+
2,
|
|
787
|
+
"1",
|
|
788
|
+
"StartTransaction",
|
|
789
|
+
{"meterStart": 1},
|
|
790
|
+
]
|
|
791
|
+
)
|
|
267
792
|
response = await communicator.receive_json_from()
|
|
268
793
|
tx_id = response[2]["transactionId"]
|
|
269
794
|
|
|
270
|
-
await communicator.send_json_to(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
795
|
+
await communicator.send_json_to(
|
|
796
|
+
[
|
|
797
|
+
2,
|
|
798
|
+
"2",
|
|
799
|
+
"StopTransaction",
|
|
800
|
+
{"transactionId": tx_id, "meterStop": 2},
|
|
801
|
+
]
|
|
802
|
+
)
|
|
276
803
|
await communicator.receive_json_from()
|
|
277
804
|
await communicator.disconnect()
|
|
278
805
|
|
|
@@ -313,12 +840,14 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
313
840
|
connected, _ = await communicator.connect()
|
|
314
841
|
self.assertTrue(connected)
|
|
315
842
|
|
|
316
|
-
await communicator.send_json_to(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
843
|
+
await communicator.send_json_to(
|
|
844
|
+
[
|
|
845
|
+
2,
|
|
846
|
+
"1",
|
|
847
|
+
"StartTransaction",
|
|
848
|
+
{"meterStart": 5},
|
|
849
|
+
]
|
|
850
|
+
)
|
|
322
851
|
await communicator.receive_json_from()
|
|
323
852
|
|
|
324
853
|
await communicator.disconnect()
|
|
@@ -333,7 +862,8 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
333
862
|
communicator1 = WebsocketCommunicator(application, "/DUPLICATE/")
|
|
334
863
|
connected, _ = await communicator1.connect()
|
|
335
864
|
self.assertTrue(connected)
|
|
336
|
-
|
|
865
|
+
pending_key = store.pending_key("DUPLICATE")
|
|
866
|
+
first_consumer = store.connections.get(pending_key)
|
|
337
867
|
|
|
338
868
|
communicator2 = WebsocketCommunicator(application, "/DUPLICATE/")
|
|
339
869
|
connected2, _ = await communicator2.connect()
|
|
@@ -341,9 +871,47 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
341
871
|
|
|
342
872
|
# The first communicator should be closed when the second connects.
|
|
343
873
|
await communicator1.wait()
|
|
344
|
-
self.assertIsNot(store.connections.get(
|
|
874
|
+
self.assertIsNot(store.connections.get(pending_key), first_consumer)
|
|
875
|
+
|
|
876
|
+
await communicator2.disconnect()
|
|
877
|
+
|
|
878
|
+
async def test_connectors_share_serial_without_disconnecting(self):
|
|
879
|
+
communicator1 = WebsocketCommunicator(application, "/MULTI/")
|
|
880
|
+
connected1, _ = await communicator1.connect()
|
|
881
|
+
self.assertTrue(connected1)
|
|
882
|
+
await communicator1.send_json_to(
|
|
883
|
+
[
|
|
884
|
+
2,
|
|
885
|
+
"1",
|
|
886
|
+
"StartTransaction",
|
|
887
|
+
{"connectorId": 1, "meterStart": 10},
|
|
888
|
+
]
|
|
889
|
+
)
|
|
890
|
+
await communicator1.receive_json_from()
|
|
891
|
+
|
|
892
|
+
communicator2 = WebsocketCommunicator(application, "/MULTI/")
|
|
893
|
+
connected2, _ = await communicator2.connect()
|
|
894
|
+
self.assertTrue(connected2)
|
|
895
|
+
await communicator2.send_json_to(
|
|
896
|
+
[
|
|
897
|
+
2,
|
|
898
|
+
"2",
|
|
899
|
+
"StartTransaction",
|
|
900
|
+
{"connectorId": 2, "meterStart": 10},
|
|
901
|
+
]
|
|
902
|
+
)
|
|
903
|
+
await communicator2.receive_json_from()
|
|
904
|
+
|
|
905
|
+
key1 = store.identity_key("MULTI", 1)
|
|
906
|
+
key2 = store.identity_key("MULTI", 2)
|
|
907
|
+
self.assertIn(key1, store.connections)
|
|
908
|
+
self.assertIn(key2, store.connections)
|
|
909
|
+
self.assertIsNot(store.connections[key1], store.connections[key2])
|
|
345
910
|
|
|
911
|
+
await communicator1.disconnect()
|
|
346
912
|
await communicator2.disconnect()
|
|
913
|
+
store.transactions.pop(key1, None)
|
|
914
|
+
store.transactions.pop(key2, None)
|
|
347
915
|
|
|
348
916
|
|
|
349
917
|
class ChargerLandingTests(TestCase):
|
|
@@ -359,10 +927,17 @@ class ChargerLandingTests(TestCase):
|
|
|
359
927
|
|
|
360
928
|
response = self.client.get(reverse("charger-page", args=["PAGE1"]))
|
|
361
929
|
self.assertEqual(response.status_code, 200)
|
|
362
|
-
self.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
930
|
+
self.assertEqual(response.context["LANGUAGE_CODE"], "es")
|
|
931
|
+
with override("es"):
|
|
932
|
+
self.assertContains(
|
|
933
|
+
response,
|
|
934
|
+
_(
|
|
935
|
+
"Plug in your vehicle and slide your RFID card over the reader to begin charging."
|
|
936
|
+
),
|
|
937
|
+
)
|
|
938
|
+
self.assertContains(response, _("Advanced View"))
|
|
939
|
+
status_url = reverse("charger-status-connector", args=["PAGE1", "all"])
|
|
940
|
+
self.assertContains(response, status_url)
|
|
366
941
|
|
|
367
942
|
def test_status_page_renders(self):
|
|
368
943
|
charger = Charger.objects.create(charger_id="PAGE2")
|
|
@@ -377,10 +952,23 @@ class ChargerLandingTests(TestCase):
|
|
|
377
952
|
meter_start=1000,
|
|
378
953
|
start_time=timezone.now(),
|
|
379
954
|
)
|
|
380
|
-
store.
|
|
955
|
+
key = store.identity_key(charger.charger_id, charger.connector_id)
|
|
956
|
+
store.transactions[key] = tx
|
|
381
957
|
resp = self.client.get(reverse("charger-page", args=["STATS"]))
|
|
382
|
-
self.assertContains(resp, "progress")
|
|
383
|
-
store.transactions.pop(
|
|
958
|
+
self.assertContains(resp, "progress-bar")
|
|
959
|
+
store.transactions.pop(key, None)
|
|
960
|
+
|
|
961
|
+
def test_display_name_used_on_public_pages(self):
|
|
962
|
+
charger = Charger.objects.create(
|
|
963
|
+
charger_id="NAMED",
|
|
964
|
+
display_name="Entrada",
|
|
965
|
+
)
|
|
966
|
+
landing = self.client.get(reverse("charger-page", args=["NAMED"]))
|
|
967
|
+
self.assertContains(landing, "Entrada")
|
|
968
|
+
status = self.client.get(
|
|
969
|
+
reverse("charger-status-connector", args=["NAMED", "all"])
|
|
970
|
+
)
|
|
971
|
+
self.assertContains(status, "Entrada")
|
|
384
972
|
|
|
385
973
|
def test_total_includes_ongoing_transaction(self):
|
|
386
974
|
charger = Charger.objects.create(charger_id="ONGOING")
|
|
@@ -389,7 +977,8 @@ class ChargerLandingTests(TestCase):
|
|
|
389
977
|
meter_start=1000,
|
|
390
978
|
start_time=timezone.now(),
|
|
391
979
|
)
|
|
392
|
-
store.
|
|
980
|
+
key = store.identity_key(charger.charger_id, charger.connector_id)
|
|
981
|
+
store.transactions[key] = tx
|
|
393
982
|
MeterReading.objects.create(
|
|
394
983
|
charger=charger,
|
|
395
984
|
transaction=tx,
|
|
@@ -399,10 +988,29 @@ class ChargerLandingTests(TestCase):
|
|
|
399
988
|
unit="W",
|
|
400
989
|
)
|
|
401
990
|
resp = self.client.get(reverse("charger-status", args=["ONGOING"]))
|
|
402
|
-
self.assertContains(
|
|
403
|
-
|
|
991
|
+
self.assertContains(resp, 'Total Energy: <span id="total-kw">1.50</span> kW')
|
|
992
|
+
store.transactions.pop(key, None)
|
|
993
|
+
|
|
994
|
+
def test_connector_specific_routes_render(self):
|
|
995
|
+
Charger.objects.create(charger_id="ROUTED")
|
|
996
|
+
connector = Charger.objects.create(charger_id="ROUTED", connector_id=1)
|
|
997
|
+
page = self.client.get(reverse("charger-page-connector", args=["ROUTED", "1"]))
|
|
998
|
+
self.assertEqual(page.status_code, 200)
|
|
999
|
+
status = self.client.get(
|
|
1000
|
+
reverse("charger-status-connector", args=["ROUTED", "1"])
|
|
1001
|
+
)
|
|
1002
|
+
self.assertEqual(status.status_code, 200)
|
|
1003
|
+
search = self.client.get(
|
|
1004
|
+
reverse("charger-session-search-connector", args=["ROUTED", "1"])
|
|
404
1005
|
)
|
|
405
|
-
|
|
1006
|
+
self.assertEqual(search.status_code, 200)
|
|
1007
|
+
log_id = store.identity_key("ROUTED", connector.connector_id)
|
|
1008
|
+
store.add_log(log_id, "entry", log_type="charger")
|
|
1009
|
+
log = self.client.get(
|
|
1010
|
+
reverse("charger-log-connector", args=["ROUTED", "1"]) + "?type=charger"
|
|
1011
|
+
)
|
|
1012
|
+
self.assertContains(log, "entry")
|
|
1013
|
+
store.clear_log(log_id, log_type="charger")
|
|
406
1014
|
|
|
407
1015
|
def test_temperature_displayed(self):
|
|
408
1016
|
charger = Charger.objects.create(
|
|
@@ -413,20 +1021,22 @@ class ChargerLandingTests(TestCase):
|
|
|
413
1021
|
self.assertContains(resp, "21.5")
|
|
414
1022
|
|
|
415
1023
|
def test_log_page_renders_without_charger(self):
|
|
416
|
-
store.
|
|
417
|
-
|
|
1024
|
+
log_id = store.identity_key("LOG1", None)
|
|
1025
|
+
store.add_log(log_id, "hello", log_type="charger")
|
|
1026
|
+
entry = store.get_logs(log_id, log_type="charger")[0]
|
|
418
1027
|
self.assertRegex(entry, r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} hello$")
|
|
419
1028
|
resp = self.client.get(reverse("charger-log", args=["LOG1"]) + "?type=charger")
|
|
420
1029
|
self.assertEqual(resp.status_code, 200)
|
|
421
1030
|
self.assertContains(resp, "hello")
|
|
422
|
-
store.clear_log(
|
|
1031
|
+
store.clear_log(log_id, log_type="charger")
|
|
423
1032
|
|
|
424
1033
|
def test_log_page_is_case_insensitive(self):
|
|
425
|
-
store.
|
|
1034
|
+
log_id = store.identity_key("cp2", None)
|
|
1035
|
+
store.add_log(log_id, "entry", log_type="charger")
|
|
426
1036
|
resp = self.client.get(reverse("charger-log", args=["CP2"]) + "?type=charger")
|
|
427
1037
|
self.assertEqual(resp.status_code, 200)
|
|
428
1038
|
self.assertContains(resp, "entry")
|
|
429
|
-
store.clear_log(
|
|
1039
|
+
store.clear_log(log_id, log_type="charger")
|
|
430
1040
|
|
|
431
1041
|
|
|
432
1042
|
class SimulatorLandingTests(TestCase):
|
|
@@ -471,7 +1081,7 @@ class ChargerAdminTests(TestCase):
|
|
|
471
1081
|
url = reverse("admin:ocpp_charger_changelist")
|
|
472
1082
|
resp = self.client.get(url)
|
|
473
1083
|
self.assertContains(resp, charger.get_absolute_url())
|
|
474
|
-
status_url = reverse("charger-status", args=["ADMIN1"])
|
|
1084
|
+
status_url = reverse("charger-status-connector", args=["ADMIN1", "all"])
|
|
475
1085
|
self.assertContains(resp, status_url)
|
|
476
1086
|
|
|
477
1087
|
def test_admin_does_not_list_qr_link(self):
|
|
@@ -484,9 +1094,19 @@ class ChargerAdminTests(TestCase):
|
|
|
484
1094
|
charger = Charger.objects.create(charger_id="LOG1")
|
|
485
1095
|
url = reverse("admin:ocpp_charger_changelist")
|
|
486
1096
|
resp = self.client.get(url)
|
|
487
|
-
log_url = reverse("
|
|
1097
|
+
log_url = reverse("admin:ocpp_charger_log", args=[charger.pk])
|
|
488
1098
|
self.assertContains(resp, log_url)
|
|
489
1099
|
|
|
1100
|
+
def test_admin_log_view_displays_entries(self):
|
|
1101
|
+
charger = Charger.objects.create(charger_id="LOG2")
|
|
1102
|
+
log_id = store.identity_key(charger.charger_id, charger.connector_id)
|
|
1103
|
+
store.add_log(log_id, "entry", log_type="charger")
|
|
1104
|
+
url = reverse("admin:ocpp_charger_log", args=[charger.pk])
|
|
1105
|
+
resp = self.client.get(url)
|
|
1106
|
+
self.assertEqual(resp.status_code, 200)
|
|
1107
|
+
self.assertContains(resp, "entry")
|
|
1108
|
+
store.clear_log(log_id, log_type="charger")
|
|
1109
|
+
|
|
490
1110
|
def test_admin_change_links_landing_page(self):
|
|
491
1111
|
charger = Charger.objects.create(charger_id="CHANGE1")
|
|
492
1112
|
url = reverse("admin:ocpp_charger_change", args=[charger.pk])
|
|
@@ -525,12 +1145,14 @@ class ChargerAdminTests(TestCase):
|
|
|
525
1145
|
timestamp=timezone.now(),
|
|
526
1146
|
value=1,
|
|
527
1147
|
)
|
|
528
|
-
store.add_log("PURGE1", "entry", log_type="charger")
|
|
1148
|
+
store.add_log(store.identity_key("PURGE1", None), "entry", log_type="charger")
|
|
529
1149
|
url = reverse("admin:ocpp_charger_changelist")
|
|
530
|
-
self.client.post(
|
|
1150
|
+
self.client.post(
|
|
1151
|
+
url, {"action": "purge_data", "_selected_action": [charger.pk]}
|
|
1152
|
+
)
|
|
531
1153
|
self.assertFalse(Transaction.objects.filter(charger=charger).exists())
|
|
532
1154
|
self.assertFalse(MeterReading.objects.filter(charger=charger).exists())
|
|
533
|
-
self.assertNotIn("PURGE1", store.logs["charger"])
|
|
1155
|
+
self.assertNotIn(store.identity_key("PURGE1", None), store.logs["charger"])
|
|
534
1156
|
|
|
535
1157
|
def test_delete_requires_purge(self):
|
|
536
1158
|
charger = Charger.objects.create(charger_id="DEL1")
|
|
@@ -543,11 +1165,44 @@ class ChargerAdminTests(TestCase):
|
|
|
543
1165
|
self.client.post(delete_url, {"post": "yes"})
|
|
544
1166
|
self.assertTrue(Charger.objects.filter(pk=charger.pk).exists())
|
|
545
1167
|
url = reverse("admin:ocpp_charger_changelist")
|
|
546
|
-
self.client.post(
|
|
1168
|
+
self.client.post(
|
|
1169
|
+
url, {"action": "purge_data", "_selected_action": [charger.pk]}
|
|
1170
|
+
)
|
|
547
1171
|
self.client.post(delete_url, {"post": "yes"})
|
|
548
1172
|
self.assertFalse(Charger.objects.filter(pk=charger.pk).exists())
|
|
549
1173
|
|
|
550
1174
|
|
|
1175
|
+
class LocationAdminTests(TestCase):
|
|
1176
|
+
def setUp(self):
|
|
1177
|
+
self.client = Client()
|
|
1178
|
+
User = get_user_model()
|
|
1179
|
+
self.admin = User.objects.create_superuser(
|
|
1180
|
+
username="loc-admin", password="secret", email="loc@example.com"
|
|
1181
|
+
)
|
|
1182
|
+
self.client.force_login(self.admin)
|
|
1183
|
+
|
|
1184
|
+
def test_change_form_lists_related_chargers(self):
|
|
1185
|
+
location = Location.objects.create(name="LocAdmin")
|
|
1186
|
+
base = Charger.objects.create(charger_id="LOCBASE", location=location)
|
|
1187
|
+
connector = Charger.objects.create(
|
|
1188
|
+
charger_id="LOCALTWO",
|
|
1189
|
+
connector_id=1,
|
|
1190
|
+
location=location,
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
url = reverse("admin:ocpp_location_change", args=[location.pk])
|
|
1194
|
+
resp = self.client.get(url)
|
|
1195
|
+
self.assertEqual(resp.status_code, 200)
|
|
1196
|
+
|
|
1197
|
+
base_change_url = reverse("admin:ocpp_charger_change", args=[base.pk])
|
|
1198
|
+
connector_change_url = reverse("admin:ocpp_charger_change", args=[connector.pk])
|
|
1199
|
+
|
|
1200
|
+
self.assertContains(resp, base_change_url)
|
|
1201
|
+
self.assertContains(resp, connector_change_url)
|
|
1202
|
+
self.assertContains(resp, f"Charge Point: {base.charger_id}")
|
|
1203
|
+
self.assertContains(resp, f"Charge Point: {connector.charger_id} #1")
|
|
1204
|
+
|
|
1205
|
+
|
|
551
1206
|
class TransactionAdminTests(TestCase):
|
|
552
1207
|
def setUp(self):
|
|
553
1208
|
self.client = Client()
|
|
@@ -572,7 +1227,7 @@ class TransactionAdminTests(TestCase):
|
|
|
572
1227
|
self.assertContains(resp, str(reading.value))
|
|
573
1228
|
|
|
574
1229
|
|
|
575
|
-
class SimulatorAdminTests(
|
|
1230
|
+
class SimulatorAdminTests(TransactionTestCase):
|
|
576
1231
|
def setUp(self):
|
|
577
1232
|
self.client = Client()
|
|
578
1233
|
User = get_user_model()
|
|
@@ -580,17 +1235,48 @@ class SimulatorAdminTests(TestCase):
|
|
|
580
1235
|
username="admin2", password="secret", email="admin2@example.com"
|
|
581
1236
|
)
|
|
582
1237
|
self.client.force_login(self.admin)
|
|
1238
|
+
store.simulators.clear()
|
|
1239
|
+
store.logs["simulator"].clear()
|
|
1240
|
+
store.log_names["simulator"].clear()
|
|
583
1241
|
|
|
584
1242
|
def test_admin_lists_log_link(self):
|
|
585
1243
|
sim = Simulator.objects.create(name="SIM", cp_path="SIMX")
|
|
586
1244
|
url = reverse("admin:ocpp_simulator_changelist")
|
|
587
1245
|
resp = self.client.get(url)
|
|
588
|
-
log_url = reverse("
|
|
1246
|
+
log_url = reverse("admin:ocpp_simulator_log", args=[sim.pk])
|
|
589
1247
|
self.assertContains(resp, log_url)
|
|
590
1248
|
|
|
1249
|
+
def test_admin_log_view_displays_entries(self):
|
|
1250
|
+
sim = Simulator.objects.create(name="SIMLOG", cp_path="SIMLOG")
|
|
1251
|
+
store.add_log("SIMLOG", "entry", log_type="simulator")
|
|
1252
|
+
url = reverse("admin:ocpp_simulator_log", args=[sim.pk])
|
|
1253
|
+
resp = self.client.get(url)
|
|
1254
|
+
self.assertEqual(resp.status_code, 200)
|
|
1255
|
+
self.assertContains(resp, "entry")
|
|
1256
|
+
store.clear_log("SIMLOG", log_type="simulator")
|
|
1257
|
+
|
|
1258
|
+
@patch("ocpp.admin.ChargePointSimulator.start")
|
|
1259
|
+
def test_start_simulator_message_includes_log_link(self, mock_start):
|
|
1260
|
+
sim = Simulator.objects.create(name="SIMMSG", cp_path="SIMMSG")
|
|
1261
|
+
mock_start.return_value = (True, "Connection accepted", "/tmp/sim.log")
|
|
1262
|
+
url = reverse("admin:ocpp_simulator_changelist")
|
|
1263
|
+
resp = self.client.post(
|
|
1264
|
+
url,
|
|
1265
|
+
{"action": "start_simulator", "_selected_action": [sim.pk]},
|
|
1266
|
+
follow=True,
|
|
1267
|
+
)
|
|
1268
|
+
self.assertEqual(resp.status_code, 200)
|
|
1269
|
+
log_url = reverse("admin:ocpp_simulator_log", args=[sim.pk])
|
|
1270
|
+
self.assertContains(resp, "View Log")
|
|
1271
|
+
self.assertContains(resp, log_url)
|
|
1272
|
+
self.assertContains(resp, "/tmp/sim.log")
|
|
1273
|
+
mock_start.assert_called_once()
|
|
1274
|
+
store.simulators.clear()
|
|
1275
|
+
|
|
591
1276
|
def test_admin_shows_ws_url(self):
|
|
592
|
-
sim = Simulator.objects.create(
|
|
593
|
-
|
|
1277
|
+
sim = Simulator.objects.create(
|
|
1278
|
+
name="SIM2", cp_path="SIMY", host="h", ws_port=1111
|
|
1279
|
+
)
|
|
594
1280
|
url = reverse("admin:ocpp_simulator_changelist")
|
|
595
1281
|
resp = self.client.get(url)
|
|
596
1282
|
self.assertContains(resp, "ws://h:1111/SIMY/")
|
|
@@ -617,7 +1303,9 @@ class SimulatorAdminTests(TestCase):
|
|
|
617
1303
|
connected, _ = await communicator.connect()
|
|
618
1304
|
self.assertTrue(connected)
|
|
619
1305
|
|
|
620
|
-
exists = await database_sync_to_async(
|
|
1306
|
+
exists = await database_sync_to_async(
|
|
1307
|
+
Charger.objects.filter(charger_id="NEWCHG").exists
|
|
1308
|
+
)()
|
|
621
1309
|
self.assertTrue(exists)
|
|
622
1310
|
|
|
623
1311
|
charger = await database_sync_to_async(Charger.objects.get)(charger_id="NEWCHG")
|
|
@@ -636,21 +1324,27 @@ class SimulatorAdminTests(TestCase):
|
|
|
636
1324
|
self.assertEqual(charger.last_path, "/foo/NEST/")
|
|
637
1325
|
|
|
638
1326
|
async def test_rfid_required_rejects_invalid(self):
|
|
639
|
-
await database_sync_to_async(Charger.objects.create)(
|
|
1327
|
+
await database_sync_to_async(Charger.objects.create)(
|
|
1328
|
+
charger_id="RFID", require_rfid=True
|
|
1329
|
+
)
|
|
640
1330
|
communicator = WebsocketCommunicator(application, "/RFID/")
|
|
641
1331
|
connected, _ = await communicator.connect()
|
|
642
1332
|
self.assertTrue(connected)
|
|
643
1333
|
|
|
644
|
-
await communicator.send_json_to(
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1334
|
+
await communicator.send_json_to(
|
|
1335
|
+
[
|
|
1336
|
+
2,
|
|
1337
|
+
"1",
|
|
1338
|
+
"StartTransaction",
|
|
1339
|
+
{"meterStart": 0},
|
|
1340
|
+
]
|
|
1341
|
+
)
|
|
650
1342
|
response = await communicator.receive_json_from()
|
|
651
1343
|
self.assertEqual(response[2]["idTagInfo"]["status"], "Invalid")
|
|
652
1344
|
|
|
653
|
-
exists = await database_sync_to_async(
|
|
1345
|
+
exists = await database_sync_to_async(
|
|
1346
|
+
Transaction.objects.filter(charger__charger_id="RFID").exists
|
|
1347
|
+
)()
|
|
654
1348
|
self.assertFalse(exists)
|
|
655
1349
|
|
|
656
1350
|
await communicator.disconnect()
|
|
@@ -668,22 +1362,28 @@ class SimulatorAdminTests(TestCase):
|
|
|
668
1362
|
)
|
|
669
1363
|
tag = await database_sync_to_async(RFID.objects.create)(rfid="CARDX")
|
|
670
1364
|
await database_sync_to_async(acc.rfids.add)(tag)
|
|
671
|
-
await database_sync_to_async(Charger.objects.create)(
|
|
1365
|
+
await database_sync_to_async(Charger.objects.create)(
|
|
1366
|
+
charger_id="RFIDOK", require_rfid=True
|
|
1367
|
+
)
|
|
672
1368
|
communicator = WebsocketCommunicator(application, "/RFIDOK/")
|
|
673
1369
|
connected, _ = await communicator.connect()
|
|
674
1370
|
self.assertTrue(connected)
|
|
675
1371
|
|
|
676
|
-
await communicator.send_json_to(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
1372
|
+
await communicator.send_json_to(
|
|
1373
|
+
[
|
|
1374
|
+
2,
|
|
1375
|
+
"1",
|
|
1376
|
+
"StartTransaction",
|
|
1377
|
+
{"meterStart": 5, "idTag": "CARDX"},
|
|
1378
|
+
]
|
|
1379
|
+
)
|
|
682
1380
|
response = await communicator.receive_json_from()
|
|
683
1381
|
self.assertEqual(response[2]["idTagInfo"]["status"], "Accepted")
|
|
684
1382
|
tx_id = response[2]["transactionId"]
|
|
685
1383
|
|
|
686
|
-
tx = await database_sync_to_async(Transaction.objects.get)(
|
|
1384
|
+
tx = await database_sync_to_async(Transaction.objects.get)(
|
|
1385
|
+
pk=tx_id, charger__charger_id="RFIDOK"
|
|
1386
|
+
)
|
|
687
1387
|
self.assertEqual(tx.account_id, user.energy_account.id)
|
|
688
1388
|
|
|
689
1389
|
async def test_status_fields_updated(self):
|
|
@@ -709,7 +1409,10 @@ class SimulatorAdminTests(TestCase):
|
|
|
709
1409
|
await communicator.receive_json_from()
|
|
710
1410
|
|
|
711
1411
|
await database_sync_to_async(charger.refresh_from_db)()
|
|
712
|
-
self.assertEqual(
|
|
1412
|
+
self.assertEqual(
|
|
1413
|
+
charger.last_meter_values.get("meterValue")[0]["sampledValue"][0]["value"],
|
|
1414
|
+
"42",
|
|
1415
|
+
)
|
|
713
1416
|
|
|
714
1417
|
await communicator.disconnect()
|
|
715
1418
|
|
|
@@ -724,6 +1427,18 @@ class ChargerLocationTests(TestCase):
|
|
|
724
1427
|
self.assertAlmostEqual(float(charger.longitude), -20.654321)
|
|
725
1428
|
self.assertEqual(charger.name, "Loc1")
|
|
726
1429
|
|
|
1430
|
+
def test_location_created_when_missing(self):
|
|
1431
|
+
charger = Charger.objects.create(charger_id="AUTOLOC")
|
|
1432
|
+
self.assertIsNotNone(charger.location)
|
|
1433
|
+
self.assertEqual(charger.location.name, "AUTOLOC")
|
|
1434
|
+
|
|
1435
|
+
def test_location_reused_for_matching_serial(self):
|
|
1436
|
+
first = Charger.objects.create(charger_id="SHARE", connector_id=1)
|
|
1437
|
+
first.location.name = "Custom"
|
|
1438
|
+
first.location.save()
|
|
1439
|
+
second = Charger.objects.create(charger_id="SHARE", connector_id=2)
|
|
1440
|
+
self.assertEqual(second.location, first.location)
|
|
1441
|
+
|
|
727
1442
|
|
|
728
1443
|
class MeterReadingTests(TransactionTestCase):
|
|
729
1444
|
async def test_meter_values_saved_as_readings(self):
|
|
@@ -750,10 +1465,14 @@ class MeterReadingTests(TransactionTestCase):
|
|
|
750
1465
|
await communicator.send_json_to([2, "1", "MeterValues", payload])
|
|
751
1466
|
await communicator.receive_json_from()
|
|
752
1467
|
|
|
753
|
-
reading = await database_sync_to_async(MeterReading.objects.get)(
|
|
1468
|
+
reading = await database_sync_to_async(MeterReading.objects.get)(
|
|
1469
|
+
charger__charger_id="MR1"
|
|
1470
|
+
)
|
|
754
1471
|
self.assertEqual(reading.transaction_id, 100)
|
|
755
1472
|
self.assertEqual(str(reading.value), "2.749")
|
|
756
|
-
tx = await database_sync_to_async(Transaction.objects.get)(
|
|
1473
|
+
tx = await database_sync_to_async(Transaction.objects.get)(
|
|
1474
|
+
pk=100, charger__charger_id="MR1"
|
|
1475
|
+
)
|
|
757
1476
|
self.assertEqual(tx.meter_start, 2749)
|
|
758
1477
|
|
|
759
1478
|
await communicator.disconnect()
|
|
@@ -819,7 +1538,9 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
819
1538
|
)
|
|
820
1539
|
break
|
|
821
1540
|
|
|
822
|
-
server = await websockets.serve(
|
|
1541
|
+
server = await websockets.serve(
|
|
1542
|
+
handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"]
|
|
1543
|
+
)
|
|
823
1544
|
port = server.sockets[0].getsockname()[1]
|
|
824
1545
|
|
|
825
1546
|
try:
|
|
@@ -833,6 +1554,8 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
833
1554
|
kw_min=0.1,
|
|
834
1555
|
kw_max=0.2,
|
|
835
1556
|
pre_charge_delay=0.0,
|
|
1557
|
+
serial_number="SN123",
|
|
1558
|
+
connector_id=7,
|
|
836
1559
|
)
|
|
837
1560
|
sim = ChargePointSimulator(cfg)
|
|
838
1561
|
await sim._run_session()
|
|
@@ -843,8 +1566,11 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
843
1566
|
actions = [msg[2] for msg in received]
|
|
844
1567
|
self.assertIn("BootNotification", actions)
|
|
845
1568
|
self.assertIn("StartTransaction", actions)
|
|
1569
|
+
boot_msg = next(msg for msg in received if msg[2] == "BootNotification")
|
|
1570
|
+
self.assertEqual(boot_msg[3].get("serialNumber"), "SN123")
|
|
846
1571
|
start_msg = next(msg for msg in received if msg[2] == "StartTransaction")
|
|
847
1572
|
self.assertEqual(start_msg[3].get("vin"), "WP0ZZZ12345678901")
|
|
1573
|
+
self.assertEqual(start_msg[3].get("connectorId"), 7)
|
|
848
1574
|
|
|
849
1575
|
async def test_start_returns_status_and_log(self):
|
|
850
1576
|
async def handler(ws):
|
|
@@ -867,9 +1593,7 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
867
1593
|
)
|
|
868
1594
|
elif action == "Authorize":
|
|
869
1595
|
await ws.send(
|
|
870
|
-
json.dumps(
|
|
871
|
-
[3, data[1], {"idTagInfo": {"status": "Accepted"}}]
|
|
872
|
-
)
|
|
1596
|
+
json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}])
|
|
873
1597
|
)
|
|
874
1598
|
elif action == "StartTransaction":
|
|
875
1599
|
await ws.send(
|
|
@@ -886,15 +1610,15 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
886
1610
|
)
|
|
887
1611
|
elif action == "StopTransaction":
|
|
888
1612
|
await ws.send(
|
|
889
|
-
json.dumps(
|
|
890
|
-
[3, data[1], {"idTagInfo": {"status": "Accepted"}}]
|
|
891
|
-
)
|
|
1613
|
+
json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}])
|
|
892
1614
|
)
|
|
893
1615
|
break
|
|
894
1616
|
else:
|
|
895
1617
|
await ws.send(json.dumps([3, data[1], {}]))
|
|
896
1618
|
|
|
897
|
-
server = await websockets.serve(
|
|
1619
|
+
server = await websockets.serve(
|
|
1620
|
+
handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"]
|
|
1621
|
+
)
|
|
898
1622
|
port = server.sockets[0].getsockname()[1]
|
|
899
1623
|
|
|
900
1624
|
cfg = SimulatorConfig(
|
|
@@ -927,9 +1651,7 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
927
1651
|
data = json.loads(msg)
|
|
928
1652
|
action = data[2]
|
|
929
1653
|
if action == "BootNotification":
|
|
930
|
-
await ws.send(
|
|
931
|
-
json.dumps([3, data[1], {"status": "Accepted"}])
|
|
932
|
-
)
|
|
1654
|
+
await ws.send(json.dumps([3, data[1], {"status": "Accepted"}]))
|
|
933
1655
|
elif action == "Authorize":
|
|
934
1656
|
await ws.send(
|
|
935
1657
|
json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}])
|
|
@@ -937,7 +1659,9 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
937
1659
|
await ws.close()
|
|
938
1660
|
break
|
|
939
1661
|
|
|
940
|
-
server = await websockets.serve(
|
|
1662
|
+
server = await websockets.serve(
|
|
1663
|
+
handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"]
|
|
1664
|
+
)
|
|
941
1665
|
port = server.sockets[0].getsockname()[1]
|
|
942
1666
|
|
|
943
1667
|
cfg = SimulatorConfig(
|
|
@@ -973,7 +1697,12 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
973
1697
|
action = data[2]
|
|
974
1698
|
if action == "BootNotification":
|
|
975
1699
|
await ws.send(json.dumps([3, data[1], {"status": "Accepted"}]))
|
|
976
|
-
elif action in {
|
|
1700
|
+
elif action in {
|
|
1701
|
+
"Authorize",
|
|
1702
|
+
"StatusNotification",
|
|
1703
|
+
"Heartbeat",
|
|
1704
|
+
"MeterValues",
|
|
1705
|
+
}:
|
|
977
1706
|
await ws.send(json.dumps([3, data[1], {}]))
|
|
978
1707
|
elif action == "StartTransaction":
|
|
979
1708
|
await ws.send(
|
|
@@ -981,15 +1710,22 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
981
1710
|
[
|
|
982
1711
|
3,
|
|
983
1712
|
data[1],
|
|
984
|
-
{
|
|
1713
|
+
{
|
|
1714
|
+
"transactionId": 1,
|
|
1715
|
+
"idTagInfo": {"status": "Accepted"},
|
|
1716
|
+
},
|
|
985
1717
|
]
|
|
986
1718
|
)
|
|
987
1719
|
)
|
|
988
1720
|
elif action == "StopTransaction":
|
|
989
|
-
await ws.send(
|
|
1721
|
+
await ws.send(
|
|
1722
|
+
json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}])
|
|
1723
|
+
)
|
|
990
1724
|
break
|
|
991
1725
|
|
|
992
|
-
server = await websockets.serve(
|
|
1726
|
+
server = await websockets.serve(
|
|
1727
|
+
handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"]
|
|
1728
|
+
)
|
|
993
1729
|
port = server.sockets[0].getsockname()[1]
|
|
994
1730
|
|
|
995
1731
|
try:
|
|
@@ -1020,13 +1756,16 @@ class ChargePointSimulatorTests(TransactionTestCase):
|
|
|
1020
1756
|
async for _ in ws:
|
|
1021
1757
|
pass
|
|
1022
1758
|
|
|
1023
|
-
server = await websockets.serve(
|
|
1759
|
+
server = await websockets.serve(
|
|
1760
|
+
handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"]
|
|
1761
|
+
)
|
|
1024
1762
|
port = server.sockets[0].getsockname()[1]
|
|
1025
1763
|
|
|
1026
1764
|
cfg = SimulatorConfig(host="127.0.0.1", ws_port=port, cp_path="SIMTO/")
|
|
1027
1765
|
sim = ChargePointSimulator(cfg)
|
|
1028
1766
|
store.simulators[99] = sim
|
|
1029
1767
|
try:
|
|
1768
|
+
|
|
1030
1769
|
async def fake_wait_for(coro, timeout):
|
|
1031
1770
|
coro.close()
|
|
1032
1771
|
raise asyncio.TimeoutError
|
|
@@ -1066,7 +1805,9 @@ class PurgeMeterReadingsTaskTests(TestCase):
|
|
|
1066
1805
|
|
|
1067
1806
|
self.assertEqual(MeterReading.objects.count(), 1)
|
|
1068
1807
|
self.assertTrue(
|
|
1069
|
-
MeterReading.objects.filter(
|
|
1808
|
+
MeterReading.objects.filter(
|
|
1809
|
+
timestamp__gte=recent - timedelta(minutes=1)
|
|
1810
|
+
).exists()
|
|
1070
1811
|
)
|
|
1071
1812
|
self.assertTrue(Transaction.objects.filter(pk=tx.pk).exists())
|
|
1072
1813
|
|
|
@@ -1090,19 +1831,21 @@ class PurgeMeterReadingsTaskTests(TestCase):
|
|
|
1090
1831
|
class TransactionKwTests(TestCase):
|
|
1091
1832
|
def test_kw_sums_meter_readings(self):
|
|
1092
1833
|
charger = Charger.objects.create(charger_id="SUM1")
|
|
1093
|
-
tx = Transaction.objects.create(
|
|
1834
|
+
tx = Transaction.objects.create(
|
|
1835
|
+
charger=charger, start_time=timezone.now(), meter_start=0
|
|
1836
|
+
)
|
|
1094
1837
|
MeterReading.objects.create(
|
|
1095
1838
|
charger=charger,
|
|
1096
1839
|
transaction=tx,
|
|
1097
1840
|
timestamp=timezone.now(),
|
|
1098
|
-
value=Decimal("
|
|
1099
|
-
unit="
|
|
1841
|
+
value=Decimal("1000"),
|
|
1842
|
+
unit="W",
|
|
1100
1843
|
)
|
|
1101
1844
|
MeterReading.objects.create(
|
|
1102
1845
|
charger=charger,
|
|
1103
1846
|
transaction=tx,
|
|
1104
1847
|
timestamp=timezone.now(),
|
|
1105
|
-
value=Decimal("
|
|
1848
|
+
value=Decimal("1500"),
|
|
1106
1849
|
unit="W",
|
|
1107
1850
|
)
|
|
1108
1851
|
self.assertAlmostEqual(tx.kw, 1.5)
|
|
@@ -1113,15 +1856,95 @@ class TransactionKwTests(TestCase):
|
|
|
1113
1856
|
self.assertEqual(tx.kw, 0.0)
|
|
1114
1857
|
|
|
1115
1858
|
|
|
1859
|
+
class DispatchActionViewTests(TestCase):
|
|
1860
|
+
def setUp(self):
|
|
1861
|
+
self.client = Client()
|
|
1862
|
+
User = get_user_model()
|
|
1863
|
+
self.user = User.objects.create_user(username="dispatch", password="pw")
|
|
1864
|
+
self.client.force_login(self.user)
|
|
1865
|
+
try:
|
|
1866
|
+
self.previous_loop = asyncio.get_event_loop()
|
|
1867
|
+
except RuntimeError:
|
|
1868
|
+
self.previous_loop = None
|
|
1869
|
+
self.loop = asyncio.new_event_loop()
|
|
1870
|
+
asyncio.set_event_loop(self.loop)
|
|
1871
|
+
self.addCleanup(self._close_loop)
|
|
1872
|
+
self.charger = Charger.objects.create(
|
|
1873
|
+
charger_id="DISPATCH", connector_id=1
|
|
1874
|
+
)
|
|
1875
|
+
self.ws = DummyWebSocket()
|
|
1876
|
+
store.set_connection(
|
|
1877
|
+
self.charger.charger_id, self.charger.connector_id, self.ws
|
|
1878
|
+
)
|
|
1879
|
+
self.addCleanup(
|
|
1880
|
+
store.pop_connection,
|
|
1881
|
+
self.charger.charger_id,
|
|
1882
|
+
self.charger.connector_id,
|
|
1883
|
+
)
|
|
1884
|
+
self.log_key = store.identity_key(
|
|
1885
|
+
self.charger.charger_id, self.charger.connector_id
|
|
1886
|
+
)
|
|
1887
|
+
store.clear_log(self.log_key, log_type="charger")
|
|
1888
|
+
self.addCleanup(store.clear_log, self.log_key, "charger")
|
|
1889
|
+
self.url = reverse(
|
|
1890
|
+
"charger-action-connector",
|
|
1891
|
+
args=[self.charger.charger_id, self.charger.connector_slug],
|
|
1892
|
+
)
|
|
1893
|
+
|
|
1894
|
+
def _close_loop(self):
|
|
1895
|
+
try:
|
|
1896
|
+
if not self.loop.is_closed():
|
|
1897
|
+
self.loop.run_until_complete(asyncio.sleep(0))
|
|
1898
|
+
except RuntimeError:
|
|
1899
|
+
pass
|
|
1900
|
+
finally:
|
|
1901
|
+
if not self.loop.is_closed():
|
|
1902
|
+
self.loop.close()
|
|
1903
|
+
asyncio.set_event_loop(self.previous_loop)
|
|
1904
|
+
|
|
1905
|
+
def test_remote_start_requires_id_tag(self):
|
|
1906
|
+
response = self.client.post(
|
|
1907
|
+
self.url,
|
|
1908
|
+
data=json.dumps({"action": "remote_start"}),
|
|
1909
|
+
content_type="application/json",
|
|
1910
|
+
)
|
|
1911
|
+
self.assertEqual(response.status_code, 400)
|
|
1912
|
+
self.assertEqual(response.json().get("detail"), "idTag required")
|
|
1913
|
+
self.loop.run_until_complete(asyncio.sleep(0))
|
|
1914
|
+
self.assertEqual(self.ws.sent, [])
|
|
1915
|
+
|
|
1916
|
+
def test_remote_start_dispatches_frame(self):
|
|
1917
|
+
response = self.client.post(
|
|
1918
|
+
self.url,
|
|
1919
|
+
data=json.dumps({"action": "remote_start", "idTag": "RF1234"}),
|
|
1920
|
+
content_type="application/json",
|
|
1921
|
+
)
|
|
1922
|
+
self.assertEqual(response.status_code, 200)
|
|
1923
|
+
self.loop.run_until_complete(asyncio.sleep(0))
|
|
1924
|
+
self.assertEqual(len(self.ws.sent), 1)
|
|
1925
|
+
frame = json.loads(self.ws.sent[0])
|
|
1926
|
+
self.assertEqual(frame[0], 2)
|
|
1927
|
+
self.assertEqual(frame[2], "RemoteStartTransaction")
|
|
1928
|
+
self.assertEqual(frame[3]["idTag"], "RF1234")
|
|
1929
|
+
self.assertEqual(frame[3]["connectorId"], 1)
|
|
1930
|
+
log_entries = store.logs["charger"].get(self.log_key, [])
|
|
1931
|
+
self.assertTrue(
|
|
1932
|
+
any("RemoteStartTransaction" in entry for entry in log_entries)
|
|
1933
|
+
)
|
|
1934
|
+
|
|
1935
|
+
|
|
1116
1936
|
class ChargerStatusViewTests(TestCase):
|
|
1117
1937
|
def setUp(self):
|
|
1118
1938
|
self.client = Client()
|
|
1119
1939
|
User = get_user_model()
|
|
1120
1940
|
self.user = User.objects.create_user(username="status", password="pwd")
|
|
1121
1941
|
self.client.force_login(self.user)
|
|
1942
|
+
|
|
1122
1943
|
def test_chart_data_populated_from_existing_readings(self):
|
|
1123
|
-
charger = Charger.objects.create(charger_id="VIEW1")
|
|
1124
|
-
tx = Transaction.objects.create(
|
|
1944
|
+
charger = Charger.objects.create(charger_id="VIEW1", connector_id=1)
|
|
1945
|
+
tx = Transaction.objects.create(
|
|
1946
|
+
charger=charger, start_time=timezone.now(), meter_start=0
|
|
1947
|
+
)
|
|
1125
1948
|
t0 = timezone.now()
|
|
1126
1949
|
MeterReading.objects.create(
|
|
1127
1950
|
charger=charger,
|
|
@@ -1134,17 +1957,109 @@ class ChargerStatusViewTests(TestCase):
|
|
|
1134
1957
|
charger=charger,
|
|
1135
1958
|
transaction=tx,
|
|
1136
1959
|
timestamp=t0 + timedelta(seconds=10),
|
|
1137
|
-
value=Decimal("
|
|
1960
|
+
value=Decimal("1500"),
|
|
1138
1961
|
unit="W",
|
|
1139
1962
|
)
|
|
1140
|
-
store.
|
|
1141
|
-
|
|
1963
|
+
key = store.identity_key(charger.charger_id, charger.connector_id)
|
|
1964
|
+
store.transactions[key] = tx
|
|
1965
|
+
resp = self.client.get(
|
|
1966
|
+
reverse(
|
|
1967
|
+
"charger-status-connector",
|
|
1968
|
+
args=[charger.charger_id, charger.connector_slug],
|
|
1969
|
+
)
|
|
1970
|
+
)
|
|
1142
1971
|
self.assertEqual(resp.status_code, 200)
|
|
1143
|
-
chart =
|
|
1972
|
+
chart = resp.context["chart_data"]
|
|
1144
1973
|
self.assertEqual(len(chart["labels"]), 2)
|
|
1145
|
-
self.
|
|
1146
|
-
|
|
1147
|
-
|
|
1974
|
+
self.assertEqual(len(chart["datasets"]), 1)
|
|
1975
|
+
values = chart["datasets"][0]["values"]
|
|
1976
|
+
self.assertEqual(chart["datasets"][0]["connector_id"], 1)
|
|
1977
|
+
self.assertAlmostEqual(values[0], 1.0)
|
|
1978
|
+
self.assertAlmostEqual(values[1], 1.5)
|
|
1979
|
+
store.transactions.pop(key, None)
|
|
1980
|
+
|
|
1981
|
+
def test_chart_data_uses_meter_start_for_register_values(self):
|
|
1982
|
+
charger = Charger.objects.create(charger_id="VIEWREG", connector_id=1)
|
|
1983
|
+
tx = Transaction.objects.create(
|
|
1984
|
+
charger=charger, start_time=timezone.now(), meter_start=746060
|
|
1985
|
+
)
|
|
1986
|
+
t0 = timezone.now()
|
|
1987
|
+
MeterReading.objects.create(
|
|
1988
|
+
charger=charger,
|
|
1989
|
+
transaction=tx,
|
|
1990
|
+
timestamp=t0,
|
|
1991
|
+
measurand="Energy.Active.Import.Register",
|
|
1992
|
+
value=Decimal("746.060"),
|
|
1993
|
+
unit="kWh",
|
|
1994
|
+
)
|
|
1995
|
+
MeterReading.objects.create(
|
|
1996
|
+
charger=charger,
|
|
1997
|
+
transaction=tx,
|
|
1998
|
+
timestamp=t0 + timedelta(seconds=10),
|
|
1999
|
+
measurand="Energy.Active.Import.Register",
|
|
2000
|
+
value=Decimal("746.080"),
|
|
2001
|
+
unit="kWh",
|
|
2002
|
+
)
|
|
2003
|
+
key = store.identity_key(charger.charger_id, charger.connector_id)
|
|
2004
|
+
store.transactions[key] = tx
|
|
2005
|
+
resp = self.client.get(
|
|
2006
|
+
reverse(
|
|
2007
|
+
"charger-status-connector",
|
|
2008
|
+
args=[charger.charger_id, charger.connector_slug],
|
|
2009
|
+
)
|
|
2010
|
+
)
|
|
2011
|
+
chart = resp.context["chart_data"]
|
|
2012
|
+
self.assertEqual(len(chart["labels"]), 2)
|
|
2013
|
+
self.assertEqual(len(chart["datasets"]), 1)
|
|
2014
|
+
values = chart["datasets"][0]["values"]
|
|
2015
|
+
self.assertEqual(chart["datasets"][0]["connector_id"], 1)
|
|
2016
|
+
self.assertAlmostEqual(values[0], 0.0)
|
|
2017
|
+
self.assertAlmostEqual(values[1], 0.02)
|
|
2018
|
+
self.assertAlmostEqual(resp.context["tx"].kw, 0.02)
|
|
2019
|
+
store.transactions.pop(key, None)
|
|
2020
|
+
|
|
2021
|
+
def test_diagnostics_status_displayed(self):
|
|
2022
|
+
reported_at = timezone.now().replace(microsecond=0)
|
|
2023
|
+
charger = Charger.objects.create(
|
|
2024
|
+
charger_id="DIAGPAGE",
|
|
2025
|
+
diagnostics_status="Uploaded",
|
|
2026
|
+
diagnostics_location="https://example.com/report.tar",
|
|
2027
|
+
diagnostics_timestamp=reported_at,
|
|
2028
|
+
)
|
|
2029
|
+
|
|
2030
|
+
resp = self.client.get(reverse("charger-status", args=[charger.charger_id]))
|
|
2031
|
+
self.assertEqual(resp.status_code, 200)
|
|
2032
|
+
self.assertContains(resp, "Diagnostics")
|
|
2033
|
+
self.assertContains(resp, "id=\"diagnostics-status\"")
|
|
2034
|
+
self.assertContains(resp, "Uploaded")
|
|
2035
|
+
self.assertContains(resp, "id=\"diagnostics-timestamp\"")
|
|
2036
|
+
self.assertContains(resp, "id=\"diagnostics-location\"")
|
|
2037
|
+
self.assertContains(resp, "https://example.com/report.tar")
|
|
2038
|
+
|
|
2039
|
+
def test_connector_status_prefers_connector_diagnostics(self):
|
|
2040
|
+
aggregate = Charger.objects.create(
|
|
2041
|
+
charger_id="DIAGCONN",
|
|
2042
|
+
diagnostics_status="Uploaded",
|
|
2043
|
+
)
|
|
2044
|
+
connector = Charger.objects.create(
|
|
2045
|
+
charger_id="DIAGCONN",
|
|
2046
|
+
connector_id=1,
|
|
2047
|
+
diagnostics_status="Uploading",
|
|
2048
|
+
)
|
|
2049
|
+
|
|
2050
|
+
aggregate_resp = self.client.get(
|
|
2051
|
+
reverse("charger-status", args=[aggregate.charger_id])
|
|
2052
|
+
)
|
|
2053
|
+
self.assertContains(aggregate_resp, "Uploaded")
|
|
2054
|
+
self.assertNotContains(aggregate_resp, "Uploading")
|
|
2055
|
+
|
|
2056
|
+
connector_resp = self.client.get(
|
|
2057
|
+
reverse(
|
|
2058
|
+
"charger-status-connector",
|
|
2059
|
+
args=[connector.charger_id, connector.connector_slug],
|
|
2060
|
+
)
|
|
2061
|
+
)
|
|
2062
|
+
self.assertContains(connector_resp, "Uploading")
|
|
1148
2063
|
|
|
1149
2064
|
def test_sessions_are_linked(self):
|
|
1150
2065
|
charger = Charger.objects.create(charger_id="LINK1")
|
|
@@ -1157,9 +2072,27 @@ class ChargerStatusViewTests(TestCase):
|
|
|
1157
2072
|
resp = self.client.get(reverse("charger-status", args=[charger.charger_id]))
|
|
1158
2073
|
self.assertContains(resp, reverse("charger-page", args=[charger.charger_id]))
|
|
1159
2074
|
|
|
2075
|
+
def test_configuration_link_hidden_for_non_staff(self):
|
|
2076
|
+
charger = Charger.objects.create(charger_id="CFG-HIDE")
|
|
2077
|
+
response = self.client.get(reverse("charger-status", args=[charger.charger_id]))
|
|
2078
|
+
admin_url = reverse("admin:ocpp_charger_change", args=[charger.pk])
|
|
2079
|
+
self.assertNotContains(response, admin_url)
|
|
2080
|
+
self.assertNotContains(response, _("Configuration"))
|
|
2081
|
+
|
|
2082
|
+
def test_configuration_link_visible_for_staff(self):
|
|
2083
|
+
charger = Charger.objects.create(charger_id="CFG-SHOW")
|
|
2084
|
+
self.user.is_staff = True
|
|
2085
|
+
self.user.save(update_fields=["is_staff"])
|
|
2086
|
+
response = self.client.get(reverse("charger-status", args=[charger.charger_id]))
|
|
2087
|
+
admin_url = reverse("admin:ocpp_charger_change", args=[charger.pk])
|
|
2088
|
+
self.assertContains(response, admin_url)
|
|
2089
|
+
self.assertContains(response, _("Configuration"))
|
|
2090
|
+
|
|
1160
2091
|
def test_past_session_chart(self):
|
|
1161
|
-
charger = Charger.objects.create(charger_id="PAST1")
|
|
1162
|
-
tx = Transaction.objects.create(
|
|
2092
|
+
charger = Charger.objects.create(charger_id="PAST1", connector_id=1)
|
|
2093
|
+
tx = Transaction.objects.create(
|
|
2094
|
+
charger=charger, start_time=timezone.now(), meter_start=0
|
|
2095
|
+
)
|
|
1163
2096
|
t0 = timezone.now()
|
|
1164
2097
|
MeterReading.objects.create(
|
|
1165
2098
|
charger=charger,
|
|
@@ -1172,17 +2105,147 @@ class ChargerStatusViewTests(TestCase):
|
|
|
1172
2105
|
charger=charger,
|
|
1173
2106
|
transaction=tx,
|
|
1174
2107
|
timestamp=t0 + timedelta(seconds=10),
|
|
1175
|
-
value=Decimal("
|
|
2108
|
+
value=Decimal("1500"),
|
|
1176
2109
|
unit="W",
|
|
1177
2110
|
)
|
|
1178
2111
|
resp = self.client.get(
|
|
1179
|
-
reverse(
|
|
2112
|
+
reverse(
|
|
2113
|
+
"charger-status-connector",
|
|
2114
|
+
args=[charger.charger_id, charger.connector_slug],
|
|
2115
|
+
)
|
|
2116
|
+
+ f"?session={tx.id}"
|
|
1180
2117
|
)
|
|
1181
2118
|
self.assertContains(resp, "Back to live")
|
|
1182
|
-
chart =
|
|
2119
|
+
chart = resp.context["chart_data"]
|
|
1183
2120
|
self.assertEqual(len(chart["labels"]), 2)
|
|
2121
|
+
self.assertEqual(len(chart["datasets"]), 1)
|
|
2122
|
+
self.assertEqual(chart["datasets"][0]["connector_id"], 1)
|
|
1184
2123
|
self.assertTrue(resp.context["past_session"])
|
|
1185
2124
|
|
|
2125
|
+
def test_aggregate_chart_includes_multiple_connectors(self):
|
|
2126
|
+
aggregate = Charger.objects.create(charger_id="VIEWAGG")
|
|
2127
|
+
connector_one = Charger.objects.create(charger_id="VIEWAGG", connector_id=1)
|
|
2128
|
+
connector_two = Charger.objects.create(charger_id="VIEWAGG", connector_id=2)
|
|
2129
|
+
base_time = timezone.now()
|
|
2130
|
+
tx_one = Transaction.objects.create(
|
|
2131
|
+
charger=connector_one, start_time=base_time, meter_start=0
|
|
2132
|
+
)
|
|
2133
|
+
tx_two = Transaction.objects.create(
|
|
2134
|
+
charger=connector_two, start_time=base_time, meter_start=0
|
|
2135
|
+
)
|
|
2136
|
+
MeterReading.objects.create(
|
|
2137
|
+
charger=connector_one,
|
|
2138
|
+
transaction=tx_one,
|
|
2139
|
+
timestamp=base_time,
|
|
2140
|
+
value=Decimal("1000"),
|
|
2141
|
+
unit="W",
|
|
2142
|
+
)
|
|
2143
|
+
MeterReading.objects.create(
|
|
2144
|
+
charger=connector_one,
|
|
2145
|
+
transaction=tx_one,
|
|
2146
|
+
timestamp=base_time + timedelta(seconds=15),
|
|
2147
|
+
value=Decimal("1500"),
|
|
2148
|
+
unit="W",
|
|
2149
|
+
)
|
|
2150
|
+
MeterReading.objects.create(
|
|
2151
|
+
charger=connector_two,
|
|
2152
|
+
transaction=tx_two,
|
|
2153
|
+
timestamp=base_time + timedelta(seconds=5),
|
|
2154
|
+
value=Decimal("2000"),
|
|
2155
|
+
unit="W",
|
|
2156
|
+
)
|
|
2157
|
+
MeterReading.objects.create(
|
|
2158
|
+
charger=connector_two,
|
|
2159
|
+
transaction=tx_two,
|
|
2160
|
+
timestamp=base_time + timedelta(seconds=20),
|
|
2161
|
+
value=Decimal("2600"),
|
|
2162
|
+
unit="W",
|
|
2163
|
+
)
|
|
2164
|
+
key_one = store.identity_key(
|
|
2165
|
+
connector_one.charger_id, connector_one.connector_id
|
|
2166
|
+
)
|
|
2167
|
+
key_two = store.identity_key(
|
|
2168
|
+
connector_two.charger_id, connector_two.connector_id
|
|
2169
|
+
)
|
|
2170
|
+
store.transactions[key_one] = tx_one
|
|
2171
|
+
store.transactions[key_two] = tx_two
|
|
2172
|
+
try:
|
|
2173
|
+
resp = self.client.get(
|
|
2174
|
+
reverse("charger-status", args=[aggregate.charger_id])
|
|
2175
|
+
)
|
|
2176
|
+
chart = resp.context["chart_data"]
|
|
2177
|
+
self.assertTrue(resp.context["show_chart"])
|
|
2178
|
+
self.assertEqual(len(chart["datasets"]), 2)
|
|
2179
|
+
data_map = {
|
|
2180
|
+
dataset["label"]: dataset["values"] for dataset in chart["datasets"]
|
|
2181
|
+
}
|
|
2182
|
+
connector_id_map = {
|
|
2183
|
+
dataset["label"]: dataset.get("connector_id")
|
|
2184
|
+
for dataset in chart["datasets"]
|
|
2185
|
+
}
|
|
2186
|
+
label_one = str(connector_one.connector_label)
|
|
2187
|
+
label_two = str(connector_two.connector_label)
|
|
2188
|
+
self.assertEqual(set(data_map), {label_one, label_two})
|
|
2189
|
+
self.assertEqual(len(data_map[label_one]), len(chart["labels"]))
|
|
2190
|
+
self.assertEqual(len(data_map[label_two]), len(chart["labels"]))
|
|
2191
|
+
self.assertTrue(any(value is not None for value in data_map[label_one]))
|
|
2192
|
+
self.assertTrue(any(value is not None for value in data_map[label_two]))
|
|
2193
|
+
self.assertEqual(connector_id_map[label_one], connector_one.connector_id)
|
|
2194
|
+
self.assertEqual(connector_id_map[label_two], connector_two.connector_id)
|
|
2195
|
+
finally:
|
|
2196
|
+
store.transactions.pop(key_one, None)
|
|
2197
|
+
store.transactions.pop(key_two, None)
|
|
2198
|
+
|
|
2199
|
+
|
|
2200
|
+
class ChargerApiDiagnosticsTests(TestCase):
|
|
2201
|
+
def setUp(self):
|
|
2202
|
+
self.client = Client()
|
|
2203
|
+
User = get_user_model()
|
|
2204
|
+
self.user = User.objects.create_user(username="diagapi", password="pwd")
|
|
2205
|
+
self.client.force_login(self.user)
|
|
2206
|
+
|
|
2207
|
+
def test_detail_includes_diagnostics_fields(self):
|
|
2208
|
+
reported_at = timezone.now().replace(microsecond=0)
|
|
2209
|
+
charger = Charger.objects.create(
|
|
2210
|
+
charger_id="APIDIAG",
|
|
2211
|
+
diagnostics_status="Uploaded",
|
|
2212
|
+
diagnostics_timestamp=reported_at,
|
|
2213
|
+
diagnostics_location="https://example.com/diag.tar",
|
|
2214
|
+
)
|
|
2215
|
+
|
|
2216
|
+
resp = self.client.get(reverse("charger-detail", args=[charger.charger_id]))
|
|
2217
|
+
self.assertEqual(resp.status_code, 200)
|
|
2218
|
+
payload = resp.json()
|
|
2219
|
+
self.assertEqual(payload["diagnosticsStatus"], "Uploaded")
|
|
2220
|
+
self.assertEqual(
|
|
2221
|
+
payload["diagnosticsTimestamp"], reported_at.isoformat()
|
|
2222
|
+
)
|
|
2223
|
+
self.assertEqual(
|
|
2224
|
+
payload["diagnosticsLocation"], "https://example.com/diag.tar"
|
|
2225
|
+
)
|
|
2226
|
+
|
|
2227
|
+
def test_list_includes_diagnostics_fields(self):
|
|
2228
|
+
reported_at = timezone.now().replace(microsecond=0)
|
|
2229
|
+
Charger.objects.create(
|
|
2230
|
+
charger_id="APILIST",
|
|
2231
|
+
diagnostics_status="Idle",
|
|
2232
|
+
diagnostics_timestamp=reported_at,
|
|
2233
|
+
diagnostics_location="s3://bucket/diag.zip",
|
|
2234
|
+
)
|
|
2235
|
+
|
|
2236
|
+
resp = self.client.get(reverse("charger-list"))
|
|
2237
|
+
self.assertEqual(resp.status_code, 200)
|
|
2238
|
+
payload = resp.json()
|
|
2239
|
+
self.assertIn("chargers", payload)
|
|
2240
|
+
target = next(
|
|
2241
|
+
item
|
|
2242
|
+
for item in payload["chargers"]
|
|
2243
|
+
if item["charger_id"] == "APILIST" and item["connector_id"] is None
|
|
2244
|
+
)
|
|
2245
|
+
self.assertEqual(target["diagnosticsStatus"], "Idle")
|
|
2246
|
+
self.assertEqual(target["diagnosticsLocation"], "s3://bucket/diag.zip")
|
|
2247
|
+
self.assertEqual(target["diagnosticsTimestamp"], reported_at.isoformat())
|
|
2248
|
+
|
|
1186
2249
|
|
|
1187
2250
|
class ChargerSessionPaginationTests(TestCase):
|
|
1188
2251
|
def setUp(self):
|
|
@@ -1199,7 +2262,9 @@ class ChargerSessionPaginationTests(TestCase):
|
|
|
1199
2262
|
)
|
|
1200
2263
|
|
|
1201
2264
|
def test_only_ten_transactions_shown(self):
|
|
1202
|
-
resp = self.client.get(
|
|
2265
|
+
resp = self.client.get(
|
|
2266
|
+
reverse("charger-status", args=[self.charger.charger_id])
|
|
2267
|
+
)
|
|
1203
2268
|
self.assertEqual(resp.status_code, 200)
|
|
1204
2269
|
self.assertEqual(len(resp.context["transactions"]), 10)
|
|
1205
2270
|
self.assertTrue(resp.context["page_obj"].has_next())
|
|
@@ -1214,20 +2279,18 @@ class ChargerSessionPaginationTests(TestCase):
|
|
|
1214
2279
|
self.assertEqual(len(resp.context["transactions"]), 15)
|
|
1215
2280
|
|
|
1216
2281
|
|
|
1217
|
-
class
|
|
2282
|
+
class LiveUpdateViewTests(TestCase):
|
|
1218
2283
|
def setUp(self):
|
|
1219
2284
|
User = get_user_model()
|
|
1220
|
-
self.user = User.objects.create_user(
|
|
1221
|
-
username="eff", password="secret", email="eff@example.com"
|
|
1222
|
-
)
|
|
2285
|
+
self.user = User.objects.create_user(username="lu", password="pw")
|
|
1223
2286
|
self.client.force_login(self.user)
|
|
1224
2287
|
|
|
1225
|
-
def
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
self.assertContains(resp, "
|
|
2288
|
+
def test_dashboard_includes_interval(self):
|
|
2289
|
+
resp = self.client.get(reverse("ocpp-dashboard"))
|
|
2290
|
+
self.assertEqual(resp.context["request"].live_update_interval, 5)
|
|
2291
|
+
self.assertContains(resp, "setInterval(() => location.reload()")
|
|
1229
2292
|
|
|
1230
|
-
def
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
self.assertContains(resp, "
|
|
2293
|
+
def test_cp_simulator_includes_interval(self):
|
|
2294
|
+
resp = self.client.get(reverse("cp-simulator"))
|
|
2295
|
+
self.assertEqual(resp.context["request"].live_update_interval, 5)
|
|
2296
|
+
self.assertContains(resp, "setInterval(() => location.reload()")
|