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
nodes/utils.py
CHANGED
|
@@ -14,8 +14,12 @@ SCREENSHOT_DIR = settings.LOG_DIR / "screenshots"
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def capture_screenshot(url: str) -> Path:
|
|
18
|
-
"""Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`.
|
|
17
|
+
def capture_screenshot(url: str, cookies=None) -> Path:
|
|
18
|
+
"""Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`.
|
|
19
|
+
|
|
20
|
+
``cookies`` can be an iterable of Selenium cookie mappings which will be
|
|
21
|
+
applied after the initial navigation and before the screenshot is taken.
|
|
22
|
+
"""
|
|
19
23
|
options = Options()
|
|
20
24
|
options.add_argument("-headless")
|
|
21
25
|
try:
|
|
@@ -27,6 +31,13 @@ def capture_screenshot(url: str) -> Path:
|
|
|
27
31
|
browser.get(url)
|
|
28
32
|
except WebDriverException as exc:
|
|
29
33
|
logger.error("Failed to load %s: %s", url, exc)
|
|
34
|
+
if cookies:
|
|
35
|
+
for cookie in cookies:
|
|
36
|
+
try:
|
|
37
|
+
browser.add_cookie(cookie)
|
|
38
|
+
except WebDriverException as exc:
|
|
39
|
+
logger.error("Failed to apply cookie for %s: %s", url, exc)
|
|
40
|
+
browser.get(url)
|
|
30
41
|
if not browser.save_screenshot(str(filename)):
|
|
31
42
|
raise RuntimeError("Screenshot capture failed")
|
|
32
43
|
return filename
|
nodes/views.py
CHANGED
|
@@ -6,6 +6,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|
|
6
6
|
from django.shortcuts import get_object_or_404
|
|
7
7
|
from django.conf import settings
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from django.utils.cache import patch_vary_headers
|
|
9
10
|
|
|
10
11
|
from utils.api import api_login_required
|
|
11
12
|
|
|
@@ -20,15 +21,16 @@ from .utils import capture_screenshot, save_screenshot
|
|
|
20
21
|
def node_list(request):
|
|
21
22
|
"""Return a JSON list of all known nodes."""
|
|
22
23
|
|
|
23
|
-
nodes =
|
|
24
|
-
|
|
25
|
-
"hostname",
|
|
26
|
-
"address",
|
|
27
|
-
"port",
|
|
28
|
-
"last_seen",
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
nodes = [
|
|
25
|
+
{
|
|
26
|
+
"hostname": node.hostname,
|
|
27
|
+
"address": node.address,
|
|
28
|
+
"port": node.port,
|
|
29
|
+
"last_seen": node.last_seen,
|
|
30
|
+
"features": list(node.features.values_list("slug", flat=True)),
|
|
31
|
+
}
|
|
32
|
+
for node in Node.objects.prefetch_related("features")
|
|
33
|
+
]
|
|
32
34
|
return JsonResponse({"nodes": nodes})
|
|
33
35
|
|
|
34
36
|
|
|
@@ -47,7 +49,7 @@ def node_info(request):
|
|
|
47
49
|
"port": node.port,
|
|
48
50
|
"mac_address": node.mac_address,
|
|
49
51
|
"public_key": node.public_key,
|
|
50
|
-
"
|
|
52
|
+
"features": list(node.features.values_list("slug", flat=True)),
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
if token:
|
|
@@ -74,19 +76,49 @@ def node_info(request):
|
|
|
74
76
|
return response
|
|
75
77
|
|
|
76
78
|
|
|
79
|
+
def _add_cors_headers(request, response):
|
|
80
|
+
origin = request.headers.get("Origin")
|
|
81
|
+
if origin:
|
|
82
|
+
response["Access-Control-Allow-Origin"] = origin
|
|
83
|
+
response["Access-Control-Allow-Credentials"] = "true"
|
|
84
|
+
allow_headers = request.headers.get(
|
|
85
|
+
"Access-Control-Request-Headers", "Content-Type"
|
|
86
|
+
)
|
|
87
|
+
response["Access-Control-Allow-Headers"] = allow_headers
|
|
88
|
+
response["Access-Control-Allow-Methods"] = "POST, OPTIONS"
|
|
89
|
+
patch_vary_headers(response, ["Origin"])
|
|
90
|
+
return response
|
|
91
|
+
|
|
92
|
+
|
|
77
93
|
@csrf_exempt
|
|
78
94
|
@api_login_required
|
|
79
95
|
def register_node(request):
|
|
80
96
|
"""Register or update a node from POSTed JSON data."""
|
|
81
97
|
|
|
98
|
+
if request.method == "OPTIONS":
|
|
99
|
+
response = JsonResponse({"detail": "ok"})
|
|
100
|
+
return _add_cors_headers(request, response)
|
|
101
|
+
|
|
82
102
|
if request.method != "POST":
|
|
83
|
-
|
|
103
|
+
response = JsonResponse({"detail": "POST required"}, status=400)
|
|
104
|
+
return _add_cors_headers(request, response)
|
|
84
105
|
|
|
85
106
|
try:
|
|
86
107
|
data = json.loads(request.body.decode())
|
|
87
108
|
except json.JSONDecodeError:
|
|
88
109
|
data = request.POST
|
|
89
110
|
|
|
111
|
+
if hasattr(data, "getlist"):
|
|
112
|
+
raw_features = data.getlist("features")
|
|
113
|
+
if not raw_features:
|
|
114
|
+
features = None
|
|
115
|
+
elif len(raw_features) == 1:
|
|
116
|
+
features = raw_features[0]
|
|
117
|
+
else:
|
|
118
|
+
features = raw_features
|
|
119
|
+
else:
|
|
120
|
+
features = data.get("features")
|
|
121
|
+
|
|
90
122
|
hostname = data.get("hostname")
|
|
91
123
|
address = data.get("address")
|
|
92
124
|
port = data.get("port", 8000)
|
|
@@ -94,12 +126,12 @@ def register_node(request):
|
|
|
94
126
|
public_key = data.get("public_key")
|
|
95
127
|
token = data.get("token")
|
|
96
128
|
signature = data.get("signature")
|
|
97
|
-
has_lcd_screen = data.get("has_lcd_screen")
|
|
98
129
|
|
|
99
130
|
if not hostname or not address or not mac_address:
|
|
100
|
-
|
|
131
|
+
response = JsonResponse(
|
|
101
132
|
{"detail": "hostname, address and mac_address required"}, status=400
|
|
102
133
|
)
|
|
134
|
+
return _add_cors_headers(request, response)
|
|
103
135
|
|
|
104
136
|
verified = False
|
|
105
137
|
if public_key and token and signature:
|
|
@@ -113,14 +145,14 @@ def register_node(request):
|
|
|
113
145
|
)
|
|
114
146
|
verified = True
|
|
115
147
|
except Exception:
|
|
116
|
-
|
|
148
|
+
response = JsonResponse({"detail": "invalid signature"}, status=403)
|
|
149
|
+
return _add_cors_headers(request, response)
|
|
117
150
|
|
|
118
151
|
mac_address = mac_address.lower()
|
|
119
152
|
defaults = {
|
|
120
153
|
"hostname": hostname,
|
|
121
154
|
"address": address,
|
|
122
155
|
"port": port,
|
|
123
|
-
"has_lcd_screen": bool(has_lcd_screen),
|
|
124
156
|
}
|
|
125
157
|
if verified:
|
|
126
158
|
defaults["public_key"] = public_key
|
|
@@ -137,15 +169,27 @@ def register_node(request):
|
|
|
137
169
|
if verified:
|
|
138
170
|
node.public_key = public_key
|
|
139
171
|
update_fields.append("public_key")
|
|
140
|
-
if has_lcd_screen is not None:
|
|
141
|
-
node.has_lcd_screen = bool(has_lcd_screen)
|
|
142
|
-
update_fields.append("has_lcd_screen")
|
|
143
172
|
node.save(update_fields=update_fields)
|
|
144
|
-
|
|
173
|
+
if features is not None:
|
|
174
|
+
if isinstance(features, (str, bytes)):
|
|
175
|
+
feature_list = [features]
|
|
176
|
+
else:
|
|
177
|
+
feature_list = list(features)
|
|
178
|
+
node.update_manual_features(feature_list)
|
|
179
|
+
response = JsonResponse(
|
|
145
180
|
{"id": node.id, "detail": f"Node already exists (id: {node.id})"}
|
|
146
181
|
)
|
|
182
|
+
return _add_cors_headers(request, response)
|
|
147
183
|
|
|
148
|
-
|
|
184
|
+
if features is not None:
|
|
185
|
+
if isinstance(features, (str, bytes)):
|
|
186
|
+
feature_list = [features]
|
|
187
|
+
else:
|
|
188
|
+
feature_list = list(features)
|
|
189
|
+
node.update_manual_features(feature_list)
|
|
190
|
+
|
|
191
|
+
response = JsonResponse({"id": node.id})
|
|
192
|
+
return _add_cors_headers(request, response)
|
|
149
193
|
|
|
150
194
|
|
|
151
195
|
@api_login_required
|
|
@@ -172,9 +216,7 @@ def public_node_endpoint(request, endpoint):
|
|
|
172
216
|
- ``POST`` broadcasts the request body as a :class:`NetMessage`.
|
|
173
217
|
"""
|
|
174
218
|
|
|
175
|
-
node = get_object_or_404(
|
|
176
|
-
Node, public_endpoint=endpoint, enable_public_api=True
|
|
177
|
-
)
|
|
219
|
+
node = get_object_or_404(Node, public_endpoint=endpoint, enable_public_api=True)
|
|
178
220
|
|
|
179
221
|
if request.method == "GET":
|
|
180
222
|
data = {
|
|
@@ -183,6 +225,7 @@ def public_node_endpoint(request, endpoint):
|
|
|
183
225
|
"port": node.port,
|
|
184
226
|
"badge_color": node.badge_color,
|
|
185
227
|
"last_seen": node.last_seen,
|
|
228
|
+
"features": list(node.features.values_list("slug", flat=True)),
|
|
186
229
|
}
|
|
187
230
|
return JsonResponse(data)
|
|
188
231
|
|
ocpp/admin.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from django.contrib import admin
|
|
1
|
+
from django.contrib import admin, messages
|
|
2
2
|
from django import forms
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
5
|
from datetime import timedelta
|
|
6
6
|
import json
|
|
7
7
|
|
|
8
|
+
from django.shortcuts import redirect
|
|
8
9
|
from django.utils import timezone
|
|
9
10
|
from django.urls import path
|
|
10
11
|
from django.http import HttpResponse, HttpResponseRedirect
|
|
@@ -13,10 +14,9 @@ from django.template.response import TemplateResponse
|
|
|
13
14
|
from .models import (
|
|
14
15
|
Charger,
|
|
15
16
|
Simulator,
|
|
16
|
-
|
|
17
|
+
MeterValue,
|
|
17
18
|
Transaction,
|
|
18
19
|
Location,
|
|
19
|
-
ElectricVehicle,
|
|
20
20
|
)
|
|
21
21
|
from .simulator import ChargePointSimulator
|
|
22
22
|
from . import store
|
|
@@ -24,8 +24,7 @@ from .transactions_io import (
|
|
|
24
24
|
export_transactions,
|
|
25
25
|
import_transactions as import_transactions_data,
|
|
26
26
|
)
|
|
27
|
-
from core.
|
|
28
|
-
from .models import RFID
|
|
27
|
+
from core.user_data import EntityModelAdmin
|
|
29
28
|
|
|
30
29
|
|
|
31
30
|
class LocationAdminForm(forms.ModelForm):
|
|
@@ -39,9 +38,7 @@ class LocationAdminForm(forms.ModelForm):
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
class Media:
|
|
42
|
-
css = {
|
|
43
|
-
"all": ("https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",)
|
|
44
|
-
}
|
|
41
|
+
css = {"all": ("https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",)}
|
|
45
42
|
js = (
|
|
46
43
|
"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
|
|
47
44
|
"ocpp/charger_map.js",
|
|
@@ -60,29 +57,91 @@ class TransactionImportForm(forms.Form):
|
|
|
60
57
|
file = forms.FileField()
|
|
61
58
|
|
|
62
59
|
|
|
60
|
+
class LogViewAdminMixin:
|
|
61
|
+
"""Mixin providing an admin view to display charger or simulator logs."""
|
|
62
|
+
|
|
63
|
+
log_type = "charger"
|
|
64
|
+
log_template_name = "admin/ocpp/log_view.html"
|
|
65
|
+
|
|
66
|
+
def get_log_identifier(self, obj): # pragma: no cover - mixin hook
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
def get_log_title(self, obj):
|
|
70
|
+
return f"Log for {obj}"
|
|
71
|
+
|
|
72
|
+
def get_urls(self):
|
|
73
|
+
urls = super().get_urls()
|
|
74
|
+
info = self.model._meta.app_label, self.model._meta.model_name
|
|
75
|
+
custom = [
|
|
76
|
+
path(
|
|
77
|
+
"<path:object_id>/log/",
|
|
78
|
+
self.admin_site.admin_view(self.log_view),
|
|
79
|
+
name=f"{info[0]}_{info[1]}_log",
|
|
80
|
+
),
|
|
81
|
+
]
|
|
82
|
+
return custom + urls
|
|
83
|
+
|
|
84
|
+
def log_view(self, request, object_id):
|
|
85
|
+
obj = self.get_object(request, object_id)
|
|
86
|
+
if obj is None:
|
|
87
|
+
self.message_user(request, "Log is not available.", messages.ERROR)
|
|
88
|
+
return redirect("..")
|
|
89
|
+
identifier = self.get_log_identifier(obj)
|
|
90
|
+
log_entries = store.get_logs(identifier, log_type=self.log_type)
|
|
91
|
+
log_file = store._file_path(identifier, log_type=self.log_type)
|
|
92
|
+
context = {
|
|
93
|
+
**self.admin_site.each_context(request),
|
|
94
|
+
"opts": self.model._meta,
|
|
95
|
+
"original": obj,
|
|
96
|
+
"title": self.get_log_title(obj),
|
|
97
|
+
"log_entries": log_entries,
|
|
98
|
+
"log_file": str(log_file),
|
|
99
|
+
"log_identifier": identifier,
|
|
100
|
+
}
|
|
101
|
+
return TemplateResponse(request, self.log_template_name, context)
|
|
102
|
+
|
|
103
|
+
|
|
63
104
|
@admin.register(Location)
|
|
64
|
-
class LocationAdmin(
|
|
105
|
+
class LocationAdmin(EntityModelAdmin):
|
|
65
106
|
form = LocationAdminForm
|
|
66
107
|
list_display = ("name", "latitude", "longitude")
|
|
108
|
+
change_form_template = "admin/ocpp/location/change_form.html"
|
|
67
109
|
|
|
68
110
|
|
|
69
111
|
@admin.register(Charger)
|
|
70
|
-
class ChargerAdmin(
|
|
112
|
+
class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
71
113
|
fieldsets = (
|
|
72
114
|
(
|
|
73
115
|
"General",
|
|
74
116
|
{
|
|
75
117
|
"fields": (
|
|
76
118
|
"charger_id",
|
|
119
|
+
"display_name",
|
|
77
120
|
"connector_id",
|
|
78
|
-
"
|
|
121
|
+
"location",
|
|
122
|
+
"last_path",
|
|
79
123
|
"last_heartbeat",
|
|
80
124
|
"last_meter_values",
|
|
81
|
-
"
|
|
82
|
-
"
|
|
125
|
+
"firmware_status",
|
|
126
|
+
"firmware_status_info",
|
|
127
|
+
"firmware_timestamp",
|
|
128
|
+
)
|
|
129
|
+
},
|
|
130
|
+
),
|
|
131
|
+
(
|
|
132
|
+
"Diagnostics",
|
|
133
|
+
{
|
|
134
|
+
"fields": (
|
|
135
|
+
"diagnostics_status",
|
|
136
|
+
"diagnostics_timestamp",
|
|
137
|
+
"diagnostics_location",
|
|
83
138
|
)
|
|
84
139
|
},
|
|
85
140
|
),
|
|
141
|
+
(
|
|
142
|
+
"Configuration",
|
|
143
|
+
{"fields": ("require_rfid",)},
|
|
144
|
+
),
|
|
86
145
|
(
|
|
87
146
|
"References",
|
|
88
147
|
{
|
|
@@ -90,13 +149,21 @@ class ChargerAdmin(admin.ModelAdmin):
|
|
|
90
149
|
},
|
|
91
150
|
),
|
|
92
151
|
)
|
|
93
|
-
readonly_fields = (
|
|
152
|
+
readonly_fields = (
|
|
153
|
+
"last_heartbeat",
|
|
154
|
+
"last_meter_values",
|
|
155
|
+
"firmware_status",
|
|
156
|
+
"firmware_status_info",
|
|
157
|
+
"firmware_timestamp",
|
|
158
|
+
)
|
|
94
159
|
list_display = (
|
|
95
160
|
"charger_id",
|
|
96
161
|
"connector_id",
|
|
97
162
|
"location_name",
|
|
98
|
-
"
|
|
163
|
+
"require_rfid_display",
|
|
99
164
|
"last_heartbeat",
|
|
165
|
+
"firmware_status",
|
|
166
|
+
"firmware_timestamp",
|
|
100
167
|
"session_kw",
|
|
101
168
|
"total_kw_display",
|
|
102
169
|
"page_link",
|
|
@@ -109,6 +176,12 @@ class ChargerAdmin(admin.ModelAdmin):
|
|
|
109
176
|
def get_view_on_site_url(self, obj=None):
|
|
110
177
|
return obj.get_absolute_url() if obj else None
|
|
111
178
|
|
|
179
|
+
def require_rfid_display(self, obj):
|
|
180
|
+
return obj.require_rfid
|
|
181
|
+
|
|
182
|
+
require_rfid_display.boolean = True
|
|
183
|
+
require_rfid_display.short_description = "RFID Auth"
|
|
184
|
+
|
|
112
185
|
def page_link(self, obj):
|
|
113
186
|
from django.utils.html import format_html
|
|
114
187
|
|
|
@@ -116,7 +189,7 @@ class ChargerAdmin(admin.ModelAdmin):
|
|
|
116
189
|
'<a href="{}" target="_blank">open</a>', obj.get_absolute_url()
|
|
117
190
|
)
|
|
118
191
|
|
|
119
|
-
page_link.short_description = "Landing
|
|
192
|
+
page_link.short_description = "Landing"
|
|
120
193
|
|
|
121
194
|
def qr_link(self, obj):
|
|
122
195
|
from django.utils.html import format_html
|
|
@@ -133,19 +206,25 @@ class ChargerAdmin(admin.ModelAdmin):
|
|
|
133
206
|
from django.utils.html import format_html
|
|
134
207
|
from django.urls import reverse
|
|
135
208
|
|
|
136
|
-
url = reverse("
|
|
209
|
+
url = reverse("admin:ocpp_charger_log", args=[obj.pk])
|
|
137
210
|
return format_html('<a href="{}" target="_blank">view</a>', url)
|
|
138
211
|
|
|
139
212
|
log_link.short_description = "Log"
|
|
140
|
-
|
|
213
|
+
|
|
214
|
+
def get_log_identifier(self, obj):
|
|
215
|
+
return store.identity_key(obj.charger_id, obj.connector_id)
|
|
216
|
+
|
|
141
217
|
def status_link(self, obj):
|
|
142
218
|
from django.utils.html import format_html
|
|
143
219
|
from django.urls import reverse
|
|
144
220
|
|
|
145
|
-
url = reverse(
|
|
221
|
+
url = reverse(
|
|
222
|
+
"charger-status-connector",
|
|
223
|
+
args=[obj.charger_id, obj.connector_slug],
|
|
224
|
+
)
|
|
146
225
|
return format_html('<a href="{}" target="_blank">status</a>', url)
|
|
147
226
|
|
|
148
|
-
status_link.short_description = "Status
|
|
227
|
+
status_link.short_description = "Status"
|
|
149
228
|
|
|
150
229
|
def location_name(self, obj):
|
|
151
230
|
return obj.location.name if obj.location else ""
|
|
@@ -169,7 +248,7 @@ class ChargerAdmin(admin.ModelAdmin):
|
|
|
169
248
|
total_kw_display.short_description = "Total kW"
|
|
170
249
|
|
|
171
250
|
def session_kw(self, obj):
|
|
172
|
-
tx = store.
|
|
251
|
+
tx = store.get_transaction(obj.charger_id, obj.connector_id)
|
|
173
252
|
if tx:
|
|
174
253
|
return round(tx.kw, 2)
|
|
175
254
|
return 0.0
|
|
@@ -178,7 +257,7 @@ class ChargerAdmin(admin.ModelAdmin):
|
|
|
178
257
|
|
|
179
258
|
|
|
180
259
|
@admin.register(Simulator)
|
|
181
|
-
class SimulatorAdmin(
|
|
260
|
+
class SimulatorAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
182
261
|
list_display = (
|
|
183
262
|
"name",
|
|
184
263
|
"cp_path",
|
|
@@ -202,12 +281,17 @@ class SimulatorAdmin(admin.ModelAdmin):
|
|
|
202
281
|
)
|
|
203
282
|
actions = ("start_simulator", "stop_simulator")
|
|
204
283
|
|
|
284
|
+
log_type = "simulator"
|
|
285
|
+
|
|
205
286
|
def running(self, obj):
|
|
206
287
|
return obj.pk in store.simulators
|
|
207
288
|
|
|
208
289
|
running.boolean = True
|
|
209
290
|
|
|
210
291
|
def start_simulator(self, request, queryset):
|
|
292
|
+
from django.urls import reverse
|
|
293
|
+
from django.utils.html import format_html
|
|
294
|
+
|
|
211
295
|
for obj in queryset:
|
|
212
296
|
if obj.pk in store.simulators:
|
|
213
297
|
self.message_user(request, f"{obj.name}: already running")
|
|
@@ -217,8 +301,16 @@ class SimulatorAdmin(admin.ModelAdmin):
|
|
|
217
301
|
started, status, log_file = sim.start()
|
|
218
302
|
if started:
|
|
219
303
|
store.simulators[obj.pk] = sim
|
|
304
|
+
log_url = reverse("admin:ocpp_simulator_log", args=[obj.pk])
|
|
220
305
|
self.message_user(
|
|
221
|
-
request,
|
|
306
|
+
request,
|
|
307
|
+
format_html(
|
|
308
|
+
'{}: {}. Log: <code>{}</code> (<a href="{}" target="_blank">View Log</a>)',
|
|
309
|
+
obj.name,
|
|
310
|
+
status,
|
|
311
|
+
log_file,
|
|
312
|
+
log_url,
|
|
313
|
+
),
|
|
222
314
|
)
|
|
223
315
|
|
|
224
316
|
start_simulator.short_description = "Start selected simulators"
|
|
@@ -239,22 +331,35 @@ class SimulatorAdmin(admin.ModelAdmin):
|
|
|
239
331
|
from django.utils.html import format_html
|
|
240
332
|
from django.urls import reverse
|
|
241
333
|
|
|
242
|
-
url = reverse("
|
|
334
|
+
url = reverse("admin:ocpp_simulator_log", args=[obj.pk])
|
|
243
335
|
return format_html('<a href="{}" target="_blank">view</a>', url)
|
|
244
336
|
|
|
245
337
|
log_link.short_description = "Log"
|
|
246
338
|
|
|
339
|
+
def get_log_identifier(self, obj):
|
|
340
|
+
return obj.cp_path
|
|
247
341
|
|
|
248
|
-
|
|
249
|
-
|
|
342
|
+
|
|
343
|
+
class MeterValueInline(admin.TabularInline):
|
|
344
|
+
model = MeterValue
|
|
250
345
|
extra = 0
|
|
251
|
-
fields = (
|
|
346
|
+
fields = (
|
|
347
|
+
"timestamp",
|
|
348
|
+
"context",
|
|
349
|
+
"energy",
|
|
350
|
+
"voltage",
|
|
351
|
+
"current_import",
|
|
352
|
+
"current_offered",
|
|
353
|
+
"temperature",
|
|
354
|
+
"soc",
|
|
355
|
+
"connector_id",
|
|
356
|
+
)
|
|
252
357
|
readonly_fields = fields
|
|
253
358
|
can_delete = False
|
|
254
359
|
|
|
255
360
|
|
|
256
361
|
@admin.register(Transaction)
|
|
257
|
-
class TransactionAdmin(
|
|
362
|
+
class TransactionAdmin(EntityModelAdmin):
|
|
258
363
|
change_list_template = "admin/ocpp/transaction/change_list.html"
|
|
259
364
|
list_display = (
|
|
260
365
|
"charger",
|
|
@@ -269,7 +374,7 @@ class TransactionAdmin(admin.ModelAdmin):
|
|
|
269
374
|
readonly_fields = ("kw",)
|
|
270
375
|
list_filter = ("charger", "account")
|
|
271
376
|
date_hierarchy = "start_time"
|
|
272
|
-
inlines = [
|
|
377
|
+
inlines = [MeterValueInline]
|
|
273
378
|
|
|
274
379
|
def get_urls(self):
|
|
275
380
|
urls = super().get_urls()
|
|
@@ -301,17 +406,15 @@ class TransactionAdmin(admin.ModelAdmin):
|
|
|
301
406
|
json.dumps(data, indent=2, ensure_ascii=False),
|
|
302
407
|
content_type="application/json",
|
|
303
408
|
)
|
|
304
|
-
response[
|
|
305
|
-
"
|
|
306
|
-
|
|
409
|
+
response["Content-Disposition"] = (
|
|
410
|
+
"attachment; filename=transactions.json"
|
|
411
|
+
)
|
|
307
412
|
return response
|
|
308
413
|
else:
|
|
309
414
|
form = TransactionExportForm()
|
|
310
415
|
context = self.admin_site.each_context(request)
|
|
311
416
|
context["form"] = form
|
|
312
|
-
return TemplateResponse(
|
|
313
|
-
request, "admin/ocpp/transaction/export.html", context
|
|
314
|
-
)
|
|
417
|
+
return TemplateResponse(request, "admin/ocpp/transaction/export.html", context)
|
|
315
418
|
|
|
316
419
|
def import_view(self, request):
|
|
317
420
|
if request.method == "POST":
|
|
@@ -325,12 +428,10 @@ class TransactionAdmin(admin.ModelAdmin):
|
|
|
325
428
|
form = TransactionImportForm()
|
|
326
429
|
context = self.admin_site.each_context(request)
|
|
327
430
|
context["form"] = form
|
|
328
|
-
return TemplateResponse(
|
|
329
|
-
request, "admin/ocpp/transaction/import.html", context
|
|
330
|
-
)
|
|
431
|
+
return TemplateResponse(request, "admin/ocpp/transaction/import.html", context)
|
|
331
432
|
|
|
332
433
|
|
|
333
|
-
class
|
|
434
|
+
class MeterValueDateFilter(admin.SimpleListFilter):
|
|
334
435
|
title = "Timestamp"
|
|
335
436
|
parameter_name = "timestamp_range"
|
|
336
437
|
|
|
@@ -361,32 +462,20 @@ class MeterReadingDateFilter(admin.SimpleListFilter):
|
|
|
361
462
|
return queryset
|
|
362
463
|
|
|
363
464
|
|
|
364
|
-
@admin.register(
|
|
365
|
-
class
|
|
465
|
+
@admin.register(MeterValue)
|
|
466
|
+
class MeterValueAdmin(EntityModelAdmin):
|
|
366
467
|
list_display = (
|
|
367
468
|
"charger",
|
|
368
469
|
"timestamp",
|
|
369
|
-
"
|
|
370
|
-
"
|
|
470
|
+
"context",
|
|
471
|
+
"energy",
|
|
472
|
+
"voltage",
|
|
473
|
+
"current_import",
|
|
474
|
+
"current_offered",
|
|
475
|
+
"temperature",
|
|
476
|
+
"soc",
|
|
371
477
|
"connector_id",
|
|
372
478
|
"transaction",
|
|
373
479
|
)
|
|
374
480
|
date_hierarchy = "timestamp"
|
|
375
|
-
list_filter = ("charger",
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
@admin.register(ElectricVehicle)
|
|
379
|
-
class ElectricVehicleAdmin(admin.ModelAdmin):
|
|
380
|
-
list_display = ("vin", "license_plate", "brand", "model", "account")
|
|
381
|
-
search_fields = (
|
|
382
|
-
"vin",
|
|
383
|
-
"license_plate",
|
|
384
|
-
"brand__name",
|
|
385
|
-
"model__name",
|
|
386
|
-
"account__name",
|
|
387
|
-
)
|
|
388
|
-
fields = ("account", "vin", "license_plate", "brand", "model")
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
admin.site.register(RFID, RFIDAdmin)
|
|
392
|
-
|
|
481
|
+
list_filter = ("charger", MeterValueDateFilter)
|
ocpp/apps.py
CHANGED
|
@@ -6,7 +6,7 @@ from django.conf import settings
|
|
|
6
6
|
class OcppConfig(AppConfig):
|
|
7
7
|
default_auto_field = "django.db.models.BigAutoField"
|
|
8
8
|
name = "ocpp"
|
|
9
|
-
verbose_name = "3.
|
|
9
|
+
verbose_name = "3. Protocol"
|
|
10
10
|
|
|
11
11
|
def ready(self): # pragma: no cover - startup side effects
|
|
12
12
|
control_lock = Path(settings.BASE_DIR) / "locks" / "control.lck"
|