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.

Files changed (51) hide show
  1. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/METADATA +76 -23
  2. arthexis-0.1.11.dist-info/RECORD +99 -0
  3. config/context_processors.py +1 -0
  4. config/settings.py +245 -26
  5. config/urls.py +11 -4
  6. core/admin.py +585 -57
  7. core/apps.py +29 -1
  8. core/auto_upgrade.py +57 -0
  9. core/backends.py +115 -3
  10. core/environment.py +23 -5
  11. core/fields.py +93 -0
  12. core/mailer.py +3 -1
  13. core/models.py +482 -38
  14. core/reference_utils.py +108 -0
  15. core/sigil_builder.py +23 -5
  16. core/sigil_resolver.py +35 -4
  17. core/system.py +400 -140
  18. core/tasks.py +151 -8
  19. core/temp_passwords.py +181 -0
  20. core/test_system_info.py +97 -1
  21. core/tests.py +393 -15
  22. core/user_data.py +154 -16
  23. core/views.py +499 -20
  24. nodes/admin.py +149 -6
  25. nodes/backends.py +125 -18
  26. nodes/dns.py +203 -0
  27. nodes/models.py +498 -9
  28. nodes/tests.py +682 -3
  29. nodes/views.py +154 -7
  30. ocpp/admin.py +63 -3
  31. ocpp/consumers.py +255 -41
  32. ocpp/evcs.py +6 -3
  33. ocpp/models.py +52 -7
  34. ocpp/reference_utils.py +42 -0
  35. ocpp/simulator.py +62 -5
  36. ocpp/store.py +30 -0
  37. ocpp/test_rfid.py +169 -7
  38. ocpp/tests.py +414 -8
  39. ocpp/views.py +109 -76
  40. pages/admin.py +9 -1
  41. pages/context_processors.py +24 -4
  42. pages/defaults.py +14 -0
  43. pages/forms.py +131 -0
  44. pages/models.py +53 -14
  45. pages/tests.py +450 -14
  46. pages/urls.py +4 -0
  47. pages/views.py +419 -110
  48. arthexis-0.1.9.dist-info/RECORD +0 -92
  49. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
  50. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
  51. {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": node.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
- if features is not None:
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={"subject": subject[:64], "body": body[:256], "reach": reach_role},
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()