arthexis 0.1.8__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.8.dist-info → arthexis-0.1.9.dist-info}/METADATA +42 -4
- 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 +133 -16
- config/urls.py +65 -6
- core/admin.py +1226 -191
- core/admin_history.py +50 -0
- core/admindocs.py +108 -1
- 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 +1071 -264
- 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 +358 -63
- 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 +7 -3
- core/workgroup_views.py +43 -6
- 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 +1 -1
- 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.8.dist-info/RECORD +0 -80
- arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
- {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
ocpp/views.py
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
from datetime import datetime, timedelta, timezone as dt_timezone
|
|
4
|
+
from types import SimpleNamespace
|
|
4
5
|
|
|
5
|
-
from django.http import JsonResponse,
|
|
6
|
+
from django.http import JsonResponse, Http404
|
|
7
|
+
from django.http.request import split_domain_port
|
|
6
8
|
from django.views.decorators.csrf import csrf_exempt
|
|
7
9
|
from django.shortcuts import render, get_object_or_404
|
|
8
10
|
from django.core.paginator import Paginator
|
|
9
11
|
from django.contrib.auth.decorators import login_required
|
|
10
|
-
from django.utils.translation import gettext_lazy as _
|
|
12
|
+
from django.utils.translation import gettext_lazy as _, gettext, ngettext
|
|
13
|
+
from django.urls import NoReverseMatch, reverse
|
|
14
|
+
from django.conf import settings
|
|
15
|
+
from django.utils import translation
|
|
11
16
|
|
|
12
17
|
from utils.api import api_login_required
|
|
13
18
|
|
|
14
19
|
from pages.utils import landing
|
|
20
|
+
from core.liveupdate import live_update
|
|
15
21
|
|
|
16
22
|
from . import store
|
|
17
23
|
from .models import Transaction, Charger
|
|
@@ -23,17 +29,201 @@ from .evcs import (
|
|
|
23
29
|
)
|
|
24
30
|
|
|
25
31
|
|
|
26
|
-
def
|
|
32
|
+
def _normalize_connector_slug(slug: str | None) -> tuple[int | None, str]:
|
|
33
|
+
"""Return connector value and normalized slug or raise 404."""
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
value = Charger.connector_value_from_slug(slug)
|
|
37
|
+
except ValueError as exc: # pragma: no cover - defensive guard
|
|
38
|
+
raise Http404("Invalid connector") from exc
|
|
39
|
+
return value, Charger.connector_slug_from_value(value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _reverse_connector_url(name: str, serial: str, connector_slug: str) -> str:
|
|
43
|
+
"""Return URL name for connector-aware routes."""
|
|
44
|
+
|
|
45
|
+
target = f"{name}-connector"
|
|
46
|
+
if connector_slug == Charger.AGGREGATE_CONNECTOR_SLUG:
|
|
47
|
+
try:
|
|
48
|
+
return reverse(target, args=[serial, connector_slug])
|
|
49
|
+
except NoReverseMatch:
|
|
50
|
+
return reverse(name, args=[serial])
|
|
51
|
+
return reverse(target, args=[serial, connector_slug])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_charger(serial: str, connector_slug: str | None) -> tuple[Charger, str]:
|
|
55
|
+
"""Return charger for the requested identity, creating if necessary."""
|
|
56
|
+
|
|
57
|
+
connector_value, normalized_slug = _normalize_connector_slug(connector_slug)
|
|
58
|
+
if connector_value is None:
|
|
59
|
+
charger, _ = Charger.objects.get_or_create(
|
|
60
|
+
charger_id=serial,
|
|
61
|
+
connector_id=None,
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
charger, _ = Charger.objects.get_or_create(
|
|
65
|
+
charger_id=serial,
|
|
66
|
+
connector_id=connector_value,
|
|
67
|
+
)
|
|
68
|
+
return charger, normalized_slug
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _connector_set(charger: Charger) -> list[Charger]:
|
|
72
|
+
"""Return chargers sharing the same serial ordered for navigation."""
|
|
73
|
+
|
|
74
|
+
siblings = list(Charger.objects.filter(charger_id=charger.charger_id))
|
|
75
|
+
siblings.sort(key=lambda c: (c.connector_id is not None, c.connector_id or 0))
|
|
76
|
+
return siblings
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _connector_overview(charger: Charger) -> list[dict]:
|
|
80
|
+
"""Return connector metadata used for navigation and summaries."""
|
|
81
|
+
|
|
82
|
+
overview: list[dict] = []
|
|
83
|
+
for sibling in _connector_set(charger):
|
|
84
|
+
tx_obj = store.get_transaction(sibling.charger_id, sibling.connector_id)
|
|
85
|
+
state, color = _charger_state(sibling, tx_obj)
|
|
86
|
+
overview.append(
|
|
87
|
+
{
|
|
88
|
+
"charger": sibling,
|
|
89
|
+
"slug": sibling.connector_slug,
|
|
90
|
+
"label": sibling.connector_label,
|
|
91
|
+
"url": _reverse_connector_url(
|
|
92
|
+
"charger-page", sibling.charger_id, sibling.connector_slug
|
|
93
|
+
),
|
|
94
|
+
"status": state,
|
|
95
|
+
"color": color,
|
|
96
|
+
"last_status": sibling.last_status,
|
|
97
|
+
"last_error_code": sibling.last_error_code,
|
|
98
|
+
"last_status_timestamp": sibling.last_status_timestamp,
|
|
99
|
+
"last_status_vendor_info": sibling.last_status_vendor_info,
|
|
100
|
+
"tx": tx_obj,
|
|
101
|
+
"connected": store.is_connected(
|
|
102
|
+
sibling.charger_id, sibling.connector_id
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
return overview
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _live_sessions(charger: Charger) -> list[tuple[Charger, Transaction]]:
|
|
110
|
+
"""Return active sessions grouped by connector for the charger."""
|
|
111
|
+
|
|
112
|
+
siblings = _connector_set(charger)
|
|
113
|
+
ordered = [c for c in siblings if c.connector_id is not None] + [
|
|
114
|
+
c for c in siblings if c.connector_id is None
|
|
115
|
+
]
|
|
116
|
+
sessions: list[tuple[Charger, Transaction]] = []
|
|
117
|
+
seen: set[int] = set()
|
|
118
|
+
for sibling in ordered:
|
|
119
|
+
tx_obj = store.get_transaction(sibling.charger_id, sibling.connector_id)
|
|
120
|
+
if not tx_obj:
|
|
121
|
+
continue
|
|
122
|
+
if tx_obj.pk and tx_obj.pk in seen:
|
|
123
|
+
continue
|
|
124
|
+
if tx_obj.pk:
|
|
125
|
+
seen.add(tx_obj.pk)
|
|
126
|
+
sessions.append((sibling, tx_obj))
|
|
127
|
+
return sessions
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _landing_page_translations() -> dict[str, dict[str, str]]:
|
|
131
|
+
"""Return static translations used by the charger public landing page."""
|
|
132
|
+
|
|
133
|
+
catalog: dict[str, dict[str, str]] = {}
|
|
134
|
+
for code in ("en", "es"):
|
|
135
|
+
with translation.override(code):
|
|
136
|
+
catalog[code] = {
|
|
137
|
+
"serial_number_label": gettext("Serial Number"),
|
|
138
|
+
"connector_label": gettext("Connector"),
|
|
139
|
+
"advanced_view_label": gettext("Advanced View"),
|
|
140
|
+
"require_rfid_label": gettext("Require RFID Authorization"),
|
|
141
|
+
"charging_label": gettext("Charging"),
|
|
142
|
+
"energy_label": gettext("Energy"),
|
|
143
|
+
"started_label": gettext("Started"),
|
|
144
|
+
"instruction_text": gettext(
|
|
145
|
+
"Plug in your vehicle and slide your RFID card over the reader to begin charging."
|
|
146
|
+
),
|
|
147
|
+
"connectors_heading": gettext("Connectors"),
|
|
148
|
+
"no_active_transaction": gettext("No active transaction"),
|
|
149
|
+
"connectors_active_singular": ngettext(
|
|
150
|
+
"%(count)s connector active",
|
|
151
|
+
"%(count)s connectors active",
|
|
152
|
+
1,
|
|
153
|
+
),
|
|
154
|
+
"connectors_active_plural": ngettext(
|
|
155
|
+
"%(count)s connector active",
|
|
156
|
+
"%(count)s connectors active",
|
|
157
|
+
2,
|
|
158
|
+
),
|
|
159
|
+
"status_reported_label": gettext("Reported status"),
|
|
160
|
+
"status_error_label": gettext("Error code"),
|
|
161
|
+
"status_updated_label": gettext("Last status update"),
|
|
162
|
+
"status_vendor_label": gettext("Vendor"),
|
|
163
|
+
"status_info_label": gettext("Info"),
|
|
164
|
+
}
|
|
165
|
+
return catalog
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
STATUS_BADGE_MAP: dict[str, tuple[str, str]] = {
|
|
169
|
+
"available": (_("Available"), "#0d6efd"),
|
|
170
|
+
"preparing": (_("Preparing"), "#0d6efd"),
|
|
171
|
+
"charging": (_("Charging"), "#198754"),
|
|
172
|
+
"suspendedevse": (_("Suspended (EVSE)"), "#fd7e14"),
|
|
173
|
+
"suspendedev": (_("Suspended (EV)"), "#fd7e14"),
|
|
174
|
+
"finishing": (_("Finishing"), "#20c997"),
|
|
175
|
+
"faulted": (_("Faulted"), "#dc3545"),
|
|
176
|
+
"unavailable": (_("Unavailable"), "#6c757d"),
|
|
177
|
+
"reserved": (_("Reserved"), "#6f42c1"),
|
|
178
|
+
"occupied": (_("Occupied"), "#0dcaf0"),
|
|
179
|
+
"outofservice": (_("Out of Service"), "#6c757d"),
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_ERROR_OK_VALUES = {"", "noerror", "no_error"}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _charger_state(charger: Charger, tx_obj: Transaction | list | None):
|
|
27
186
|
"""Return human readable state and color for a charger."""
|
|
187
|
+
|
|
188
|
+
status_value = (charger.last_status or "").strip()
|
|
189
|
+
if status_value:
|
|
190
|
+
key = status_value.lower()
|
|
191
|
+
label, color = STATUS_BADGE_MAP.get(key, (status_value, "#0d6efd"))
|
|
192
|
+
error_code = (charger.last_error_code or "").strip()
|
|
193
|
+
if error_code and error_code.lower() not in _ERROR_OK_VALUES:
|
|
194
|
+
label = _("%(status)s (%(error)s)") % {
|
|
195
|
+
"status": label,
|
|
196
|
+
"error": error_code,
|
|
197
|
+
}
|
|
198
|
+
color = "#dc3545"
|
|
199
|
+
return label, color
|
|
200
|
+
|
|
28
201
|
cid = charger.charger_id
|
|
29
|
-
connected = cid
|
|
30
|
-
|
|
31
|
-
|
|
202
|
+
connected = store.is_connected(cid, charger.connector_id)
|
|
203
|
+
has_session = bool(tx_obj)
|
|
204
|
+
if connected and has_session:
|
|
205
|
+
return _("Charging"), "green"
|
|
32
206
|
if connected:
|
|
33
|
-
return "Available", "blue"
|
|
34
|
-
return "Offline", "grey"
|
|
207
|
+
return _("Available"), "blue"
|
|
208
|
+
return _("Offline"), "grey"
|
|
35
209
|
|
|
36
210
|
|
|
211
|
+
def _diagnostics_payload(charger: Charger) -> dict[str, str | None]:
|
|
212
|
+
"""Return diagnostics metadata for API responses."""
|
|
213
|
+
|
|
214
|
+
timestamp = (
|
|
215
|
+
charger.diagnostics_timestamp.isoformat()
|
|
216
|
+
if charger.diagnostics_timestamp
|
|
217
|
+
else None
|
|
218
|
+
)
|
|
219
|
+
status = charger.diagnostics_status or None
|
|
220
|
+
location = charger.diagnostics_location or None
|
|
221
|
+
return {
|
|
222
|
+
"diagnosticsStatus": status,
|
|
223
|
+
"diagnosticsTimestamp": timestamp,
|
|
224
|
+
"diagnosticsLocation": location,
|
|
225
|
+
}
|
|
226
|
+
|
|
37
227
|
|
|
38
228
|
@api_login_required
|
|
39
229
|
def charger_list(request):
|
|
@@ -41,7 +231,14 @@ def charger_list(request):
|
|
|
41
231
|
data = []
|
|
42
232
|
for charger in Charger.objects.all():
|
|
43
233
|
cid = charger.charger_id
|
|
44
|
-
|
|
234
|
+
sessions: list[tuple[Charger, Transaction]] = []
|
|
235
|
+
tx_obj = store.get_transaction(cid, charger.connector_id)
|
|
236
|
+
if charger.connector_id is None:
|
|
237
|
+
sessions = _live_sessions(charger)
|
|
238
|
+
if sessions:
|
|
239
|
+
tx_obj = sessions[0][1]
|
|
240
|
+
elif tx_obj:
|
|
241
|
+
sessions = [(charger, tx_obj)]
|
|
45
242
|
if not tx_obj:
|
|
46
243
|
tx_obj = (
|
|
47
244
|
Transaction.objects.filter(charger__charger_id=cid)
|
|
@@ -61,27 +258,78 @@ def charger_list(request):
|
|
|
61
258
|
tx_data["meterStop"] = tx_obj.meter_stop
|
|
62
259
|
if tx_obj.stop_time is not None:
|
|
63
260
|
tx_data["stopTime"] = tx_obj.stop_time.isoformat()
|
|
261
|
+
active_transactions = []
|
|
262
|
+
for session_charger, session_tx in sessions:
|
|
263
|
+
active_payload = {
|
|
264
|
+
"charger_id": session_charger.charger_id,
|
|
265
|
+
"connector_id": session_charger.connector_id,
|
|
266
|
+
"connector_slug": session_charger.connector_slug,
|
|
267
|
+
"transactionId": session_tx.pk,
|
|
268
|
+
"meterStart": session_tx.meter_start,
|
|
269
|
+
"startTime": session_tx.start_time.isoformat(),
|
|
270
|
+
}
|
|
271
|
+
if session_tx.vin:
|
|
272
|
+
active_payload["vin"] = session_tx.vin
|
|
273
|
+
if session_tx.meter_stop is not None:
|
|
274
|
+
active_payload["meterStop"] = session_tx.meter_stop
|
|
275
|
+
if session_tx.stop_time is not None:
|
|
276
|
+
active_payload["stopTime"] = session_tx.stop_time.isoformat()
|
|
277
|
+
active_transactions.append(active_payload)
|
|
278
|
+
state, color = _charger_state(
|
|
279
|
+
charger,
|
|
280
|
+
tx_obj if charger.connector_id is not None else (sessions if sessions else None),
|
|
281
|
+
)
|
|
64
282
|
data.append(
|
|
65
283
|
{
|
|
66
284
|
"charger_id": cid,
|
|
67
285
|
"name": charger.name,
|
|
286
|
+
"connector_id": charger.connector_id,
|
|
287
|
+
"connector_slug": charger.connector_slug,
|
|
288
|
+
"connector_label": charger.connector_label,
|
|
68
289
|
"require_rfid": charger.require_rfid,
|
|
69
290
|
"transaction": tx_data,
|
|
70
|
-
"
|
|
291
|
+
"activeTransactions": active_transactions,
|
|
292
|
+
"lastHeartbeat": (
|
|
293
|
+
charger.last_heartbeat.isoformat()
|
|
294
|
+
if charger.last_heartbeat
|
|
295
|
+
else None
|
|
296
|
+
),
|
|
71
297
|
"lastMeterValues": charger.last_meter_values,
|
|
72
|
-
"
|
|
298
|
+
"firmwareStatus": charger.firmware_status,
|
|
299
|
+
"firmwareStatusInfo": charger.firmware_status_info,
|
|
300
|
+
"firmwareTimestamp": (
|
|
301
|
+
charger.firmware_timestamp.isoformat()
|
|
302
|
+
if charger.firmware_timestamp
|
|
303
|
+
else None
|
|
304
|
+
),
|
|
305
|
+
"connected": store.is_connected(cid, charger.connector_id),
|
|
306
|
+
"lastStatus": charger.last_status or None,
|
|
307
|
+
"lastErrorCode": charger.last_error_code or None,
|
|
308
|
+
"lastStatusTimestamp": (
|
|
309
|
+
charger.last_status_timestamp.isoformat()
|
|
310
|
+
if charger.last_status_timestamp
|
|
311
|
+
else None
|
|
312
|
+
),
|
|
313
|
+
"lastStatusVendorInfo": charger.last_status_vendor_info,
|
|
314
|
+
"status": state,
|
|
315
|
+
"statusColor": color,
|
|
73
316
|
}
|
|
74
317
|
)
|
|
75
318
|
return JsonResponse({"chargers": data})
|
|
76
319
|
|
|
77
320
|
|
|
78
321
|
@api_login_required
|
|
79
|
-
def charger_detail(request, cid):
|
|
80
|
-
charger =
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
322
|
+
def charger_detail(request, cid, connector=None):
|
|
323
|
+
charger, connector_slug = _get_charger(cid, connector)
|
|
324
|
+
|
|
325
|
+
sessions: list[tuple[Charger, Transaction]] = []
|
|
326
|
+
tx_obj = store.get_transaction(cid, charger.connector_id)
|
|
327
|
+
if charger.connector_id is None:
|
|
328
|
+
sessions = _live_sessions(charger)
|
|
329
|
+
if sessions:
|
|
330
|
+
tx_obj = sessions[0][1]
|
|
331
|
+
elif tx_obj:
|
|
332
|
+
sessions = [(charger, tx_obj)]
|
|
85
333
|
if not tx_obj:
|
|
86
334
|
tx_obj = (
|
|
87
335
|
Transaction.objects.filter(charger__charger_id=cid)
|
|
@@ -103,27 +351,73 @@ def charger_detail(request, cid):
|
|
|
103
351
|
if tx_obj.stop_time is not None:
|
|
104
352
|
tx_data["stopTime"] = tx_obj.stop_time.isoformat()
|
|
105
353
|
|
|
106
|
-
|
|
354
|
+
active_transactions = []
|
|
355
|
+
for session_charger, session_tx in sessions:
|
|
356
|
+
payload = {
|
|
357
|
+
"charger_id": session_charger.charger_id,
|
|
358
|
+
"connector_id": session_charger.connector_id,
|
|
359
|
+
"connector_slug": session_charger.connector_slug,
|
|
360
|
+
"transactionId": session_tx.pk,
|
|
361
|
+
"meterStart": session_tx.meter_start,
|
|
362
|
+
"startTime": session_tx.start_time.isoformat(),
|
|
363
|
+
}
|
|
364
|
+
if session_tx.vin:
|
|
365
|
+
payload["vin"] = session_tx.vin
|
|
366
|
+
if session_tx.meter_stop is not None:
|
|
367
|
+
payload["meterStop"] = session_tx.meter_stop
|
|
368
|
+
if session_tx.stop_time is not None:
|
|
369
|
+
payload["stopTime"] = session_tx.stop_time.isoformat()
|
|
370
|
+
active_transactions.append(payload)
|
|
371
|
+
|
|
372
|
+
log_key = store.identity_key(cid, charger.connector_id)
|
|
373
|
+
log = store.get_logs(log_key, log_type="charger")
|
|
374
|
+
state, color = _charger_state(
|
|
375
|
+
charger,
|
|
376
|
+
tx_obj if charger.connector_id is not None else (sessions if sessions else None),
|
|
377
|
+
)
|
|
107
378
|
return JsonResponse(
|
|
108
379
|
{
|
|
109
380
|
"charger_id": cid,
|
|
381
|
+
"connector_id": charger.connector_id,
|
|
382
|
+
"connector_slug": connector_slug,
|
|
110
383
|
"name": charger.name,
|
|
111
384
|
"require_rfid": charger.require_rfid,
|
|
112
385
|
"transaction": tx_data,
|
|
113
|
-
"
|
|
386
|
+
"activeTransactions": active_transactions,
|
|
387
|
+
"lastHeartbeat": (
|
|
388
|
+
charger.last_heartbeat.isoformat() if charger.last_heartbeat else None
|
|
389
|
+
),
|
|
114
390
|
"lastMeterValues": charger.last_meter_values,
|
|
391
|
+
"firmwareStatus": charger.firmware_status,
|
|
392
|
+
"firmwareStatusInfo": charger.firmware_status_info,
|
|
393
|
+
"firmwareTimestamp": (
|
|
394
|
+
charger.firmware_timestamp.isoformat()
|
|
395
|
+
if charger.firmware_timestamp
|
|
396
|
+
else None
|
|
397
|
+
),
|
|
115
398
|
"log": log,
|
|
399
|
+
"lastStatus": charger.last_status or None,
|
|
400
|
+
"lastErrorCode": charger.last_error_code or None,
|
|
401
|
+
"lastStatusTimestamp": (
|
|
402
|
+
charger.last_status_timestamp.isoformat()
|
|
403
|
+
if charger.last_status_timestamp
|
|
404
|
+
else None
|
|
405
|
+
),
|
|
406
|
+
"lastStatusVendorInfo": charger.last_status_vendor_info,
|
|
407
|
+
"status": state,
|
|
408
|
+
"statusColor": color,
|
|
116
409
|
}
|
|
117
410
|
)
|
|
118
411
|
|
|
119
412
|
|
|
120
413
|
@login_required
|
|
121
414
|
@landing("Dashboard")
|
|
415
|
+
@live_update()
|
|
122
416
|
def dashboard(request):
|
|
123
417
|
"""Landing page listing all known chargers and their status."""
|
|
124
418
|
chargers = []
|
|
125
419
|
for charger in Charger.objects.all():
|
|
126
|
-
tx_obj = store.
|
|
420
|
+
tx_obj = store.get_transaction(charger.charger_id, charger.connector_id)
|
|
127
421
|
if not tx_obj:
|
|
128
422
|
tx_obj = (
|
|
129
423
|
Transaction.objects.filter(charger=charger)
|
|
@@ -137,11 +431,17 @@ def dashboard(request):
|
|
|
137
431
|
|
|
138
432
|
@login_required
|
|
139
433
|
@landing("CP Simulator")
|
|
434
|
+
@live_update()
|
|
140
435
|
def cp_simulator(request):
|
|
141
436
|
"""Public landing page to control the OCPP charge point simulator."""
|
|
142
|
-
|
|
143
|
-
|
|
437
|
+
host_header = request.get_host()
|
|
438
|
+
default_host, host_port = split_domain_port(host_header)
|
|
439
|
+
if not default_host:
|
|
440
|
+
default_host = "127.0.0.1"
|
|
441
|
+
default_ws_port = request.get_port() or host_port or "8000"
|
|
144
442
|
default_cp_paths = ["CP1", "CP2"]
|
|
443
|
+
default_serial_numbers = default_cp_paths
|
|
444
|
+
default_connector_id = 1
|
|
145
445
|
default_rfid = "FFFFFFFF"
|
|
146
446
|
default_vins = ["WP0ZZZ00000000000", "WAUZZZ00000000000"]
|
|
147
447
|
|
|
@@ -153,11 +453,15 @@ def cp_simulator(request):
|
|
|
153
453
|
sim_params = dict(
|
|
154
454
|
host=request.POST.get("host") or default_host,
|
|
155
455
|
ws_port=int(request.POST.get("ws_port") or default_ws_port),
|
|
156
|
-
cp_path=request.POST.get("cp_path")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
456
|
+
cp_path=request.POST.get("cp_path") or default_cp_paths[cp_idx - 1],
|
|
457
|
+
serial_number=request.POST.get("serial_number")
|
|
458
|
+
or default_serial_numbers[cp_idx - 1],
|
|
459
|
+
connector_id=int(
|
|
460
|
+
request.POST.get("connector_id") or default_connector_id
|
|
461
|
+
),
|
|
462
|
+
rfid=request.POST.get("rfid") or default_rfid,
|
|
463
|
+
vin=request.POST.get("vin") or default_vins[cp_idx - 1],
|
|
464
|
+
duration=int(request.POST.get("duration") or 600),
|
|
161
465
|
interval=float(request.POST.get("interval") or 5),
|
|
162
466
|
kw_min=float(request.POST.get("kw_min") or 30),
|
|
163
467
|
kw_max=float(request.POST.get("kw_max") or 60),
|
|
@@ -201,6 +505,8 @@ def cp_simulator(request):
|
|
|
201
505
|
"default_host": default_host,
|
|
202
506
|
"default_ws_port": default_ws_port,
|
|
203
507
|
"default_cp_paths": default_cp_paths,
|
|
508
|
+
"default_serial_numbers": default_serial_numbers,
|
|
509
|
+
"default_connector_id": default_connector_id,
|
|
204
510
|
"default_rfid": default_rfid,
|
|
205
511
|
"default_vins": default_vins,
|
|
206
512
|
"params_jsons": params_jsons,
|
|
@@ -209,46 +515,218 @@ def cp_simulator(request):
|
|
|
209
515
|
return render(request, "ocpp/cp_simulator.html", context)
|
|
210
516
|
|
|
211
517
|
|
|
212
|
-
def charger_page(request, cid):
|
|
518
|
+
def charger_page(request, cid, connector=None):
|
|
213
519
|
"""Public landing page for a charger displaying usage guidance or progress."""
|
|
214
|
-
charger =
|
|
215
|
-
|
|
216
|
-
|
|
520
|
+
charger, connector_slug = _get_charger(cid, connector)
|
|
521
|
+
overview = _connector_overview(charger)
|
|
522
|
+
sessions = _live_sessions(charger)
|
|
523
|
+
tx = None
|
|
524
|
+
active_connector_count = 0
|
|
525
|
+
if charger.connector_id is None:
|
|
526
|
+
if sessions:
|
|
527
|
+
total_kw = 0.0
|
|
528
|
+
start_times = [
|
|
529
|
+
tx_obj.start_time for _, tx_obj in sessions if tx_obj.start_time
|
|
530
|
+
]
|
|
531
|
+
for _, tx_obj in sessions:
|
|
532
|
+
if tx_obj.kw:
|
|
533
|
+
total_kw += tx_obj.kw
|
|
534
|
+
tx = SimpleNamespace(
|
|
535
|
+
kw=total_kw, start_time=min(start_times) if start_times else None
|
|
536
|
+
)
|
|
537
|
+
active_connector_count = len(sessions)
|
|
538
|
+
else:
|
|
539
|
+
tx = (
|
|
540
|
+
sessions[0][1]
|
|
541
|
+
if sessions
|
|
542
|
+
else store.get_transaction(cid, charger.connector_id)
|
|
543
|
+
)
|
|
544
|
+
if tx:
|
|
545
|
+
active_connector_count = 1
|
|
546
|
+
state_source = tx if charger.connector_id is not None else (sessions if sessions else None)
|
|
547
|
+
state, color = _charger_state(charger, state_source)
|
|
548
|
+
language_cookie = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
|
|
549
|
+
preferred_language = "es"
|
|
550
|
+
supported_languages = {code for code, _ in settings.LANGUAGES}
|
|
551
|
+
if preferred_language in supported_languages and not language_cookie:
|
|
552
|
+
translation.activate(preferred_language)
|
|
553
|
+
request.LANGUAGE_CODE = translation.get_language()
|
|
554
|
+
connector_links = [
|
|
555
|
+
{
|
|
556
|
+
"slug": item["slug"],
|
|
557
|
+
"label": item["label"],
|
|
558
|
+
"url": item["url"],
|
|
559
|
+
"active": item["slug"] == connector_slug,
|
|
560
|
+
}
|
|
561
|
+
for item in overview
|
|
562
|
+
]
|
|
563
|
+
connector_overview = [
|
|
564
|
+
item for item in overview if item["charger"].connector_id is not None
|
|
565
|
+
]
|
|
566
|
+
status_url = _reverse_connector_url("charger-status", cid, connector_slug)
|
|
567
|
+
return render(
|
|
568
|
+
request,
|
|
569
|
+
"ocpp/charger_page.html",
|
|
570
|
+
{
|
|
571
|
+
"charger": charger,
|
|
572
|
+
"tx": tx,
|
|
573
|
+
"connector_slug": connector_slug,
|
|
574
|
+
"connector_links": connector_links,
|
|
575
|
+
"connector_overview": connector_overview,
|
|
576
|
+
"active_connector_count": active_connector_count,
|
|
577
|
+
"status_url": status_url,
|
|
578
|
+
"landing_translations": _landing_page_translations(),
|
|
579
|
+
"state": state,
|
|
580
|
+
"color": color,
|
|
581
|
+
},
|
|
582
|
+
)
|
|
217
583
|
|
|
218
584
|
|
|
219
585
|
@login_required
|
|
220
|
-
def charger_status(request, cid):
|
|
221
|
-
charger =
|
|
586
|
+
def charger_status(request, cid, connector=None):
|
|
587
|
+
charger, connector_slug = _get_charger(cid, connector)
|
|
222
588
|
session_id = request.GET.get("session")
|
|
223
|
-
|
|
589
|
+
sessions = _live_sessions(charger)
|
|
590
|
+
live_tx = None
|
|
591
|
+
if charger.connector_id is not None and sessions:
|
|
592
|
+
live_tx = sessions[0][1]
|
|
224
593
|
tx_obj = live_tx
|
|
225
594
|
past_session = False
|
|
226
595
|
if session_id:
|
|
227
|
-
if
|
|
596
|
+
if charger.connector_id is None:
|
|
597
|
+
tx_obj = get_object_or_404(
|
|
598
|
+
Transaction, pk=session_id, charger__charger_id=cid
|
|
599
|
+
)
|
|
600
|
+
past_session = True
|
|
601
|
+
elif not (live_tx and str(live_tx.pk) == session_id):
|
|
228
602
|
tx_obj = get_object_or_404(Transaction, pk=session_id, charger=charger)
|
|
229
603
|
past_session = True
|
|
230
|
-
state, color = _charger_state(
|
|
231
|
-
|
|
604
|
+
state, color = _charger_state(
|
|
605
|
+
charger,
|
|
606
|
+
(
|
|
607
|
+
live_tx
|
|
608
|
+
if charger.connector_id is not None
|
|
609
|
+
else (sessions if sessions else None)
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
if charger.connector_id is None:
|
|
613
|
+
transactions_qs = (
|
|
614
|
+
Transaction.objects.filter(charger__charger_id=cid)
|
|
615
|
+
.select_related("charger")
|
|
616
|
+
.order_by("-start_time")
|
|
617
|
+
)
|
|
618
|
+
else:
|
|
619
|
+
transactions_qs = Transaction.objects.filter(charger=charger).order_by(
|
|
620
|
+
"-start_time"
|
|
621
|
+
)
|
|
232
622
|
paginator = Paginator(transactions_qs, 10)
|
|
233
623
|
page_obj = paginator.get_page(request.GET.get("page"))
|
|
234
624
|
transactions = page_obj.object_list
|
|
235
|
-
chart_data = {"labels": [], "
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
625
|
+
chart_data = {"labels": [], "datasets": []}
|
|
626
|
+
|
|
627
|
+
def _series_from_transaction(tx):
|
|
628
|
+
points: list[tuple[str, float]] = []
|
|
629
|
+
readings = list(
|
|
630
|
+
tx.meter_values.filter(energy__isnull=False).order_by("timestamp")
|
|
631
|
+
)
|
|
632
|
+
start_val = None
|
|
633
|
+
if tx.meter_start is not None:
|
|
634
|
+
start_val = float(tx.meter_start) / 1000.0
|
|
241
635
|
for reading in readings:
|
|
242
636
|
try:
|
|
243
|
-
val = float(reading.
|
|
637
|
+
val = float(reading.energy)
|
|
244
638
|
except (TypeError, ValueError):
|
|
245
639
|
continue
|
|
246
|
-
if
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
640
|
+
if start_val is None:
|
|
641
|
+
start_val = val
|
|
642
|
+
total = val - start_val
|
|
643
|
+
points.append((reading.timestamp.isoformat(), max(total, 0.0)))
|
|
644
|
+
return points
|
|
645
|
+
|
|
646
|
+
if tx_obj and (charger.connector_id is not None or past_session):
|
|
647
|
+
series_points = _series_from_transaction(tx_obj)
|
|
648
|
+
if series_points:
|
|
649
|
+
chart_data["labels"] = [ts for ts, _ in series_points]
|
|
650
|
+
connector_id = None
|
|
651
|
+
if tx_obj.charger and tx_obj.charger.connector_id is not None:
|
|
652
|
+
connector_id = tx_obj.charger.connector_id
|
|
653
|
+
elif charger.connector_id is not None:
|
|
654
|
+
connector_id = charger.connector_id
|
|
655
|
+
chart_data["datasets"].append(
|
|
656
|
+
{
|
|
657
|
+
"label": str(
|
|
658
|
+
tx_obj.charger.connector_label
|
|
659
|
+
if tx_obj.charger and tx_obj.charger.connector_id is not None
|
|
660
|
+
else charger.connector_label
|
|
661
|
+
),
|
|
662
|
+
"values": [value for _, value in series_points],
|
|
663
|
+
"connector_id": connector_id,
|
|
664
|
+
}
|
|
665
|
+
)
|
|
666
|
+
elif charger.connector_id is None:
|
|
667
|
+
dataset_points: list[tuple[str, list[tuple[str, float]], int]] = []
|
|
668
|
+
for sibling, sibling_tx in sessions:
|
|
669
|
+
if sibling.connector_id is None or not sibling_tx:
|
|
670
|
+
continue
|
|
671
|
+
points = _series_from_transaction(sibling_tx)
|
|
672
|
+
if not points:
|
|
673
|
+
continue
|
|
674
|
+
dataset_points.append(
|
|
675
|
+
(str(sibling.connector_label), points, sibling.connector_id)
|
|
676
|
+
)
|
|
677
|
+
if dataset_points:
|
|
678
|
+
all_labels: list[str] = sorted(
|
|
679
|
+
{ts for _, points, _ in dataset_points for ts, _ in points}
|
|
680
|
+
)
|
|
681
|
+
chart_data["labels"] = all_labels
|
|
682
|
+
for label, points, connector_id in dataset_points:
|
|
683
|
+
value_map = {ts: val for ts, val in points}
|
|
684
|
+
chart_data["datasets"].append(
|
|
685
|
+
{
|
|
686
|
+
"label": label,
|
|
687
|
+
"values": [value_map.get(ts) for ts in all_labels],
|
|
688
|
+
"connector_id": connector_id,
|
|
689
|
+
}
|
|
690
|
+
)
|
|
691
|
+
overview = _connector_overview(charger)
|
|
692
|
+
connector_links = [
|
|
693
|
+
{
|
|
694
|
+
"slug": item["slug"],
|
|
695
|
+
"label": item["label"],
|
|
696
|
+
"url": _reverse_connector_url("charger-status", cid, item["slug"]),
|
|
697
|
+
"active": item["slug"] == connector_slug,
|
|
698
|
+
}
|
|
699
|
+
for item in overview
|
|
700
|
+
]
|
|
701
|
+
connector_overview = [
|
|
702
|
+
item for item in overview if item["charger"].connector_id is not None
|
|
703
|
+
]
|
|
704
|
+
search_url = _reverse_connector_url("charger-session-search", cid, connector_slug)
|
|
705
|
+
configuration_url = None
|
|
706
|
+
if request.user.is_staff:
|
|
707
|
+
try:
|
|
708
|
+
configuration_url = reverse("admin:ocpp_charger_change", args=[charger.pk])
|
|
709
|
+
except NoReverseMatch: # pragma: no cover - admin may be disabled
|
|
710
|
+
configuration_url = None
|
|
711
|
+
is_connected = store.is_connected(cid, charger.connector_id)
|
|
712
|
+
has_active_session = bool(
|
|
713
|
+
live_tx if charger.connector_id is not None else sessions
|
|
714
|
+
)
|
|
715
|
+
can_remote_start = (
|
|
716
|
+
charger.connector_id is not None
|
|
717
|
+
and is_connected
|
|
718
|
+
and not has_active_session
|
|
719
|
+
and not past_session
|
|
720
|
+
)
|
|
721
|
+
remote_start_messages = None
|
|
722
|
+
if can_remote_start:
|
|
723
|
+
remote_start_messages = {
|
|
724
|
+
"required": str(_("RFID is required to start a session.")),
|
|
725
|
+
"sending": str(_("Sending remote start request...")),
|
|
726
|
+
"success": str(_("Remote start command queued.")),
|
|
727
|
+
"error": str(_("Unable to send remote start request.")),
|
|
728
|
+
}
|
|
729
|
+
action_url = _reverse_connector_url("charger-action", cid, connector_slug)
|
|
252
730
|
return render(
|
|
253
731
|
request,
|
|
254
732
|
"ocpp/charger_status.html",
|
|
@@ -259,79 +737,122 @@ def charger_status(request, cid):
|
|
|
259
737
|
"color": color,
|
|
260
738
|
"transactions": transactions,
|
|
261
739
|
"page_obj": page_obj,
|
|
262
|
-
"chart_data":
|
|
740
|
+
"chart_data": chart_data,
|
|
263
741
|
"past_session": past_session,
|
|
742
|
+
"connector_slug": connector_slug,
|
|
743
|
+
"connector_links": connector_links,
|
|
744
|
+
"connector_overview": connector_overview,
|
|
745
|
+
"search_url": search_url,
|
|
746
|
+
"configuration_url": configuration_url,
|
|
747
|
+
"page_url": _reverse_connector_url("charger-page", cid, connector_slug),
|
|
748
|
+
"is_connected": is_connected,
|
|
749
|
+
"is_idle": is_connected and not has_active_session,
|
|
750
|
+
"can_remote_start": can_remote_start,
|
|
751
|
+
"remote_start_messages": remote_start_messages,
|
|
752
|
+
"action_url": action_url,
|
|
753
|
+
"show_chart": bool(
|
|
754
|
+
chart_data["datasets"]
|
|
755
|
+
and any(
|
|
756
|
+
any(value is not None for value in dataset["values"])
|
|
757
|
+
for dataset in chart_data["datasets"]
|
|
758
|
+
)
|
|
759
|
+
),
|
|
264
760
|
},
|
|
265
761
|
)
|
|
266
762
|
|
|
267
763
|
|
|
268
764
|
@login_required
|
|
269
|
-
def charger_session_search(request, cid):
|
|
270
|
-
charger =
|
|
765
|
+
def charger_session_search(request, cid, connector=None):
|
|
766
|
+
charger, connector_slug = _get_charger(cid, connector)
|
|
271
767
|
date_str = request.GET.get("date")
|
|
272
768
|
transactions = None
|
|
273
769
|
if date_str:
|
|
274
770
|
try:
|
|
275
771
|
date_obj = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
276
|
-
start = datetime.combine(
|
|
277
|
-
|
|
278
|
-
transactions = (
|
|
279
|
-
Transaction.objects.filter(
|
|
280
|
-
charger=charger, start_time__gte=start, start_time__lt=end
|
|
281
|
-
).order_by("-start_time")
|
|
772
|
+
start = datetime.combine(
|
|
773
|
+
date_obj, datetime.min.time(), tzinfo=dt_timezone.utc
|
|
282
774
|
)
|
|
775
|
+
end = start + timedelta(days=1)
|
|
776
|
+
qs = Transaction.objects.filter(start_time__gte=start, start_time__lt=end)
|
|
777
|
+
if charger.connector_id is None:
|
|
778
|
+
qs = qs.filter(charger__charger_id=cid)
|
|
779
|
+
else:
|
|
780
|
+
qs = qs.filter(charger=charger)
|
|
781
|
+
transactions = qs.order_by("-start_time")
|
|
283
782
|
except ValueError:
|
|
284
783
|
transactions = []
|
|
784
|
+
overview = _connector_overview(charger)
|
|
785
|
+
connector_links = [
|
|
786
|
+
{
|
|
787
|
+
"slug": item["slug"],
|
|
788
|
+
"label": item["label"],
|
|
789
|
+
"url": _reverse_connector_url("charger-session-search", cid, item["slug"]),
|
|
790
|
+
"active": item["slug"] == connector_slug,
|
|
791
|
+
}
|
|
792
|
+
for item in overview
|
|
793
|
+
]
|
|
794
|
+
status_url = _reverse_connector_url("charger-status", cid, connector_slug)
|
|
285
795
|
return render(
|
|
286
796
|
request,
|
|
287
797
|
"ocpp/charger_session_search.html",
|
|
288
|
-
{
|
|
798
|
+
{
|
|
799
|
+
"charger": charger,
|
|
800
|
+
"transactions": transactions,
|
|
801
|
+
"date": date_str,
|
|
802
|
+
"connector_slug": connector_slug,
|
|
803
|
+
"connector_links": connector_links,
|
|
804
|
+
"status_url": status_url,
|
|
805
|
+
},
|
|
289
806
|
)
|
|
290
807
|
|
|
291
808
|
|
|
292
809
|
@login_required
|
|
293
|
-
def charger_log_page(request, cid):
|
|
810
|
+
def charger_log_page(request, cid, connector=None):
|
|
294
811
|
"""Render a simple page with the log for the charger or simulator."""
|
|
295
812
|
log_type = request.GET.get("type", "charger")
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
813
|
+
connector_links = []
|
|
814
|
+
connector_slug = None
|
|
815
|
+
status_url = None
|
|
816
|
+
if log_type == "charger":
|
|
817
|
+
charger, connector_slug = _get_charger(cid, connector)
|
|
818
|
+
log_key = store.identity_key(cid, charger.connector_id)
|
|
819
|
+
overview = _connector_overview(charger)
|
|
820
|
+
connector_links = [
|
|
821
|
+
{
|
|
822
|
+
"slug": item["slug"],
|
|
823
|
+
"label": item["label"],
|
|
824
|
+
"url": _reverse_connector_url("charger-log", cid, item["slug"]),
|
|
825
|
+
"active": item["slug"] == connector_slug,
|
|
826
|
+
}
|
|
827
|
+
for item in overview
|
|
828
|
+
]
|
|
829
|
+
target_id = log_key
|
|
830
|
+
status_url = _reverse_connector_url("charger-status", cid, connector_slug)
|
|
831
|
+
else:
|
|
832
|
+
charger = Charger.objects.filter(charger_id=cid).first() or Charger(
|
|
833
|
+
charger_id=cid
|
|
834
|
+
)
|
|
835
|
+
target_id = cid
|
|
836
|
+
log = store.get_logs(target_id, log_type=log_type)
|
|
301
837
|
return render(
|
|
302
838
|
request,
|
|
303
839
|
"ocpp/charger_logs.html",
|
|
304
|
-
{
|
|
840
|
+
{
|
|
841
|
+
"charger": charger,
|
|
842
|
+
"log": log,
|
|
843
|
+
"log_type": log_type,
|
|
844
|
+
"connector_slug": connector_slug,
|
|
845
|
+
"connector_links": connector_links,
|
|
846
|
+
"status_url": status_url,
|
|
847
|
+
},
|
|
305
848
|
)
|
|
306
849
|
|
|
307
850
|
|
|
308
|
-
@login_required
|
|
309
|
-
@landing("EV Efficiency")
|
|
310
|
-
def efficiency_calculator(request):
|
|
311
|
-
"""Simple EV efficiency calculator."""
|
|
312
|
-
form = {k: v for k, v in (request.POST or request.GET).items() if v not in (None, "")}
|
|
313
|
-
context: dict[str, object] = {"form": form}
|
|
314
|
-
if request.method == "POST":
|
|
315
|
-
try:
|
|
316
|
-
distance = float(request.POST.get("distance"))
|
|
317
|
-
energy = float(request.POST.get("energy"))
|
|
318
|
-
if distance <= 0 or energy <= 0:
|
|
319
|
-
raise ValueError
|
|
320
|
-
except (TypeError, ValueError):
|
|
321
|
-
context["error"] = _("Invalid input values")
|
|
322
|
-
else:
|
|
323
|
-
km_per_kwh = distance / energy
|
|
324
|
-
wh_per_km = (energy * 1000) / distance
|
|
325
|
-
context["result"] = {
|
|
326
|
-
"km_per_kwh": km_per_kwh,
|
|
327
|
-
"wh_per_km": wh_per_km,
|
|
328
|
-
}
|
|
329
|
-
return render(request, "ocpp/efficiency_calculator.html", context)
|
|
330
|
-
|
|
331
851
|
@csrf_exempt
|
|
332
852
|
@api_login_required
|
|
333
|
-
def dispatch_action(request, cid):
|
|
334
|
-
|
|
853
|
+
def dispatch_action(request, cid, connector=None):
|
|
854
|
+
connector_value, _ = _normalize_connector_slug(connector)
|
|
855
|
+
ws = store.get_connection(cid, connector_value)
|
|
335
856
|
if ws is None:
|
|
336
857
|
return JsonResponse({"detail": "no connection"}, status=404)
|
|
337
858
|
try:
|
|
@@ -340,20 +861,52 @@ def dispatch_action(request, cid):
|
|
|
340
861
|
data = {}
|
|
341
862
|
action = data.get("action")
|
|
342
863
|
if action == "remote_stop":
|
|
343
|
-
tx_obj = store.
|
|
864
|
+
tx_obj = store.get_transaction(cid, connector_value)
|
|
344
865
|
if not tx_obj:
|
|
345
866
|
return JsonResponse({"detail": "no transaction"}, status=404)
|
|
346
|
-
msg = json.dumps(
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
867
|
+
msg = json.dumps(
|
|
868
|
+
[
|
|
869
|
+
2,
|
|
870
|
+
str(datetime.utcnow().timestamp()),
|
|
871
|
+
"RemoteStopTransaction",
|
|
872
|
+
{"transactionId": tx_obj.pk},
|
|
873
|
+
]
|
|
874
|
+
)
|
|
875
|
+
asyncio.get_event_loop().create_task(ws.send(msg))
|
|
876
|
+
elif action == "remote_start":
|
|
877
|
+
id_tag = data.get("idTag")
|
|
878
|
+
if not isinstance(id_tag, str) or not id_tag.strip():
|
|
879
|
+
return JsonResponse({"detail": "idTag required"}, status=400)
|
|
880
|
+
id_tag = id_tag.strip()
|
|
881
|
+
payload: dict[str, object] = {"idTag": id_tag}
|
|
882
|
+
connector_id = data.get("connectorId")
|
|
883
|
+
if connector_id in ("", None):
|
|
884
|
+
connector_id = None
|
|
885
|
+
if connector_id is None and connector_value is not None:
|
|
886
|
+
connector_id = connector_value
|
|
887
|
+
if connector_id is not None:
|
|
888
|
+
try:
|
|
889
|
+
payload["connectorId"] = int(connector_id)
|
|
890
|
+
except (TypeError, ValueError):
|
|
891
|
+
payload["connectorId"] = connector_id
|
|
892
|
+
if "chargingProfile" in data and data["chargingProfile"] is not None:
|
|
893
|
+
payload["chargingProfile"] = data["chargingProfile"]
|
|
894
|
+
msg = json.dumps(
|
|
895
|
+
[
|
|
896
|
+
2,
|
|
897
|
+
str(datetime.utcnow().timestamp()),
|
|
898
|
+
"RemoteStartTransaction",
|
|
899
|
+
payload,
|
|
900
|
+
]
|
|
901
|
+
)
|
|
352
902
|
asyncio.get_event_loop().create_task(ws.send(msg))
|
|
353
903
|
elif action == "reset":
|
|
354
|
-
msg = json.dumps(
|
|
904
|
+
msg = json.dumps(
|
|
905
|
+
[2, str(datetime.utcnow().timestamp()), "Reset", {"type": "Soft"}]
|
|
906
|
+
)
|
|
355
907
|
asyncio.get_event_loop().create_task(ws.send(msg))
|
|
356
908
|
else:
|
|
357
909
|
return JsonResponse({"detail": "unknown action"}, status=400)
|
|
358
|
-
store.
|
|
910
|
+
log_key = store.identity_key(cid, connector_value)
|
|
911
|
+
store.add_log(log_key, f"< {msg}", log_type="charger")
|
|
359
912
|
return JsonResponse({"sent": msg})
|