arthexis 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- arthexis-0.1.3.dist-info/METADATA +126 -0
- arthexis-0.1.3.dist-info/RECORD +73 -0
- arthexis-0.1.3.dist-info/WHEEL +5 -0
- arthexis-0.1.3.dist-info/licenses/LICENSE +21 -0
- arthexis-0.1.3.dist-info/top_level.txt +5 -0
- config/__init__.py +6 -0
- config/active_app.py +15 -0
- config/asgi.py +29 -0
- config/auth_app.py +8 -0
- config/celery.py +19 -0
- config/context_processors.py +68 -0
- config/loadenv.py +11 -0
- config/logging.py +43 -0
- config/middleware.py +25 -0
- config/offline.py +47 -0
- config/settings.py +374 -0
- config/urls.py +91 -0
- config/wsgi.py +17 -0
- core/__init__.py +0 -0
- core/admin.py +830 -0
- core/apps.py +67 -0
- core/backends.py +82 -0
- core/entity.py +97 -0
- core/environment.py +43 -0
- core/fields.py +70 -0
- core/lcd_screen.py +77 -0
- core/middleware.py +34 -0
- core/models.py +1277 -0
- core/notifications.py +95 -0
- core/release.py +451 -0
- core/system.py +111 -0
- core/tasks.py +100 -0
- core/tests.py +483 -0
- core/urls.py +11 -0
- core/user_data.py +333 -0
- core/views.py +431 -0
- nodes/__init__.py +0 -0
- nodes/actions.py +72 -0
- nodes/admin.py +347 -0
- nodes/apps.py +76 -0
- nodes/lcd.py +151 -0
- nodes/models.py +577 -0
- nodes/tasks.py +50 -0
- nodes/tests.py +1072 -0
- nodes/urls.py +13 -0
- nodes/utils.py +62 -0
- nodes/views.py +262 -0
- ocpp/__init__.py +0 -0
- ocpp/admin.py +392 -0
- ocpp/apps.py +24 -0
- ocpp/consumers.py +267 -0
- ocpp/evcs.py +911 -0
- ocpp/models.py +300 -0
- ocpp/routing.py +9 -0
- ocpp/simulator.py +357 -0
- ocpp/store.py +175 -0
- ocpp/tasks.py +27 -0
- ocpp/test_export_import.py +129 -0
- ocpp/test_rfid.py +345 -0
- ocpp/tests.py +1229 -0
- ocpp/transactions_io.py +119 -0
- ocpp/urls.py +17 -0
- ocpp/views.py +359 -0
- pages/__init__.py +0 -0
- pages/admin.py +231 -0
- pages/apps.py +10 -0
- pages/checks.py +41 -0
- pages/context_processors.py +72 -0
- pages/models.py +224 -0
- pages/tests.py +628 -0
- pages/urls.py +17 -0
- pages/utils.py +13 -0
- pages/views.py +191 -0
ocpp/transactions_io.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from django.utils import timezone
|
|
7
|
+
from django.utils.dateparse import parse_datetime
|
|
8
|
+
|
|
9
|
+
from .models import Charger, Transaction, MeterReading
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def export_transactions(
|
|
13
|
+
start: datetime | None = None,
|
|
14
|
+
end: datetime | None = None,
|
|
15
|
+
chargers: Iterable[str] | None = None,
|
|
16
|
+
) -> dict:
|
|
17
|
+
"""Return transaction export data."""
|
|
18
|
+
qs = (
|
|
19
|
+
Transaction.objects.all()
|
|
20
|
+
.select_related("charger")
|
|
21
|
+
.prefetch_related("meter_readings")
|
|
22
|
+
)
|
|
23
|
+
if start:
|
|
24
|
+
qs = qs.filter(start_time__gte=start)
|
|
25
|
+
if end:
|
|
26
|
+
qs = qs.filter(start_time__lte=end)
|
|
27
|
+
if chargers:
|
|
28
|
+
qs = qs.filter(charger__charger_id__in=chargers)
|
|
29
|
+
|
|
30
|
+
export_chargers = set(qs.values_list("charger__charger_id", flat=True))
|
|
31
|
+
data = {"chargers": [], "transactions": []}
|
|
32
|
+
|
|
33
|
+
for charger in Charger.objects.filter(charger_id__in=export_chargers):
|
|
34
|
+
data["chargers"].append(
|
|
35
|
+
{
|
|
36
|
+
"charger_id": charger.charger_id,
|
|
37
|
+
"connector_id": charger.connector_id,
|
|
38
|
+
"require_rfid": charger.require_rfid,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
for tx in qs:
|
|
43
|
+
data["transactions"].append(
|
|
44
|
+
{
|
|
45
|
+
"charger": tx.charger.charger_id if tx.charger else None,
|
|
46
|
+
"account": tx.account_id,
|
|
47
|
+
"rfid": tx.rfid,
|
|
48
|
+
"vin": tx.vin,
|
|
49
|
+
"meter_start": tx.meter_start,
|
|
50
|
+
"meter_stop": tx.meter_stop,
|
|
51
|
+
"start_time": tx.start_time.isoformat(),
|
|
52
|
+
"stop_time": tx.stop_time.isoformat() if tx.stop_time else None,
|
|
53
|
+
"meter_readings": [
|
|
54
|
+
{
|
|
55
|
+
"connector_id": mr.connector_id,
|
|
56
|
+
"timestamp": mr.timestamp.isoformat(),
|
|
57
|
+
"measurand": mr.measurand,
|
|
58
|
+
"value": str(mr.value),
|
|
59
|
+
"unit": mr.unit,
|
|
60
|
+
}
|
|
61
|
+
for mr in tx.meter_readings.all()
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
return data
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_dt(value: str | None) -> datetime | None:
|
|
69
|
+
if value is None:
|
|
70
|
+
return None
|
|
71
|
+
dt = parse_datetime(value)
|
|
72
|
+
if dt is None:
|
|
73
|
+
raise ValueError(f"Invalid datetime: {value}")
|
|
74
|
+
if timezone.is_naive(dt):
|
|
75
|
+
dt = timezone.make_aware(dt)
|
|
76
|
+
return dt
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def import_transactions(data: dict) -> int:
|
|
80
|
+
"""Import transactions from export data.
|
|
81
|
+
|
|
82
|
+
Returns number of imported transactions.
|
|
83
|
+
"""
|
|
84
|
+
charger_map: dict[str, Charger] = {}
|
|
85
|
+
for item in data.get("chargers", []):
|
|
86
|
+
charger, _ = Charger.objects.get_or_create(
|
|
87
|
+
charger_id=item["charger_id"],
|
|
88
|
+
defaults={
|
|
89
|
+
"connector_id": item.get("connector_id", None),
|
|
90
|
+
"require_rfid": item.get("require_rfid", False),
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
charger_map[item["charger_id"]] = charger
|
|
94
|
+
|
|
95
|
+
imported = 0
|
|
96
|
+
for tx in data.get("transactions", []):
|
|
97
|
+
charger = charger_map.get(tx.get("charger"))
|
|
98
|
+
transaction = Transaction.objects.create(
|
|
99
|
+
charger=charger,
|
|
100
|
+
account_id=tx.get("account"),
|
|
101
|
+
rfid=tx.get("rfid", ""),
|
|
102
|
+
vin=tx.get("vin", ""),
|
|
103
|
+
meter_start=tx.get("meter_start"),
|
|
104
|
+
meter_stop=tx.get("meter_stop"),
|
|
105
|
+
start_time=_parse_dt(tx.get("start_time")),
|
|
106
|
+
stop_time=_parse_dt(tx.get("stop_time")),
|
|
107
|
+
)
|
|
108
|
+
for mr in tx.get("meter_readings", []):
|
|
109
|
+
MeterReading.objects.create(
|
|
110
|
+
charger=charger,
|
|
111
|
+
transaction=transaction,
|
|
112
|
+
connector_id=mr.get("connector_id"),
|
|
113
|
+
timestamp=_parse_dt(mr.get("timestamp")),
|
|
114
|
+
measurand=mr.get("measurand", ""),
|
|
115
|
+
value=mr.get("value"),
|
|
116
|
+
unit=mr.get("unit", ""),
|
|
117
|
+
)
|
|
118
|
+
imported += 1
|
|
119
|
+
return imported
|
ocpp/urls.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from django.urls import include, path
|
|
2
|
+
|
|
3
|
+
from . import views
|
|
4
|
+
|
|
5
|
+
urlpatterns = [
|
|
6
|
+
path("", views.dashboard, name="ocpp-dashboard"),
|
|
7
|
+
path("simulator/", views.cp_simulator, name="cp-simulator"),
|
|
8
|
+
path("chargers/", views.charger_list, name="charger-list"),
|
|
9
|
+
path("chargers/<str:cid>/", views.charger_detail, name="charger-detail"),
|
|
10
|
+
path("chargers/<str:cid>/action/", views.dispatch_action, name="charger-action"),
|
|
11
|
+
path("c/<str:cid>/", views.charger_page, name="charger-page"),
|
|
12
|
+
path("c/<str:cid>/sessions/", views.charger_session_search, name="charger-session-search"),
|
|
13
|
+
path("log/<str:cid>/", views.charger_log_page, name="charger-log"),
|
|
14
|
+
path("c/<str:cid>/status/", views.charger_status, name="charger-status"),
|
|
15
|
+
path("rfid/", include("ocpp.rfid.urls")),
|
|
16
|
+
path("efficiency/", views.efficiency_calculator, name="ev-efficiency"),
|
|
17
|
+
]
|
ocpp/views.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime, timedelta, timezone as dt_timezone
|
|
4
|
+
|
|
5
|
+
from django.http import JsonResponse, HttpResponse
|
|
6
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
7
|
+
from django.shortcuts import render, get_object_or_404
|
|
8
|
+
from django.core.paginator import Paginator
|
|
9
|
+
from django.contrib.auth.decorators import login_required
|
|
10
|
+
from django.utils.translation import gettext_lazy as _
|
|
11
|
+
|
|
12
|
+
from utils.api import api_login_required
|
|
13
|
+
|
|
14
|
+
from pages.utils import landing
|
|
15
|
+
|
|
16
|
+
from . import store
|
|
17
|
+
from .models import Transaction, Charger
|
|
18
|
+
from .evcs import (
|
|
19
|
+
_start_simulator,
|
|
20
|
+
_stop_simulator,
|
|
21
|
+
get_simulator_state,
|
|
22
|
+
_simulator_status_json,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _charger_state(charger: Charger, tx_obj: Transaction | None):
|
|
27
|
+
"""Return human readable state and color for a charger."""
|
|
28
|
+
cid = charger.charger_id
|
|
29
|
+
connected = cid in store.connections
|
|
30
|
+
if connected and tx_obj:
|
|
31
|
+
return "Charging", "green"
|
|
32
|
+
if connected:
|
|
33
|
+
return "Available", "blue"
|
|
34
|
+
return "Offline", "grey"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@api_login_required
|
|
39
|
+
def charger_list(request):
|
|
40
|
+
"""Return a JSON list of known chargers and state."""
|
|
41
|
+
data = []
|
|
42
|
+
for charger in Charger.objects.all():
|
|
43
|
+
cid = charger.charger_id
|
|
44
|
+
tx_obj = store.transactions.get(cid)
|
|
45
|
+
if not tx_obj:
|
|
46
|
+
tx_obj = (
|
|
47
|
+
Transaction.objects.filter(charger__charger_id=cid)
|
|
48
|
+
.order_by("-start_time")
|
|
49
|
+
.first()
|
|
50
|
+
)
|
|
51
|
+
tx_data = None
|
|
52
|
+
if tx_obj:
|
|
53
|
+
tx_data = {
|
|
54
|
+
"transactionId": tx_obj.pk,
|
|
55
|
+
"meterStart": tx_obj.meter_start,
|
|
56
|
+
"startTime": tx_obj.start_time.isoformat(),
|
|
57
|
+
}
|
|
58
|
+
if tx_obj.vin:
|
|
59
|
+
tx_data["vin"] = tx_obj.vin
|
|
60
|
+
if tx_obj.meter_stop is not None:
|
|
61
|
+
tx_data["meterStop"] = tx_obj.meter_stop
|
|
62
|
+
if tx_obj.stop_time is not None:
|
|
63
|
+
tx_data["stopTime"] = tx_obj.stop_time.isoformat()
|
|
64
|
+
data.append(
|
|
65
|
+
{
|
|
66
|
+
"charger_id": cid,
|
|
67
|
+
"name": charger.name,
|
|
68
|
+
"require_rfid": charger.require_rfid,
|
|
69
|
+
"transaction": tx_data,
|
|
70
|
+
"lastHeartbeat": charger.last_heartbeat.isoformat() if charger.last_heartbeat else None,
|
|
71
|
+
"lastMeterValues": charger.last_meter_values,
|
|
72
|
+
"connected": cid in store.connections,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
return JsonResponse({"chargers": data})
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@api_login_required
|
|
79
|
+
def charger_detail(request, cid):
|
|
80
|
+
charger = Charger.objects.filter(charger_id=cid).first()
|
|
81
|
+
if charger is None:
|
|
82
|
+
return JsonResponse({"detail": "not found"}, status=404)
|
|
83
|
+
|
|
84
|
+
tx_obj = store.transactions.get(cid)
|
|
85
|
+
if not tx_obj:
|
|
86
|
+
tx_obj = (
|
|
87
|
+
Transaction.objects.filter(charger__charger_id=cid)
|
|
88
|
+
.order_by("-start_time")
|
|
89
|
+
.first()
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
tx_data = None
|
|
93
|
+
if tx_obj:
|
|
94
|
+
tx_data = {
|
|
95
|
+
"transactionId": tx_obj.pk,
|
|
96
|
+
"meterStart": tx_obj.meter_start,
|
|
97
|
+
"startTime": tx_obj.start_time.isoformat(),
|
|
98
|
+
}
|
|
99
|
+
if tx_obj.vin:
|
|
100
|
+
tx_data["vin"] = tx_obj.vin
|
|
101
|
+
if tx_obj.meter_stop is not None:
|
|
102
|
+
tx_data["meterStop"] = tx_obj.meter_stop
|
|
103
|
+
if tx_obj.stop_time is not None:
|
|
104
|
+
tx_data["stopTime"] = tx_obj.stop_time.isoformat()
|
|
105
|
+
|
|
106
|
+
log = store.get_logs(cid, log_type="charger")
|
|
107
|
+
return JsonResponse(
|
|
108
|
+
{
|
|
109
|
+
"charger_id": cid,
|
|
110
|
+
"name": charger.name,
|
|
111
|
+
"require_rfid": charger.require_rfid,
|
|
112
|
+
"transaction": tx_data,
|
|
113
|
+
"lastHeartbeat": charger.last_heartbeat.isoformat() if charger.last_heartbeat else None,
|
|
114
|
+
"lastMeterValues": charger.last_meter_values,
|
|
115
|
+
"log": log,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@login_required
|
|
121
|
+
@landing("Dashboard")
|
|
122
|
+
def dashboard(request):
|
|
123
|
+
"""Landing page listing all known chargers and their status."""
|
|
124
|
+
chargers = []
|
|
125
|
+
for charger in Charger.objects.all():
|
|
126
|
+
tx_obj = store.transactions.get(charger.charger_id)
|
|
127
|
+
if not tx_obj:
|
|
128
|
+
tx_obj = (
|
|
129
|
+
Transaction.objects.filter(charger=charger)
|
|
130
|
+
.order_by("-start_time")
|
|
131
|
+
.first()
|
|
132
|
+
)
|
|
133
|
+
state, color = _charger_state(charger, tx_obj)
|
|
134
|
+
chargers.append({"charger": charger, "state": state, "color": color})
|
|
135
|
+
return render(request, "ocpp/dashboard.html", {"chargers": chargers})
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@login_required
|
|
139
|
+
@landing("CP Simulator")
|
|
140
|
+
def cp_simulator(request):
|
|
141
|
+
"""Public landing page to control the OCPP charge point simulator."""
|
|
142
|
+
default_host = "127.0.0.1"
|
|
143
|
+
default_ws_port = "9000"
|
|
144
|
+
default_cp_paths = ["CP1", "CP2"]
|
|
145
|
+
default_rfid = "FFFFFFFF"
|
|
146
|
+
default_vins = ["WP0ZZZ00000000000", "WAUZZZ00000000000"]
|
|
147
|
+
|
|
148
|
+
message = ""
|
|
149
|
+
if request.method == "POST":
|
|
150
|
+
cp_idx = int(request.POST.get("cp") or 1)
|
|
151
|
+
action = request.POST.get("action")
|
|
152
|
+
if action == "start":
|
|
153
|
+
sim_params = dict(
|
|
154
|
+
host=request.POST.get("host") or default_host,
|
|
155
|
+
ws_port=int(request.POST.get("ws_port") or default_ws_port),
|
|
156
|
+
cp_path=request.POST.get("cp_path")
|
|
157
|
+
or default_cp_paths[cp_idx - 1],
|
|
158
|
+
rfid=request.POST.get("rfid") or default_rfid,
|
|
159
|
+
vin=request.POST.get("vin") or default_vins[cp_idx - 1],
|
|
160
|
+
duration=int(request.POST.get("duration") or 600),
|
|
161
|
+
interval=float(request.POST.get("interval") or 5),
|
|
162
|
+
kw_min=float(request.POST.get("kw_min") or 30),
|
|
163
|
+
kw_max=float(request.POST.get("kw_max") or 60),
|
|
164
|
+
pre_charge_delay=float(request.POST.get("pre_charge_delay") or 0),
|
|
165
|
+
repeat=request.POST.get("repeat") or False,
|
|
166
|
+
daemon=True,
|
|
167
|
+
username=request.POST.get("username") or None,
|
|
168
|
+
password=request.POST.get("password") or None,
|
|
169
|
+
)
|
|
170
|
+
try:
|
|
171
|
+
started, status, log_file = _start_simulator(sim_params, cp=cp_idx)
|
|
172
|
+
if started:
|
|
173
|
+
message = f"CP{cp_idx} started: {status}. Logs: {log_file}"
|
|
174
|
+
else:
|
|
175
|
+
message = f"CP{cp_idx} {status}. Logs: {log_file}"
|
|
176
|
+
except Exception as exc: # pragma: no cover - unexpected
|
|
177
|
+
message = f"Failed to start CP{cp_idx}: {exc}"
|
|
178
|
+
elif action == "stop":
|
|
179
|
+
try:
|
|
180
|
+
_stop_simulator(cp=cp_idx)
|
|
181
|
+
message = f"CP{cp_idx} stop requested."
|
|
182
|
+
except Exception as exc: # pragma: no cover - unexpected
|
|
183
|
+
message = f"Failed to stop CP{cp_idx}: {exc}"
|
|
184
|
+
else:
|
|
185
|
+
message = "Unknown action."
|
|
186
|
+
|
|
187
|
+
states_dict = get_simulator_state()
|
|
188
|
+
state_list = [states_dict[1], states_dict[2]]
|
|
189
|
+
params_jsons = [
|
|
190
|
+
json.dumps(state_list[0].get("params", {}), indent=2),
|
|
191
|
+
json.dumps(state_list[1].get("params", {}), indent=2),
|
|
192
|
+
]
|
|
193
|
+
state_jsons = [
|
|
194
|
+
_simulator_status_json(1),
|
|
195
|
+
_simulator_status_json(2),
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
context = {
|
|
199
|
+
"message": message,
|
|
200
|
+
"states": state_list,
|
|
201
|
+
"default_host": default_host,
|
|
202
|
+
"default_ws_port": default_ws_port,
|
|
203
|
+
"default_cp_paths": default_cp_paths,
|
|
204
|
+
"default_rfid": default_rfid,
|
|
205
|
+
"default_vins": default_vins,
|
|
206
|
+
"params_jsons": params_jsons,
|
|
207
|
+
"state_jsons": state_jsons,
|
|
208
|
+
}
|
|
209
|
+
return render(request, "ocpp/cp_simulator.html", context)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def charger_page(request, cid):
|
|
213
|
+
"""Public landing page for a charger displaying usage guidance or progress."""
|
|
214
|
+
charger = get_object_or_404(Charger, charger_id=cid)
|
|
215
|
+
tx = store.transactions.get(cid)
|
|
216
|
+
return render(request, "ocpp/charger_page.html", {"charger": charger, "tx": tx})
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@login_required
|
|
220
|
+
def charger_status(request, cid):
|
|
221
|
+
charger = get_object_or_404(Charger, charger_id=cid)
|
|
222
|
+
session_id = request.GET.get("session")
|
|
223
|
+
live_tx = store.transactions.get(cid)
|
|
224
|
+
tx_obj = live_tx
|
|
225
|
+
past_session = False
|
|
226
|
+
if session_id:
|
|
227
|
+
if not (live_tx and str(live_tx.pk) == session_id):
|
|
228
|
+
tx_obj = get_object_or_404(Transaction, pk=session_id, charger=charger)
|
|
229
|
+
past_session = True
|
|
230
|
+
state, color = _charger_state(charger, live_tx)
|
|
231
|
+
transactions_qs = Transaction.objects.filter(charger=charger).order_by("-start_time")
|
|
232
|
+
paginator = Paginator(transactions_qs, 10)
|
|
233
|
+
page_obj = paginator.get_page(request.GET.get("page"))
|
|
234
|
+
transactions = page_obj.object_list
|
|
235
|
+
chart_data = {"labels": [], "values": []}
|
|
236
|
+
if tx_obj:
|
|
237
|
+
total = 0.0
|
|
238
|
+
readings = tx_obj.meter_readings.filter(
|
|
239
|
+
measurand__in=["", "Energy.Active.Import.Register"]
|
|
240
|
+
).order_by("timestamp")
|
|
241
|
+
for reading in readings:
|
|
242
|
+
try:
|
|
243
|
+
val = float(reading.value)
|
|
244
|
+
except (TypeError, ValueError):
|
|
245
|
+
continue
|
|
246
|
+
if reading.unit == "kW":
|
|
247
|
+
total += val
|
|
248
|
+
else:
|
|
249
|
+
total += val / 1000.0
|
|
250
|
+
chart_data["labels"].append(reading.timestamp.isoformat())
|
|
251
|
+
chart_data["values"].append(total)
|
|
252
|
+
return render(
|
|
253
|
+
request,
|
|
254
|
+
"ocpp/charger_status.html",
|
|
255
|
+
{
|
|
256
|
+
"charger": charger,
|
|
257
|
+
"tx": tx_obj,
|
|
258
|
+
"state": state,
|
|
259
|
+
"color": color,
|
|
260
|
+
"transactions": transactions,
|
|
261
|
+
"page_obj": page_obj,
|
|
262
|
+
"chart_data": json.dumps(chart_data),
|
|
263
|
+
"past_session": past_session,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@login_required
|
|
269
|
+
def charger_session_search(request, cid):
|
|
270
|
+
charger = get_object_or_404(Charger, charger_id=cid)
|
|
271
|
+
date_str = request.GET.get("date")
|
|
272
|
+
transactions = None
|
|
273
|
+
if date_str:
|
|
274
|
+
try:
|
|
275
|
+
date_obj = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
276
|
+
start = datetime.combine(date_obj, datetime.min.time(), tzinfo=dt_timezone.utc)
|
|
277
|
+
end = start + timedelta(days=1)
|
|
278
|
+
transactions = (
|
|
279
|
+
Transaction.objects.filter(
|
|
280
|
+
charger=charger, start_time__gte=start, start_time__lt=end
|
|
281
|
+
).order_by("-start_time")
|
|
282
|
+
)
|
|
283
|
+
except ValueError:
|
|
284
|
+
transactions = []
|
|
285
|
+
return render(
|
|
286
|
+
request,
|
|
287
|
+
"ocpp/charger_session_search.html",
|
|
288
|
+
{"charger": charger, "transactions": transactions, "date": date_str},
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@login_required
|
|
293
|
+
def charger_log_page(request, cid):
|
|
294
|
+
"""Render a simple page with the log for the charger or simulator."""
|
|
295
|
+
log_type = request.GET.get("type", "charger")
|
|
296
|
+
try:
|
|
297
|
+
charger = Charger.objects.get(charger_id=cid)
|
|
298
|
+
except Charger.DoesNotExist:
|
|
299
|
+
charger = Charger(charger_id=cid)
|
|
300
|
+
log = store.get_logs(cid, log_type=log_type)
|
|
301
|
+
return render(
|
|
302
|
+
request,
|
|
303
|
+
"ocpp/charger_logs.html",
|
|
304
|
+
{"charger": charger, "log": log},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
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
|
+
@csrf_exempt
|
|
332
|
+
@api_login_required
|
|
333
|
+
def dispatch_action(request, cid):
|
|
334
|
+
ws = store.connections.get(cid)
|
|
335
|
+
if ws is None:
|
|
336
|
+
return JsonResponse({"detail": "no connection"}, status=404)
|
|
337
|
+
try:
|
|
338
|
+
data = json.loads(request.body.decode()) if request.body else {}
|
|
339
|
+
except json.JSONDecodeError:
|
|
340
|
+
data = {}
|
|
341
|
+
action = data.get("action")
|
|
342
|
+
if action == "remote_stop":
|
|
343
|
+
tx_obj = store.transactions.get(cid)
|
|
344
|
+
if not tx_obj:
|
|
345
|
+
return JsonResponse({"detail": "no transaction"}, status=404)
|
|
346
|
+
msg = json.dumps([
|
|
347
|
+
2,
|
|
348
|
+
str(datetime.utcnow().timestamp()),
|
|
349
|
+
"RemoteStopTransaction",
|
|
350
|
+
{"transactionId": tx_obj.pk},
|
|
351
|
+
])
|
|
352
|
+
asyncio.get_event_loop().create_task(ws.send(msg))
|
|
353
|
+
elif action == "reset":
|
|
354
|
+
msg = json.dumps([2, str(datetime.utcnow().timestamp()), "Reset", {"type": "Soft"}])
|
|
355
|
+
asyncio.get_event_loop().create_task(ws.send(msg))
|
|
356
|
+
else:
|
|
357
|
+
return JsonResponse({"detail": "unknown action"}, status=400)
|
|
358
|
+
store.add_log(cid, f"< {msg}", log_type="charger")
|
|
359
|
+
return JsonResponse({"sent": msg})
|
pages/__init__.py
ADDED
|
File without changes
|