arthexis 0.1.9__py3-none-any.whl → 0.1.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/METADATA +63 -20
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/RECORD +39 -36
- config/settings.py +221 -23
- config/urls.py +6 -0
- core/admin.py +401 -35
- core/apps.py +3 -0
- core/auto_upgrade.py +57 -0
- core/backends.py +77 -3
- core/fields.py +93 -0
- core/models.py +212 -7
- core/reference_utils.py +97 -0
- core/sigil_builder.py +16 -3
- core/system.py +157 -143
- core/tasks.py +151 -8
- core/test_system_info.py +37 -1
- core/tests.py +288 -12
- core/user_data.py +103 -8
- core/views.py +257 -15
- nodes/admin.py +12 -4
- nodes/backends.py +109 -17
- nodes/models.py +205 -2
- nodes/tests.py +370 -1
- nodes/views.py +140 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +252 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +49 -7
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/tests.py +384 -8
- ocpp/views.py +101 -76
- pages/context_processors.py +20 -0
- pages/forms.py +131 -0
- pages/tests.py +434 -13
- pages/urls.py +1 -0
- pages/views.py +334 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
nodes/views.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import base64
|
|
2
|
+
import ipaddress
|
|
3
|
+
import json
|
|
4
|
+
import socket
|
|
3
5
|
|
|
4
6
|
from django.http import JsonResponse
|
|
7
|
+
from django.http.request import split_domain_port
|
|
5
8
|
from django.views.decorators.csrf import csrf_exempt
|
|
6
9
|
from django.shortcuts import get_object_or_404
|
|
7
10
|
from django.conf import settings
|
|
@@ -13,10 +16,83 @@ from utils.api import api_login_required
|
|
|
13
16
|
from cryptography.hazmat.primitives import serialization, hashes
|
|
14
17
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
15
18
|
|
|
16
|
-
from .models import Node, NetMessage, NodeRole
|
|
19
|
+
from .models import Node, NetMessage, NodeRole, node_information_updated
|
|
17
20
|
from .utils import capture_screenshot, save_screenshot
|
|
18
21
|
|
|
19
22
|
|
|
23
|
+
def _get_client_ip(request):
|
|
24
|
+
"""Return the client IP from the request headers."""
|
|
25
|
+
|
|
26
|
+
forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR", "")
|
|
27
|
+
if forwarded_for:
|
|
28
|
+
for value in forwarded_for.split(","):
|
|
29
|
+
candidate = value.strip()
|
|
30
|
+
if candidate:
|
|
31
|
+
return candidate
|
|
32
|
+
return request.META.get("REMOTE_ADDR", "")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_route_address(remote_ip: str, port: int) -> str:
|
|
36
|
+
"""Return the local address used to reach ``remote_ip``."""
|
|
37
|
+
|
|
38
|
+
if not remote_ip:
|
|
39
|
+
return ""
|
|
40
|
+
try:
|
|
41
|
+
parsed = ipaddress.ip_address(remote_ip)
|
|
42
|
+
except ValueError:
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
target_port = int(port)
|
|
47
|
+
except (TypeError, ValueError):
|
|
48
|
+
target_port = 1
|
|
49
|
+
if target_port <= 0 or target_port > 65535:
|
|
50
|
+
target_port = 1
|
|
51
|
+
|
|
52
|
+
family = socket.AF_INET6 if parsed.version == 6 else socket.AF_INET
|
|
53
|
+
try:
|
|
54
|
+
with socket.socket(family, socket.SOCK_DGRAM) as sock:
|
|
55
|
+
if family == socket.AF_INET6:
|
|
56
|
+
sock.connect((remote_ip, target_port, 0, 0))
|
|
57
|
+
else:
|
|
58
|
+
sock.connect((remote_ip, target_port))
|
|
59
|
+
return sock.getsockname()[0]
|
|
60
|
+
except OSError:
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_host_ip(request) -> str:
|
|
65
|
+
"""Return the IP address from the host header if available."""
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
host = request.get_host()
|
|
69
|
+
except Exception: # pragma: no cover - defensive
|
|
70
|
+
return ""
|
|
71
|
+
if not host:
|
|
72
|
+
return ""
|
|
73
|
+
domain, _ = split_domain_port(host)
|
|
74
|
+
if not domain:
|
|
75
|
+
return ""
|
|
76
|
+
try:
|
|
77
|
+
ipaddress.ip_address(domain)
|
|
78
|
+
except ValueError:
|
|
79
|
+
return ""
|
|
80
|
+
return domain
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _get_advertised_address(request, node) -> str:
|
|
84
|
+
"""Return the best address for the client to reach this node."""
|
|
85
|
+
|
|
86
|
+
client_ip = _get_client_ip(request)
|
|
87
|
+
route_address = _get_route_address(client_ip, node.port)
|
|
88
|
+
if route_address:
|
|
89
|
+
return route_address
|
|
90
|
+
host_ip = _get_host_ip(request)
|
|
91
|
+
if host_ip:
|
|
92
|
+
return host_ip
|
|
93
|
+
return node.address
|
|
94
|
+
|
|
95
|
+
|
|
20
96
|
@api_login_required
|
|
21
97
|
def node_list(request):
|
|
22
98
|
"""Return a JSON list of all known nodes."""
|
|
@@ -43,9 +119,10 @@ def node_info(request):
|
|
|
43
119
|
node, _ = Node.register_current()
|
|
44
120
|
|
|
45
121
|
token = request.GET.get("token", "")
|
|
122
|
+
address = _get_advertised_address(request, node)
|
|
46
123
|
data = {
|
|
47
124
|
"hostname": node.hostname,
|
|
48
|
-
"address":
|
|
125
|
+
"address": address,
|
|
49
126
|
"port": node.port,
|
|
50
127
|
"mac_address": node.mac_address,
|
|
51
128
|
"public_key": node.public_key,
|
|
@@ -91,7 +168,6 @@ def _add_cors_headers(request, response):
|
|
|
91
168
|
|
|
92
169
|
|
|
93
170
|
@csrf_exempt
|
|
94
|
-
@api_login_required
|
|
95
171
|
def register_node(request):
|
|
96
172
|
"""Register or update a node from POSTed JSON data."""
|
|
97
173
|
|
|
@@ -126,6 +202,8 @@ def register_node(request):
|
|
|
126
202
|
public_key = data.get("public_key")
|
|
127
203
|
token = data.get("token")
|
|
128
204
|
signature = data.get("signature")
|
|
205
|
+
installed_version = data.get("installed_version")
|
|
206
|
+
installed_revision = data.get("installed_revision")
|
|
129
207
|
|
|
130
208
|
if not hostname or not address or not mac_address:
|
|
131
209
|
response = JsonResponse(
|
|
@@ -148,6 +226,10 @@ def register_node(request):
|
|
|
148
226
|
response = JsonResponse({"detail": "invalid signature"}, status=403)
|
|
149
227
|
return _add_cors_headers(request, response)
|
|
150
228
|
|
|
229
|
+
if not verified and not request.user.is_authenticated:
|
|
230
|
+
response = JsonResponse({"detail": "authentication required"}, status=401)
|
|
231
|
+
return _add_cors_headers(request, response)
|
|
232
|
+
|
|
151
233
|
mac_address = mac_address.lower()
|
|
152
234
|
defaults = {
|
|
153
235
|
"hostname": hostname,
|
|
@@ -156,12 +238,18 @@ def register_node(request):
|
|
|
156
238
|
}
|
|
157
239
|
if verified:
|
|
158
240
|
defaults["public_key"] = public_key
|
|
241
|
+
if installed_version is not None:
|
|
242
|
+
defaults["installed_version"] = str(installed_version)[:20]
|
|
243
|
+
if installed_revision is not None:
|
|
244
|
+
defaults["installed_revision"] = str(installed_revision)[:40]
|
|
159
245
|
|
|
160
246
|
node, created = Node.objects.get_or_create(
|
|
161
247
|
mac_address=mac_address,
|
|
162
248
|
defaults=defaults,
|
|
163
249
|
)
|
|
164
250
|
if not created:
|
|
251
|
+
previous_version = (node.installed_version or "").strip()
|
|
252
|
+
previous_revision = (node.installed_revision or "").strip()
|
|
165
253
|
node.hostname = hostname
|
|
166
254
|
node.address = address
|
|
167
255
|
node.port = port
|
|
@@ -169,8 +257,27 @@ def register_node(request):
|
|
|
169
257
|
if verified:
|
|
170
258
|
node.public_key = public_key
|
|
171
259
|
update_fields.append("public_key")
|
|
260
|
+
if installed_version is not None:
|
|
261
|
+
node.installed_version = str(installed_version)[:20]
|
|
262
|
+
if "installed_version" not in update_fields:
|
|
263
|
+
update_fields.append("installed_version")
|
|
264
|
+
if installed_revision is not None:
|
|
265
|
+
node.installed_revision = str(installed_revision)[:40]
|
|
266
|
+
if "installed_revision" not in update_fields:
|
|
267
|
+
update_fields.append("installed_revision")
|
|
172
268
|
node.save(update_fields=update_fields)
|
|
173
|
-
|
|
269
|
+
current_version = (node.installed_version or "").strip()
|
|
270
|
+
current_revision = (node.installed_revision or "").strip()
|
|
271
|
+
node_information_updated.send(
|
|
272
|
+
sender=Node,
|
|
273
|
+
node=node,
|
|
274
|
+
previous_version=previous_version,
|
|
275
|
+
previous_revision=previous_revision,
|
|
276
|
+
current_version=current_version,
|
|
277
|
+
current_revision=current_revision,
|
|
278
|
+
request=request,
|
|
279
|
+
)
|
|
280
|
+
if features is not None and (verified or request.user.is_authenticated):
|
|
174
281
|
if isinstance(features, (str, bytes)):
|
|
175
282
|
feature_list = [features]
|
|
176
283
|
else:
|
|
@@ -181,13 +288,25 @@ def register_node(request):
|
|
|
181
288
|
)
|
|
182
289
|
return _add_cors_headers(request, response)
|
|
183
290
|
|
|
184
|
-
if features is not None:
|
|
291
|
+
if features is not None and (verified or request.user.is_authenticated):
|
|
185
292
|
if isinstance(features, (str, bytes)):
|
|
186
293
|
feature_list = [features]
|
|
187
294
|
else:
|
|
188
295
|
feature_list = list(features)
|
|
189
296
|
node.update_manual_features(feature_list)
|
|
190
297
|
|
|
298
|
+
current_version = (node.installed_version or "").strip()
|
|
299
|
+
current_revision = (node.installed_revision or "").strip()
|
|
300
|
+
node_information_updated.send(
|
|
301
|
+
sender=Node,
|
|
302
|
+
node=node,
|
|
303
|
+
previous_version="",
|
|
304
|
+
previous_revision="",
|
|
305
|
+
current_version=current_version,
|
|
306
|
+
current_revision=current_revision,
|
|
307
|
+
request=request,
|
|
308
|
+
)
|
|
309
|
+
|
|
191
310
|
response = JsonResponse({"id": node.id})
|
|
192
311
|
return _add_cors_headers(request, response)
|
|
193
312
|
|
|
@@ -277,11 +396,22 @@ def net_message(request):
|
|
|
277
396
|
if reach_name:
|
|
278
397
|
reach_role = NodeRole.objects.filter(name=reach_name).first()
|
|
279
398
|
seen = data.get("seen", [])
|
|
399
|
+
origin_id = data.get("origin")
|
|
400
|
+
origin_node = None
|
|
401
|
+
if origin_id:
|
|
402
|
+
origin_node = Node.objects.filter(uuid=origin_id).first()
|
|
403
|
+
if not origin_node:
|
|
404
|
+
origin_node = node
|
|
280
405
|
if not msg_uuid:
|
|
281
406
|
return JsonResponse({"detail": "uuid required"}, status=400)
|
|
282
407
|
msg, created = NetMessage.objects.get_or_create(
|
|
283
408
|
uuid=msg_uuid,
|
|
284
|
-
defaults={
|
|
409
|
+
defaults={
|
|
410
|
+
"subject": subject[:64],
|
|
411
|
+
"body": body[:256],
|
|
412
|
+
"reach": reach_role,
|
|
413
|
+
"node_origin": origin_node,
|
|
414
|
+
},
|
|
285
415
|
)
|
|
286
416
|
if not created:
|
|
287
417
|
msg.subject = subject[:64]
|
|
@@ -290,6 +420,9 @@ def net_message(request):
|
|
|
290
420
|
if reach_role and msg.reach_id != reach_role.id:
|
|
291
421
|
msg.reach = reach_role
|
|
292
422
|
update_fields.append("reach")
|
|
423
|
+
if msg.node_origin_id is None and origin_node:
|
|
424
|
+
msg.node_origin = origin_node
|
|
425
|
+
update_fields.append("node_origin")
|
|
293
426
|
msg.save(update_fields=update_fields)
|
|
294
427
|
msg.propagate(seen=seen)
|
|
295
428
|
return JsonResponse({"status": "propagated", "complete": msg.complete})
|
ocpp/admin.py
CHANGED
|
@@ -140,7 +140,7 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
140
140
|
),
|
|
141
141
|
(
|
|
142
142
|
"Configuration",
|
|
143
|
-
{"fields": ("require_rfid"
|
|
143
|
+
{"fields": ("public_display", "require_rfid")},
|
|
144
144
|
),
|
|
145
145
|
(
|
|
146
146
|
"References",
|
|
@@ -161,6 +161,7 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
161
161
|
"connector_id",
|
|
162
162
|
"location_name",
|
|
163
163
|
"require_rfid_display",
|
|
164
|
+
"public_display",
|
|
164
165
|
"last_heartbeat",
|
|
165
166
|
"firmware_status",
|
|
166
167
|
"firmware_timestamp",
|
|
@@ -276,18 +277,75 @@ class SimulatorAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
276
277
|
"rfid",
|
|
277
278
|
("duration", "interval", "pre_charge_delay"),
|
|
278
279
|
"kw_max",
|
|
279
|
-
"repeat",
|
|
280
|
+
("repeat", "door_open"),
|
|
280
281
|
("username", "password"),
|
|
281
282
|
)
|
|
282
|
-
actions = ("start_simulator", "stop_simulator")
|
|
283
|
+
actions = ("start_simulator", "stop_simulator", "send_open_door")
|
|
283
284
|
|
|
284
285
|
log_type = "simulator"
|
|
285
286
|
|
|
287
|
+
def save_model(self, request, obj, form, change):
|
|
288
|
+
previous_door_open = False
|
|
289
|
+
if change and obj.pk:
|
|
290
|
+
previous_door_open = (
|
|
291
|
+
type(obj)
|
|
292
|
+
.objects.filter(pk=obj.pk)
|
|
293
|
+
.values_list("door_open", flat=True)
|
|
294
|
+
.first()
|
|
295
|
+
or False
|
|
296
|
+
)
|
|
297
|
+
super().save_model(request, obj, form, change)
|
|
298
|
+
if obj.door_open and not previous_door_open:
|
|
299
|
+
triggered = self._queue_door_open(request, obj)
|
|
300
|
+
if not triggered:
|
|
301
|
+
type(obj).objects.filter(pk=obj.pk).update(door_open=False)
|
|
302
|
+
obj.door_open = False
|
|
303
|
+
|
|
304
|
+
def _queue_door_open(self, request, obj) -> bool:
|
|
305
|
+
sim = store.simulators.get(obj.pk)
|
|
306
|
+
if not sim:
|
|
307
|
+
self.message_user(
|
|
308
|
+
request,
|
|
309
|
+
f"{obj.name}: simulator is not running",
|
|
310
|
+
level=messages.ERROR,
|
|
311
|
+
)
|
|
312
|
+
return False
|
|
313
|
+
type(obj).objects.filter(pk=obj.pk).update(door_open=True)
|
|
314
|
+
obj.door_open = True
|
|
315
|
+
store.add_log(
|
|
316
|
+
obj.cp_path,
|
|
317
|
+
"Door open event requested from admin",
|
|
318
|
+
log_type="simulator",
|
|
319
|
+
)
|
|
320
|
+
if hasattr(sim, "trigger_door_open"):
|
|
321
|
+
sim.trigger_door_open()
|
|
322
|
+
else: # pragma: no cover - unexpected condition
|
|
323
|
+
self.message_user(
|
|
324
|
+
request,
|
|
325
|
+
f"{obj.name}: simulator cannot send door open event",
|
|
326
|
+
level=messages.ERROR,
|
|
327
|
+
)
|
|
328
|
+
type(obj).objects.filter(pk=obj.pk).update(door_open=False)
|
|
329
|
+
obj.door_open = False
|
|
330
|
+
return False
|
|
331
|
+
type(obj).objects.filter(pk=obj.pk).update(door_open=False)
|
|
332
|
+
obj.door_open = False
|
|
333
|
+
self.message_user(
|
|
334
|
+
request,
|
|
335
|
+
f"{obj.name}: DoorOpen status notification sent",
|
|
336
|
+
)
|
|
337
|
+
return True
|
|
338
|
+
|
|
286
339
|
def running(self, obj):
|
|
287
340
|
return obj.pk in store.simulators
|
|
288
341
|
|
|
289
342
|
running.boolean = True
|
|
290
343
|
|
|
344
|
+
@admin.action(description="Send Open Door")
|
|
345
|
+
def send_open_door(self, request, queryset):
|
|
346
|
+
for obj in queryset:
|
|
347
|
+
self._queue_door_open(request, obj)
|
|
348
|
+
|
|
291
349
|
def start_simulator(self, request, queryset):
|
|
292
350
|
from django.urls import reverse
|
|
293
351
|
from django.utils.html import format_html
|
|
@@ -296,6 +354,8 @@ class SimulatorAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
296
354
|
if obj.pk in store.simulators:
|
|
297
355
|
self.message_user(request, f"{obj.name}: already running")
|
|
298
356
|
continue
|
|
357
|
+
type(obj).objects.filter(pk=obj.pk).update(door_open=False)
|
|
358
|
+
obj.door_open = False
|
|
299
359
|
store.register_log_name(obj.cp_path, obj.name, log_type="simulator")
|
|
300
360
|
sim = ChargePointSimulator(obj.as_config())
|
|
301
361
|
started, status, log_file = sim.start()
|