arthexis 0.1.18__py3-none-any.whl → 0.1.20__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.
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/METADATA +39 -12
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/RECORD +44 -44
- config/settings.py +1 -5
- core/admin.py +142 -1
- core/backends.py +8 -2
- core/environment.py +221 -4
- core/models.py +124 -25
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/sigil_builder.py +2 -2
- core/system.py +125 -0
- core/tasks.py +24 -23
- core/tests.py +1 -0
- core/views.py +105 -40
- nodes/admin.py +134 -3
- nodes/models.py +310 -69
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +100 -2
- nodes/tests.py +573 -48
- nodes/urls.py +4 -1
- nodes/views.py +498 -106
- ocpp/admin.py +124 -5
- ocpp/consumers.py +106 -9
- ocpp/models.py +90 -1
- ocpp/store.py +6 -4
- ocpp/tasks.py +4 -0
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +3 -1
- ocpp/tests.py +114 -10
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +166 -40
- pages/admin.py +63 -10
- pages/context_processors.py +26 -9
- pages/defaults.py +1 -1
- pages/middleware.py +3 -0
- pages/models.py +35 -0
- pages/module_defaults.py +5 -5
- pages/tests.py +280 -65
- pages/urls.py +3 -1
- pages/views.py +176 -29
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/WHEEL +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/top_level.txt +0 -0
ocpp/tests.py
CHANGED
|
@@ -61,13 +61,14 @@ from config.asgi import application
|
|
|
61
61
|
from .models import (
|
|
62
62
|
Transaction,
|
|
63
63
|
Charger,
|
|
64
|
+
ChargerConfiguration,
|
|
64
65
|
Simulator,
|
|
65
66
|
MeterReading,
|
|
66
67
|
Location,
|
|
67
68
|
DataTransferMessage,
|
|
68
69
|
)
|
|
69
70
|
from .consumers import CSMSConsumer
|
|
70
|
-
from .views import dispatch_action
|
|
71
|
+
from .views import dispatch_action, _transaction_rfid_details
|
|
71
72
|
from .status_display import STATUS_BADGE_MAP
|
|
72
73
|
from core.models import EnergyAccount, EnergyCredit, Reference, RFID, SecurityGroup
|
|
73
74
|
from . import store
|
|
@@ -714,6 +715,13 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
714
715
|
connected, _ = await communicator.connect()
|
|
715
716
|
self.assertTrue(connected)
|
|
716
717
|
|
|
718
|
+
await database_sync_to_async(Charger.objects.get_or_create)(
|
|
719
|
+
charger_id="CFGRES", connector_id=1
|
|
720
|
+
)
|
|
721
|
+
await database_sync_to_async(Charger.objects.get_or_create)(
|
|
722
|
+
charger_id="CFGRES", connector_id=2
|
|
723
|
+
)
|
|
724
|
+
|
|
717
725
|
message_id = "cfg-result"
|
|
718
726
|
payload = {
|
|
719
727
|
"configurationKey": [
|
|
@@ -744,6 +752,31 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
744
752
|
)
|
|
745
753
|
self.assertNotIn(message_id, store.pending_calls)
|
|
746
754
|
|
|
755
|
+
configuration = await database_sync_to_async(
|
|
756
|
+
lambda: ChargerConfiguration.objects.order_by("-created_at").first()
|
|
757
|
+
)()
|
|
758
|
+
self.assertIsNotNone(configuration)
|
|
759
|
+
self.assertEqual(configuration.charger_identifier, "CFGRES")
|
|
760
|
+
self.assertEqual(
|
|
761
|
+
configuration.configuration_keys,
|
|
762
|
+
[
|
|
763
|
+
{
|
|
764
|
+
"key": "AllowOfflineTxForUnknownId",
|
|
765
|
+
"value": "false",
|
|
766
|
+
"readonly": True,
|
|
767
|
+
}
|
|
768
|
+
],
|
|
769
|
+
)
|
|
770
|
+
self.assertEqual(configuration.unknown_keys, [])
|
|
771
|
+
config_ids = await database_sync_to_async(
|
|
772
|
+
lambda: set(
|
|
773
|
+
Charger.objects.filter(charger_id="CFGRES").values_list(
|
|
774
|
+
"configuration_id", flat=True
|
|
775
|
+
)
|
|
776
|
+
)
|
|
777
|
+
)()
|
|
778
|
+
self.assertEqual(config_ids, {configuration.pk})
|
|
779
|
+
|
|
747
780
|
await communicator.disconnect()
|
|
748
781
|
store.clear_log(log_key, log_type="charger")
|
|
749
782
|
store.clear_log(pending_key, log_type="charger")
|
|
@@ -1109,7 +1142,7 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
1109
1142
|
|
|
1110
1143
|
await communicator.disconnect()
|
|
1111
1144
|
|
|
1112
|
-
async def
|
|
1145
|
+
async def test_vid_populated_from_vin(self):
|
|
1113
1146
|
await database_sync_to_async(Charger.objects.create)(charger_id="VINREC")
|
|
1114
1147
|
communicator = WebsocketCommunicator(application, "/VINREC/")
|
|
1115
1148
|
connected, _ = await communicator.connect()
|
|
@@ -1124,7 +1157,29 @@ class CSMSConsumerTests(TransactionTestCase):
|
|
|
1124
1157
|
tx = await database_sync_to_async(Transaction.objects.get)(
|
|
1125
1158
|
pk=tx_id, charger__charger_id="VINREC"
|
|
1126
1159
|
)
|
|
1127
|
-
self.assertEqual(tx.
|
|
1160
|
+
self.assertEqual(tx.vid, "WP0ZZZ11111111111")
|
|
1161
|
+
self.assertEqual(tx.vehicle_identifier, "WP0ZZZ11111111111")
|
|
1162
|
+
self.assertEqual(tx.vehicle_identifier_source, "vid")
|
|
1163
|
+
|
|
1164
|
+
await communicator.disconnect()
|
|
1165
|
+
|
|
1166
|
+
async def test_vid_recorded(self):
|
|
1167
|
+
await database_sync_to_async(Charger.objects.create)(charger_id="VIDREC")
|
|
1168
|
+
communicator = WebsocketCommunicator(application, "/VIDREC/")
|
|
1169
|
+
connected, _ = await communicator.connect()
|
|
1170
|
+
self.assertTrue(connected)
|
|
1171
|
+
|
|
1172
|
+
await communicator.send_json_to(
|
|
1173
|
+
[2, "1", "StartTransaction", {"meterStart": 1, "vid": "VID123456"}]
|
|
1174
|
+
)
|
|
1175
|
+
response = await communicator.receive_json_from()
|
|
1176
|
+
tx_id = response[2]["transactionId"]
|
|
1177
|
+
|
|
1178
|
+
tx = await database_sync_to_async(Transaction.objects.get)(
|
|
1179
|
+
pk=tx_id, charger__charger_id="VIDREC"
|
|
1180
|
+
)
|
|
1181
|
+
self.assertEqual(tx.vid, "VID123456")
|
|
1182
|
+
self.assertEqual(tx.rfid, "")
|
|
1128
1183
|
|
|
1129
1184
|
await communicator.disconnect()
|
|
1130
1185
|
|
|
@@ -1852,6 +1907,16 @@ class ChargerLandingTests(TestCase):
|
|
|
1852
1907
|
status_url = reverse("charger-status-connector", args=["PAGE1", "all"])
|
|
1853
1908
|
self.assertContains(response, status_url)
|
|
1854
1909
|
|
|
1910
|
+
def test_charger_page_respects_language_configuration(self):
|
|
1911
|
+
charger = Charger.objects.create(charger_id="PAGE-DE", language="de")
|
|
1912
|
+
|
|
1913
|
+
response = self.client.get(reverse("charger-page", args=["PAGE-DE"]))
|
|
1914
|
+
|
|
1915
|
+
self.assertEqual(response.status_code, 200)
|
|
1916
|
+
self.assertEqual(response.context["LANGUAGE_CODE"], "de")
|
|
1917
|
+
self.assertContains(response, 'lang="de"')
|
|
1918
|
+
self.assertContains(response, 'data-preferred-language="de"')
|
|
1919
|
+
|
|
1855
1920
|
def test_status_page_renders(self):
|
|
1856
1921
|
charger = Charger.objects.create(charger_id="PAGE2")
|
|
1857
1922
|
resp = self.client.get(reverse("charger-status", args=["PAGE2"]))
|
|
@@ -2078,7 +2143,10 @@ class ChargerLandingTests(TestCase):
|
|
|
2078
2143
|
log_id = store.identity_key("LOG1", None)
|
|
2079
2144
|
store.add_log(log_id, "hello", log_type="charger")
|
|
2080
2145
|
entry = store.get_logs(log_id, log_type="charger")[0]
|
|
2081
|
-
self.assertRegex(
|
|
2146
|
+
self.assertRegex(
|
|
2147
|
+
entry,
|
|
2148
|
+
r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} hello$",
|
|
2149
|
+
)
|
|
2082
2150
|
resp = self.client.get(reverse("charger-log", args=["LOG1"]) + "?type=charger")
|
|
2083
2151
|
self.assertEqual(resp.status_code, 200)
|
|
2084
2152
|
self.assertContains(resp, "hello")
|
|
@@ -2113,12 +2181,12 @@ class SimulatorLandingTests(TestCase):
|
|
|
2113
2181
|
@skip("Navigation links unavailable in test environment")
|
|
2114
2182
|
def test_simulator_app_link_in_nav(self):
|
|
2115
2183
|
resp = self.client.get(reverse("pages:index"))
|
|
2116
|
-
self.assertContains(resp, "/ocpp/")
|
|
2117
|
-
self.assertNotContains(resp, "/ocpp/simulator/")
|
|
2184
|
+
self.assertContains(resp, "/ocpp/cpms/dashboard/")
|
|
2185
|
+
self.assertNotContains(resp, "/ocpp/evcs/simulator/")
|
|
2118
2186
|
self.client.force_login(self.user)
|
|
2119
2187
|
resp = self.client.get(reverse("pages:index"))
|
|
2120
|
-
self.assertContains(resp, "/ocpp/")
|
|
2121
|
-
self.assertContains(resp, "/ocpp/simulator/")
|
|
2188
|
+
self.assertContains(resp, "/ocpp/cpms/dashboard/")
|
|
2189
|
+
self.assertContains(resp, "/ocpp/evcs/simulator/")
|
|
2122
2190
|
|
|
2123
2191
|
def test_cp_simulator_redirects_to_login(self):
|
|
2124
2192
|
response = self.client.get(reverse("cp-simulator"))
|
|
@@ -3199,8 +3267,7 @@ class SimulatorAdminTests(TransactionTestCase):
|
|
|
3199
3267
|
self.assertIsNotNone(aggregate.last_heartbeat)
|
|
3200
3268
|
if previous_heartbeat:
|
|
3201
3269
|
self.assertNotEqual(aggregate.last_heartbeat, previous_heartbeat)
|
|
3202
|
-
|
|
3203
|
-
self.assertNotEqual(aggregate.last_heartbeat, connector.last_heartbeat)
|
|
3270
|
+
self.assertEqual(connector.last_heartbeat, aggregate.last_heartbeat)
|
|
3204
3271
|
|
|
3205
3272
|
await communicator.disconnect()
|
|
3206
3273
|
|
|
@@ -3996,6 +4063,43 @@ class TransactionKwTests(TestCase):
|
|
|
3996
4063
|
self.assertEqual(tx.kw, 0.0)
|
|
3997
4064
|
|
|
3998
4065
|
|
|
4066
|
+
class TransactionIdentifierTests(TestCase):
|
|
4067
|
+
def test_vehicle_identifier_prefers_vid(self):
|
|
4068
|
+
charger = Charger.objects.create(charger_id="VIDPREF")
|
|
4069
|
+
tx = Transaction.objects.create(
|
|
4070
|
+
charger=charger,
|
|
4071
|
+
start_time=timezone.now(),
|
|
4072
|
+
vid="VID-123",
|
|
4073
|
+
vin="VIN-456",
|
|
4074
|
+
)
|
|
4075
|
+
self.assertEqual(tx.vehicle_identifier, "VID-123")
|
|
4076
|
+
self.assertEqual(tx.vehicle_identifier_source, "vid")
|
|
4077
|
+
|
|
4078
|
+
def test_vehicle_identifier_falls_back_to_vin(self):
|
|
4079
|
+
charger = Charger.objects.create(charger_id="VINONLY")
|
|
4080
|
+
tx = Transaction.objects.create(
|
|
4081
|
+
charger=charger,
|
|
4082
|
+
start_time=timezone.now(),
|
|
4083
|
+
vin="WP0ZZZ00000000001",
|
|
4084
|
+
)
|
|
4085
|
+
self.assertEqual(tx.vehicle_identifier, "WP0ZZZ00000000001")
|
|
4086
|
+
self.assertEqual(tx.vehicle_identifier_source, "vin")
|
|
4087
|
+
|
|
4088
|
+
def test_transaction_rfid_details_handles_vin(self):
|
|
4089
|
+
charger = Charger.objects.create(charger_id="VINDET")
|
|
4090
|
+
tx = Transaction.objects.create(
|
|
4091
|
+
charger=charger,
|
|
4092
|
+
start_time=timezone.now(),
|
|
4093
|
+
vin="WAUZZZ00000000002",
|
|
4094
|
+
)
|
|
4095
|
+
details = _transaction_rfid_details(tx, cache={})
|
|
4096
|
+
self.assertIsNotNone(details)
|
|
4097
|
+
assert details is not None # for type checkers
|
|
4098
|
+
self.assertEqual(details["value"], "WAUZZZ00000000002")
|
|
4099
|
+
self.assertEqual(details["display_label"], "VIN")
|
|
4100
|
+
self.assertEqual(details["type"], "vin")
|
|
4101
|
+
|
|
4102
|
+
|
|
3999
4103
|
class DispatchActionViewTests(TestCase):
|
|
4000
4104
|
def setUp(self):
|
|
4001
4105
|
self.client = Client()
|
ocpp/transactions_io.py
CHANGED
|
@@ -46,6 +46,7 @@ def export_transactions(
|
|
|
46
46
|
"charger": tx.charger.charger_id if tx.charger else None,
|
|
47
47
|
"account": tx.account_id,
|
|
48
48
|
"rfid": tx.rfid,
|
|
49
|
+
"vid": tx.vehicle_identifier,
|
|
49
50
|
"vin": tx.vin,
|
|
50
51
|
"meter_start": tx.meter_start,
|
|
51
52
|
"meter_stop": tx.meter_stop,
|
|
@@ -144,11 +145,18 @@ def import_transactions(data: dict) -> int:
|
|
|
144
145
|
except ValidationError:
|
|
145
146
|
continue
|
|
146
147
|
charger_map[serial] = charger
|
|
148
|
+
vid_value = tx.get("vid")
|
|
149
|
+
vin_value = tx.get("vin")
|
|
150
|
+
vid_text = str(vid_value).strip() if vid_value is not None else ""
|
|
151
|
+
vin_text = str(vin_value).strip() if vin_value is not None else ""
|
|
152
|
+
if not vid_text and vin_text:
|
|
153
|
+
vid_text = vin_text
|
|
147
154
|
transaction = Transaction.objects.create(
|
|
148
155
|
charger=charger,
|
|
149
156
|
account_id=tx.get("account"),
|
|
150
157
|
rfid=tx.get("rfid", ""),
|
|
151
|
-
|
|
158
|
+
vid=vid_text,
|
|
159
|
+
vin=vin_text,
|
|
152
160
|
meter_start=tx.get("meter_start"),
|
|
153
161
|
meter_stop=tx.get("meter_stop"),
|
|
154
162
|
voltage_start=tx.get("voltage_start"),
|
ocpp/urls.py
CHANGED
|
@@ -3,8 +3,8 @@ from django.urls import include, path
|
|
|
3
3
|
from . import views
|
|
4
4
|
|
|
5
5
|
urlpatterns = [
|
|
6
|
-
path("", views.dashboard, name="ocpp-dashboard"),
|
|
7
|
-
path("simulator/", views.cp_simulator, name="cp-simulator"),
|
|
6
|
+
path("cpms/dashboard/", views.dashboard, name="ocpp-dashboard"),
|
|
7
|
+
path("evcs/simulator/", views.cp_simulator, name="cp-simulator"),
|
|
8
8
|
path("chargers/", views.charger_list, name="charger-list"),
|
|
9
9
|
path("chargers/<str:cid>/", views.charger_detail, name="charger-detail"),
|
|
10
10
|
path(
|
|
@@ -46,5 +46,5 @@ urlpatterns = [
|
|
|
46
46
|
views.charger_status,
|
|
47
47
|
name="charger-status-connector",
|
|
48
48
|
),
|
|
49
|
-
path("rfid/", include("ocpp.rfid.urls")),
|
|
49
|
+
path("rfid/validator/", include("ocpp.rfid.urls")),
|
|
50
50
|
]
|
ocpp/views.py
CHANGED
|
@@ -11,6 +11,7 @@ from django.core.paginator import Paginator
|
|
|
11
11
|
from django.contrib.auth.decorators import login_required
|
|
12
12
|
from django.contrib.auth.views import redirect_to_login
|
|
13
13
|
from django.utils.translation import gettext_lazy as _, gettext, ngettext
|
|
14
|
+
from django.utils.text import slugify
|
|
14
15
|
from django.urls import NoReverseMatch, reverse
|
|
15
16
|
from django.conf import settings
|
|
16
17
|
from django.utils import translation, timezone
|
|
@@ -223,24 +224,75 @@ def _transaction_rfid_details(
|
|
|
223
224
|
if not tx_obj:
|
|
224
225
|
return None
|
|
225
226
|
rfid_value = getattr(tx_obj, "rfid", None)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
normalized
|
|
229
|
-
|
|
227
|
+
normalized = str(rfid_value or "").strip().upper()
|
|
228
|
+
cache_key = normalized
|
|
229
|
+
if normalized:
|
|
230
|
+
if cache is not None and cache_key in cache:
|
|
231
|
+
return cache[cache_key]
|
|
232
|
+
tag = (
|
|
233
|
+
RFID.matching_queryset(normalized)
|
|
234
|
+
.only("pk", "label_id", "custom_label")
|
|
235
|
+
.first()
|
|
236
|
+
)
|
|
237
|
+
rfid_url = None
|
|
238
|
+
label_value = None
|
|
239
|
+
canonical_value = normalized
|
|
240
|
+
if tag:
|
|
241
|
+
try:
|
|
242
|
+
rfid_url = reverse("admin:core_rfid_change", args=[tag.pk])
|
|
243
|
+
except NoReverseMatch: # pragma: no cover - admin may be disabled
|
|
244
|
+
rfid_url = None
|
|
245
|
+
custom_label = (tag.custom_label or "").strip()
|
|
246
|
+
if custom_label:
|
|
247
|
+
label_value = custom_label
|
|
248
|
+
elif tag.label_id is not None:
|
|
249
|
+
label_value = str(tag.label_id)
|
|
250
|
+
canonical_value = tag.rfid or canonical_value
|
|
251
|
+
display_value = label_value or canonical_value
|
|
252
|
+
details = {
|
|
253
|
+
"value": display_value,
|
|
254
|
+
"url": rfid_url,
|
|
255
|
+
"uid": canonical_value,
|
|
256
|
+
"type": "rfid",
|
|
257
|
+
"display_label": gettext("RFID"),
|
|
258
|
+
}
|
|
259
|
+
if label_value:
|
|
260
|
+
details["label"] = label_value
|
|
261
|
+
if cache is not None:
|
|
262
|
+
cache[cache_key] = details
|
|
263
|
+
return details
|
|
264
|
+
|
|
265
|
+
identifier_value = getattr(tx_obj, "vehicle_identifier", None)
|
|
266
|
+
normalized_identifier = str(identifier_value or "").strip()
|
|
267
|
+
if not normalized_identifier:
|
|
268
|
+
vid_value = getattr(tx_obj, "vid", None)
|
|
269
|
+
vin_value = getattr(tx_obj, "vin", None)
|
|
270
|
+
normalized_identifier = str(vid_value or vin_value or "").strip()
|
|
271
|
+
if not normalized_identifier:
|
|
230
272
|
return None
|
|
231
|
-
|
|
232
|
-
if
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
273
|
+
source = getattr(tx_obj, "vehicle_identifier_source", "") or "vid"
|
|
274
|
+
if source not in {"vid", "vin"}:
|
|
275
|
+
vid_raw = getattr(tx_obj, "vid", None)
|
|
276
|
+
vin_raw = getattr(tx_obj, "vin", None)
|
|
277
|
+
if str(vid_raw or "").strip():
|
|
278
|
+
source = "vid"
|
|
279
|
+
elif str(vin_raw or "").strip():
|
|
280
|
+
source = "vin"
|
|
281
|
+
else:
|
|
282
|
+
source = "vid"
|
|
283
|
+
cache_key = f"{source}:{normalized_identifier}"
|
|
284
|
+
if cache is not None and cache_key in cache:
|
|
285
|
+
return cache[cache_key]
|
|
286
|
+
label = gettext("VID") if source == "vid" else gettext("VIN")
|
|
287
|
+
details = {
|
|
288
|
+
"value": normalized_identifier,
|
|
289
|
+
"url": None,
|
|
290
|
+
"uid": None,
|
|
291
|
+
"type": source,
|
|
292
|
+
"display_label": label,
|
|
293
|
+
}
|
|
242
294
|
if cache is not None:
|
|
243
|
-
cache[
|
|
295
|
+
cache[cache_key] = details
|
|
244
296
|
return details
|
|
245
297
|
|
|
246
298
|
|
|
@@ -309,9 +361,14 @@ def _landing_page_translations() -> dict[str, dict[str, str]]:
|
|
|
309
361
|
"""Return static translations used by the charger public landing page."""
|
|
310
362
|
|
|
311
363
|
catalog: dict[str, dict[str, str]] = {}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
364
|
+
seen_codes: set[str] = set()
|
|
365
|
+
for code, _name in settings.LANGUAGES:
|
|
366
|
+
normalized = str(code).strip()
|
|
367
|
+
if not normalized or normalized in seen_codes:
|
|
368
|
+
continue
|
|
369
|
+
seen_codes.add(normalized)
|
|
370
|
+
with translation.override(normalized):
|
|
371
|
+
catalog[normalized] = {
|
|
315
372
|
"serial_number_label": gettext("Serial Number"),
|
|
316
373
|
"connector_label": gettext("Connector"),
|
|
317
374
|
"advanced_view_label": gettext("Advanced View"),
|
|
@@ -503,8 +560,12 @@ def charger_list(request):
|
|
|
503
560
|
"meterStart": tx_obj.meter_start,
|
|
504
561
|
"startTime": tx_obj.start_time.isoformat(),
|
|
505
562
|
}
|
|
506
|
-
|
|
507
|
-
|
|
563
|
+
identifier = str(getattr(tx_obj, "vehicle_identifier", "") or "").strip()
|
|
564
|
+
if identifier:
|
|
565
|
+
tx_data["vid"] = identifier
|
|
566
|
+
legacy_vin = str(getattr(tx_obj, "vin", "") or "").strip()
|
|
567
|
+
if legacy_vin:
|
|
568
|
+
tx_data["vin"] = legacy_vin
|
|
508
569
|
if tx_obj.meter_stop is not None:
|
|
509
570
|
tx_data["meterStop"] = tx_obj.meter_stop
|
|
510
571
|
if tx_obj.stop_time is not None:
|
|
@@ -519,8 +580,12 @@ def charger_list(request):
|
|
|
519
580
|
"meterStart": session_tx.meter_start,
|
|
520
581
|
"startTime": session_tx.start_time.isoformat(),
|
|
521
582
|
}
|
|
522
|
-
|
|
523
|
-
|
|
583
|
+
identifier = str(getattr(session_tx, "vehicle_identifier", "") or "").strip()
|
|
584
|
+
if identifier:
|
|
585
|
+
active_payload["vid"] = identifier
|
|
586
|
+
legacy_vin = str(getattr(session_tx, "vin", "") or "").strip()
|
|
587
|
+
if legacy_vin:
|
|
588
|
+
active_payload["vin"] = legacy_vin
|
|
524
589
|
if session_tx.meter_stop is not None:
|
|
525
590
|
active_payload["meterStop"] = session_tx.meter_stop
|
|
526
591
|
if session_tx.stop_time is not None:
|
|
@@ -600,8 +665,12 @@ def charger_detail(request, cid, connector=None):
|
|
|
600
665
|
"meterStart": tx_obj.meter_start,
|
|
601
666
|
"startTime": tx_obj.start_time.isoformat(),
|
|
602
667
|
}
|
|
603
|
-
|
|
604
|
-
|
|
668
|
+
identifier = str(getattr(tx_obj, "vehicle_identifier", "") or "").strip()
|
|
669
|
+
if identifier:
|
|
670
|
+
tx_data["vid"] = identifier
|
|
671
|
+
legacy_vin = str(getattr(tx_obj, "vin", "") or "").strip()
|
|
672
|
+
if legacy_vin:
|
|
673
|
+
tx_data["vin"] = legacy_vin
|
|
605
674
|
if tx_obj.meter_stop is not None:
|
|
606
675
|
tx_data["meterStop"] = tx_obj.meter_stop
|
|
607
676
|
if tx_obj.stop_time is not None:
|
|
@@ -617,8 +686,12 @@ def charger_detail(request, cid, connector=None):
|
|
|
617
686
|
"meterStart": session_tx.meter_start,
|
|
618
687
|
"startTime": session_tx.start_time.isoformat(),
|
|
619
688
|
}
|
|
620
|
-
|
|
621
|
-
|
|
689
|
+
identifier = str(getattr(session_tx, "vehicle_identifier", "") or "").strip()
|
|
690
|
+
if identifier:
|
|
691
|
+
payload["vid"] = identifier
|
|
692
|
+
legacy_vin = str(getattr(session_tx, "vin", "") or "").strip()
|
|
693
|
+
if legacy_vin:
|
|
694
|
+
payload["vin"] = legacy_vin
|
|
622
695
|
if session_tx.meter_stop is not None:
|
|
623
696
|
payload["meterStop"] = session_tx.meter_stop
|
|
624
697
|
if session_tx.stop_time is not None:
|
|
@@ -673,12 +746,12 @@ def dashboard(request):
|
|
|
673
746
|
node = Node.get_local()
|
|
674
747
|
role = node.role if node else None
|
|
675
748
|
role_name = role.name if role else ""
|
|
676
|
-
allow_anonymous_roles = {"Constellation", "Satellite"}
|
|
749
|
+
allow_anonymous_roles = {"Watchtower", "Constellation", "Satellite"}
|
|
677
750
|
if not request.user.is_authenticated and role_name not in allow_anonymous_roles:
|
|
678
751
|
return redirect_to_login(
|
|
679
752
|
request.get_full_path(), login_url=reverse("pages:login")
|
|
680
753
|
)
|
|
681
|
-
|
|
754
|
+
is_watchtower = role_name in {"Watchtower", "Constellation"}
|
|
682
755
|
chargers = []
|
|
683
756
|
for charger in _visible_chargers(request.user):
|
|
684
757
|
tx_obj = store.get_transaction(charger.charger_id, charger.connector_id)
|
|
@@ -695,7 +768,7 @@ def dashboard(request):
|
|
|
695
768
|
ws_url = f"{scheme}://{host}/ocpp/<CHARGE_POINT_ID>/"
|
|
696
769
|
context = {
|
|
697
770
|
"chargers": chargers,
|
|
698
|
-
"show_demo_notice":
|
|
771
|
+
"show_demo_notice": is_watchtower,
|
|
699
772
|
"demo_ws_url": ws_url,
|
|
700
773
|
"ws_rate_limit": store.MAX_CONNECTIONS_PER_IP,
|
|
701
774
|
}
|
|
@@ -842,11 +915,30 @@ def charger_page(request, cid, connector=None):
|
|
|
842
915
|
state_source = tx if charger.connector_id is not None else (sessions if sessions else None)
|
|
843
916
|
state, color = _charger_state(charger, state_source)
|
|
844
917
|
language_cookie = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
918
|
+
available_languages = [
|
|
919
|
+
str(code).strip()
|
|
920
|
+
for code, _ in settings.LANGUAGES
|
|
921
|
+
if str(code).strip()
|
|
922
|
+
]
|
|
923
|
+
supported_languages = set(available_languages)
|
|
924
|
+
charger_language = (charger.language or "es").strip()
|
|
925
|
+
if charger_language not in supported_languages:
|
|
926
|
+
fallback = "es" if "es" in supported_languages else ""
|
|
927
|
+
if not fallback and available_languages:
|
|
928
|
+
fallback = available_languages[0]
|
|
929
|
+
charger_language = fallback
|
|
930
|
+
if (
|
|
931
|
+
charger_language
|
|
932
|
+
and (
|
|
933
|
+
not language_cookie
|
|
934
|
+
or language_cookie not in supported_languages
|
|
935
|
+
or language_cookie != charger_language
|
|
936
|
+
)
|
|
937
|
+
):
|
|
938
|
+
translation.activate(charger_language)
|
|
939
|
+
current_language = translation.get_language()
|
|
940
|
+
request.LANGUAGE_CODE = current_language
|
|
941
|
+
preferred_language = charger_language or current_language
|
|
850
942
|
connector_links = [
|
|
851
943
|
{
|
|
852
944
|
"slug": item["slug"],
|
|
@@ -874,6 +966,7 @@ def charger_page(request, cid, connector=None):
|
|
|
874
966
|
"active_connector_count": active_connector_count,
|
|
875
967
|
"status_url": status_url,
|
|
876
968
|
"landing_translations": _landing_page_translations(),
|
|
969
|
+
"preferred_language": preferred_language,
|
|
877
970
|
"state": state,
|
|
878
971
|
"color": color,
|
|
879
972
|
},
|
|
@@ -1023,7 +1116,10 @@ def charger_status(request, cid, connector=None):
|
|
|
1023
1116
|
"connector_id": connector_id,
|
|
1024
1117
|
}
|
|
1025
1118
|
)
|
|
1026
|
-
|
|
1119
|
+
rfid_cache: dict[str, dict[str, str | None]] = {}
|
|
1120
|
+
overview = _connector_overview(
|
|
1121
|
+
charger, request.user, rfid_cache=rfid_cache
|
|
1122
|
+
)
|
|
1027
1123
|
connector_links = [
|
|
1028
1124
|
{
|
|
1029
1125
|
"slug": item["slug"],
|
|
@@ -1064,12 +1160,15 @@ def charger_status(request, cid, connector=None):
|
|
|
1064
1160
|
action_url = _reverse_connector_url("charger-action", cid, connector_slug)
|
|
1065
1161
|
chart_should_animate = bool(has_active_session and not past_session)
|
|
1066
1162
|
|
|
1163
|
+
tx_rfid_details = _transaction_rfid_details(tx_obj, cache=rfid_cache)
|
|
1164
|
+
|
|
1067
1165
|
return render(
|
|
1068
1166
|
request,
|
|
1069
1167
|
"ocpp/charger_status.html",
|
|
1070
1168
|
{
|
|
1071
1169
|
"charger": charger,
|
|
1072
1170
|
"tx": tx_obj,
|
|
1171
|
+
"tx_rfid_details": tx_rfid_details,
|
|
1073
1172
|
"state": state,
|
|
1074
1173
|
"color": color,
|
|
1075
1174
|
"transactions": transactions,
|
|
@@ -1209,8 +1308,11 @@ def charger_log_page(request, cid, connector=None):
|
|
|
1209
1308
|
charger_id=cid
|
|
1210
1309
|
)
|
|
1211
1310
|
target_id = cid
|
|
1311
|
+
|
|
1312
|
+
slug_source = slugify(target_id) or slugify(cid) or "log"
|
|
1313
|
+
filename_parts = [log_type, slug_source]
|
|
1314
|
+
download_filename = f"{'-'.join(part for part in filename_parts if part)}.log"
|
|
1212
1315
|
limit_options = [
|
|
1213
|
-
{"value": "10", "label": "10"},
|
|
1214
1316
|
{"value": "20", "label": "20"},
|
|
1215
1317
|
{"value": "40", "label": "40"},
|
|
1216
1318
|
{"value": "100", "label": "100"},
|
|
@@ -1220,15 +1322,35 @@ def charger_log_page(request, cid, connector=None):
|
|
|
1220
1322
|
limit_choice = request.GET.get("limit", "20")
|
|
1221
1323
|
if limit_choice not in allowed_values:
|
|
1222
1324
|
limit_choice = "20"
|
|
1223
|
-
|
|
1224
|
-
|
|
1325
|
+
limit_index = allowed_values.index(limit_choice)
|
|
1326
|
+
|
|
1327
|
+
log_entries_all = list(store.get_logs(target_id, log_type=log_type) or [])
|
|
1328
|
+
download_requested = request.GET.get("download") == "1"
|
|
1329
|
+
if download_requested:
|
|
1330
|
+
download_content = "\n".join(log_entries_all)
|
|
1331
|
+
if download_content and not download_content.endswith("\n"):
|
|
1332
|
+
download_content = f"{download_content}\n"
|
|
1333
|
+
response = HttpResponse(download_content, content_type="text/plain; charset=utf-8")
|
|
1334
|
+
response["Content-Disposition"] = f'attachment; filename="{download_filename}"'
|
|
1335
|
+
return response
|
|
1336
|
+
|
|
1337
|
+
log_entries = log_entries_all
|
|
1225
1338
|
if limit_choice != "all":
|
|
1226
1339
|
try:
|
|
1227
1340
|
limit_value = int(limit_choice)
|
|
1228
1341
|
except (TypeError, ValueError):
|
|
1229
1342
|
limit_value = 20
|
|
1230
1343
|
limit_choice = "20"
|
|
1344
|
+
limit_index = allowed_values.index(limit_choice)
|
|
1231
1345
|
log_entries = log_entries[-limit_value:]
|
|
1346
|
+
|
|
1347
|
+
download_params = request.GET.copy()
|
|
1348
|
+
download_params["download"] = "1"
|
|
1349
|
+
download_params.pop("limit", None)
|
|
1350
|
+
download_query = download_params.urlencode()
|
|
1351
|
+
log_download_url = f"{request.path}?{download_query}" if download_query else request.path
|
|
1352
|
+
|
|
1353
|
+
limit_label = limit_options[limit_index]["label"]
|
|
1232
1354
|
return render(
|
|
1233
1355
|
request,
|
|
1234
1356
|
"ocpp/charger_logs.html",
|
|
@@ -1240,7 +1362,11 @@ def charger_log_page(request, cid, connector=None):
|
|
|
1240
1362
|
"connector_links": connector_links,
|
|
1241
1363
|
"status_url": status_url,
|
|
1242
1364
|
"log_limit_options": limit_options,
|
|
1243
|
-
"log_limit_index":
|
|
1365
|
+
"log_limit_index": limit_index,
|
|
1366
|
+
"log_limit_choice": limit_choice,
|
|
1367
|
+
"log_limit_label": limit_label,
|
|
1368
|
+
"log_download_url": log_download_url,
|
|
1369
|
+
"log_filename": download_filename,
|
|
1244
1370
|
},
|
|
1245
1371
|
)
|
|
1246
1372
|
|