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.

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,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
- if features is not None:
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={"subject": subject[:64], "body": body[:256], "reach": reach_role},
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()