arthexis 0.1.9__py3-none-any.whl → 0.1.11__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.11.dist-info}/METADATA +76 -23
- arthexis-0.1.11.dist-info/RECORD +99 -0
- config/context_processors.py +1 -0
- config/settings.py +245 -26
- config/urls.py +11 -4
- core/admin.py +585 -57
- core/apps.py +29 -1
- core/auto_upgrade.py +57 -0
- core/backends.py +115 -3
- core/environment.py +23 -5
- core/fields.py +93 -0
- core/mailer.py +3 -1
- core/models.py +482 -38
- core/reference_utils.py +108 -0
- core/sigil_builder.py +23 -5
- core/sigil_resolver.py +35 -4
- core/system.py +400 -140
- core/tasks.py +151 -8
- core/temp_passwords.py +181 -0
- core/test_system_info.py +97 -1
- core/tests.py +393 -15
- core/user_data.py +154 -16
- core/views.py +499 -20
- nodes/admin.py +149 -6
- nodes/backends.py +125 -18
- nodes/dns.py +203 -0
- nodes/models.py +498 -9
- nodes/tests.py +682 -3
- nodes/views.py +154 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +255 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +52 -7
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +414 -8
- ocpp/views.py +109 -76
- pages/admin.py +9 -1
- pages/context_processors.py +24 -4
- pages/defaults.py +14 -0
- pages/forms.py +131 -0
- pages/models.py +53 -14
- pages/tests.py +450 -14
- pages/urls.py +4 -0
- pages/views.py +419 -110
- arthexis-0.1.9.dist-info/RECORD +0 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.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,17 @@ 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")
|
|
207
|
+
relation_present = False
|
|
208
|
+
if hasattr(data, "getlist"):
|
|
209
|
+
relation_present = "current_relation" in data
|
|
210
|
+
else:
|
|
211
|
+
relation_present = "current_relation" in data
|
|
212
|
+
raw_relation = data.get("current_relation")
|
|
213
|
+
relation_value = (
|
|
214
|
+
Node.normalize_relation(raw_relation) if relation_present else None
|
|
215
|
+
)
|
|
129
216
|
|
|
130
217
|
if not hostname or not address or not mac_address:
|
|
131
218
|
response = JsonResponse(
|
|
@@ -148,6 +235,10 @@ def register_node(request):
|
|
|
148
235
|
response = JsonResponse({"detail": "invalid signature"}, status=403)
|
|
149
236
|
return _add_cors_headers(request, response)
|
|
150
237
|
|
|
238
|
+
if not verified and not request.user.is_authenticated:
|
|
239
|
+
response = JsonResponse({"detail": "authentication required"}, status=401)
|
|
240
|
+
return _add_cors_headers(request, response)
|
|
241
|
+
|
|
151
242
|
mac_address = mac_address.lower()
|
|
152
243
|
defaults = {
|
|
153
244
|
"hostname": hostname,
|
|
@@ -156,12 +247,20 @@ def register_node(request):
|
|
|
156
247
|
}
|
|
157
248
|
if verified:
|
|
158
249
|
defaults["public_key"] = public_key
|
|
250
|
+
if installed_version is not None:
|
|
251
|
+
defaults["installed_version"] = str(installed_version)[:20]
|
|
252
|
+
if installed_revision is not None:
|
|
253
|
+
defaults["installed_revision"] = str(installed_revision)[:40]
|
|
254
|
+
if relation_value is not None:
|
|
255
|
+
defaults["current_relation"] = relation_value
|
|
159
256
|
|
|
160
257
|
node, created = Node.objects.get_or_create(
|
|
161
258
|
mac_address=mac_address,
|
|
162
259
|
defaults=defaults,
|
|
163
260
|
)
|
|
164
261
|
if not created:
|
|
262
|
+
previous_version = (node.installed_version or "").strip()
|
|
263
|
+
previous_revision = (node.installed_revision or "").strip()
|
|
165
264
|
node.hostname = hostname
|
|
166
265
|
node.address = address
|
|
167
266
|
node.port = port
|
|
@@ -169,8 +268,30 @@ def register_node(request):
|
|
|
169
268
|
if verified:
|
|
170
269
|
node.public_key = public_key
|
|
171
270
|
update_fields.append("public_key")
|
|
271
|
+
if installed_version is not None:
|
|
272
|
+
node.installed_version = str(installed_version)[:20]
|
|
273
|
+
if "installed_version" not in update_fields:
|
|
274
|
+
update_fields.append("installed_version")
|
|
275
|
+
if installed_revision is not None:
|
|
276
|
+
node.installed_revision = str(installed_revision)[:40]
|
|
277
|
+
if "installed_revision" not in update_fields:
|
|
278
|
+
update_fields.append("installed_revision")
|
|
279
|
+
if relation_value is not None and node.current_relation != relation_value:
|
|
280
|
+
node.current_relation = relation_value
|
|
281
|
+
update_fields.append("current_relation")
|
|
172
282
|
node.save(update_fields=update_fields)
|
|
173
|
-
|
|
283
|
+
current_version = (node.installed_version or "").strip()
|
|
284
|
+
current_revision = (node.installed_revision or "").strip()
|
|
285
|
+
node_information_updated.send(
|
|
286
|
+
sender=Node,
|
|
287
|
+
node=node,
|
|
288
|
+
previous_version=previous_version,
|
|
289
|
+
previous_revision=previous_revision,
|
|
290
|
+
current_version=current_version,
|
|
291
|
+
current_revision=current_revision,
|
|
292
|
+
request=request,
|
|
293
|
+
)
|
|
294
|
+
if features is not None and (verified or request.user.is_authenticated):
|
|
174
295
|
if isinstance(features, (str, bytes)):
|
|
175
296
|
feature_list = [features]
|
|
176
297
|
else:
|
|
@@ -181,13 +302,25 @@ def register_node(request):
|
|
|
181
302
|
)
|
|
182
303
|
return _add_cors_headers(request, response)
|
|
183
304
|
|
|
184
|
-
if features is not None:
|
|
305
|
+
if features is not None and (verified or request.user.is_authenticated):
|
|
185
306
|
if isinstance(features, (str, bytes)):
|
|
186
307
|
feature_list = [features]
|
|
187
308
|
else:
|
|
188
309
|
feature_list = list(features)
|
|
189
310
|
node.update_manual_features(feature_list)
|
|
190
311
|
|
|
312
|
+
current_version = (node.installed_version or "").strip()
|
|
313
|
+
current_revision = (node.installed_revision or "").strip()
|
|
314
|
+
node_information_updated.send(
|
|
315
|
+
sender=Node,
|
|
316
|
+
node=node,
|
|
317
|
+
previous_version="",
|
|
318
|
+
previous_revision="",
|
|
319
|
+
current_version=current_version,
|
|
320
|
+
current_revision=current_revision,
|
|
321
|
+
request=request,
|
|
322
|
+
)
|
|
323
|
+
|
|
191
324
|
response = JsonResponse({"id": node.id})
|
|
192
325
|
return _add_cors_headers(request, response)
|
|
193
326
|
|
|
@@ -277,11 +410,22 @@ def net_message(request):
|
|
|
277
410
|
if reach_name:
|
|
278
411
|
reach_role = NodeRole.objects.filter(name=reach_name).first()
|
|
279
412
|
seen = data.get("seen", [])
|
|
413
|
+
origin_id = data.get("origin")
|
|
414
|
+
origin_node = None
|
|
415
|
+
if origin_id:
|
|
416
|
+
origin_node = Node.objects.filter(uuid=origin_id).first()
|
|
417
|
+
if not origin_node:
|
|
418
|
+
origin_node = node
|
|
280
419
|
if not msg_uuid:
|
|
281
420
|
return JsonResponse({"detail": "uuid required"}, status=400)
|
|
282
421
|
msg, created = NetMessage.objects.get_or_create(
|
|
283
422
|
uuid=msg_uuid,
|
|
284
|
-
defaults={
|
|
423
|
+
defaults={
|
|
424
|
+
"subject": subject[:64],
|
|
425
|
+
"body": body[:256],
|
|
426
|
+
"reach": reach_role,
|
|
427
|
+
"node_origin": origin_node,
|
|
428
|
+
},
|
|
285
429
|
)
|
|
286
430
|
if not created:
|
|
287
431
|
msg.subject = subject[:64]
|
|
@@ -290,6 +434,9 @@ def net_message(request):
|
|
|
290
434
|
if reach_role and msg.reach_id != reach_role.id:
|
|
291
435
|
msg.reach = reach_role
|
|
292
436
|
update_fields.append("reach")
|
|
437
|
+
if msg.node_origin_id is None and origin_node:
|
|
438
|
+
msg.node_origin = origin_node
|
|
439
|
+
update_fields.append("node_origin")
|
|
293
440
|
msg.save(update_fields=update_fields)
|
|
294
441
|
msg.propagate(seen=seen)
|
|
295
442
|
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()
|