arthexis 0.1.15__py3-none-any.whl → 0.1.17__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/models.py CHANGED
@@ -98,6 +98,12 @@ class NodeFeature(Entity):
98
98
  url_name="admin:nodes_nodefeature_celery_report",
99
99
  ),
100
100
  ),
101
+ "audio-capture": (
102
+ NodeFeatureDefaultAction(
103
+ label="View Waveform",
104
+ url_name="admin:nodes_nodefeature_view_waveform",
105
+ ),
106
+ ),
101
107
  "screenshot-poll": (
102
108
  NodeFeatureDefaultAction(
103
109
  label="Take Screenshot",
@@ -244,7 +250,7 @@ class Node(Entity):
244
250
  "ap-router",
245
251
  "gway-runner",
246
252
  }
247
- MANUAL_FEATURE_SLUGS = {"clipboard-poll", "screenshot-poll"}
253
+ MANUAL_FEATURE_SLUGS = {"clipboard-poll", "screenshot-poll", "audio-capture"}
248
254
 
249
255
  def __str__(self) -> str: # pragma: no cover - simple representation
250
256
  return f"{self.hostname}:{self.port}"
@@ -747,6 +753,7 @@ class Node(Entity):
747
753
  self._sync_clipboard_task(clipboard_enabled)
748
754
  self._sync_screenshot_task(screenshot_enabled)
749
755
  self._sync_landing_lead_task(celery_enabled)
756
+ self._sync_ocpp_session_report_task(celery_enabled)
750
757
 
751
758
  def _sync_clipboard_task(self, enabled: bool):
752
759
  from django_celery_beat.models import IntervalSchedule, PeriodicTask
@@ -819,6 +826,39 @@ class Node(Entity):
819
826
  else:
820
827
  PeriodicTask.objects.filter(name=task_name).delete()
821
828
 
829
+ def _sync_ocpp_session_report_task(self, celery_enabled: bool):
830
+ from django_celery_beat.models import CrontabSchedule, PeriodicTask
831
+ from django.db.utils import OperationalError, ProgrammingError
832
+
833
+ task_name = "ocpp_send_daily_session_report"
834
+
835
+ if not self.is_local:
836
+ return
837
+
838
+ if not celery_enabled or not mailer.can_send_email():
839
+ PeriodicTask.objects.filter(name=task_name).delete()
840
+ return
841
+
842
+ try:
843
+ schedule, _ = CrontabSchedule.objects.get_or_create(
844
+ minute="0",
845
+ hour="18",
846
+ day_of_week="*",
847
+ day_of_month="*",
848
+ month_of_year="*",
849
+ )
850
+ PeriodicTask.objects.update_or_create(
851
+ name=task_name,
852
+ defaults={
853
+ "crontab": schedule,
854
+ "interval": None,
855
+ "task": "ocpp.tasks.send_daily_session_report",
856
+ "enabled": True,
857
+ },
858
+ )
859
+ except (OperationalError, ProgrammingError):
860
+ logger.debug("Skipping OCPP session report task sync; tables not ready")
861
+
822
862
  def send_mail(
823
863
  self,
824
864
  subject: str,
@@ -955,15 +995,18 @@ class NodeManager(Profile):
955
995
  )
956
996
  api_key = SigilShortAutoField(
957
997
  max_length=255,
998
+ verbose_name="API key",
958
999
  help_text="API key issued by the DNS provider.",
959
1000
  )
960
1001
  api_secret = SigilShortAutoField(
961
1002
  max_length=255,
1003
+ verbose_name="API secret",
962
1004
  help_text="API secret issued by the DNS provider.",
963
1005
  )
964
1006
  customer_id = SigilShortAutoField(
965
1007
  max_length=100,
966
1008
  blank=True,
1009
+ verbose_name="Customer ID",
967
1010
  help_text="Optional GoDaddy customer identifier for the account.",
968
1011
  )
969
1012
  default_domain = SigilShortAutoField(
@@ -1367,6 +1410,7 @@ class NetMessage(Entity):
1367
1410
  propagated_to = models.ManyToManyField(
1368
1411
  Node, blank=True, related_name="received_net_messages"
1369
1412
  )
1413
+ confirmed_peers = models.JSONField(default=dict, blank=True)
1370
1414
  created = models.DateTimeField(auto_now_add=True)
1371
1415
  complete = models.BooleanField(default=False, editable=False)
1372
1416
 
@@ -1596,7 +1640,10 @@ class NetMessage(Entity):
1596
1640
  seen_list = seen.copy()
1597
1641
  selected_ids = [str(n.uuid) for n in selected]
1598
1642
  payload_seen = seen_list + selected_ids
1643
+ confirmed_peers = dict(self.confirmed_peers or {})
1644
+
1599
1645
  for node in selected:
1646
+ now = timezone.now().isoformat()
1600
1647
  payload = {
1601
1648
  "uuid": str(self.uuid),
1602
1649
  "subject": self.subject,
@@ -1632,20 +1679,39 @@ class NetMessage(Entity):
1632
1679
  headers["X-Signature"] = base64.b64encode(signature).decode()
1633
1680
  except Exception:
1634
1681
  pass
1682
+ status_entry = {
1683
+ "status": "pending",
1684
+ "status_code": None,
1685
+ "updated": now,
1686
+ }
1635
1687
  try:
1636
- requests.post(
1688
+ response = requests.post(
1637
1689
  f"http://{node.address}:{node.port}/nodes/net-message/",
1638
1690
  data=payload_json,
1639
1691
  headers=headers,
1640
1692
  timeout=1,
1641
1693
  )
1694
+ status_entry["status_code"] = getattr(response, "status_code", None)
1695
+ if getattr(response, "ok", False):
1696
+ status_entry["status"] = "acknowledged"
1697
+ else:
1698
+ status_entry["status"] = "failed"
1642
1699
  except Exception:
1643
- pass
1700
+ status_entry["status"] = "error"
1644
1701
  self.propagated_to.add(node)
1702
+ confirmed_peers[str(node.uuid)] = status_entry
1703
+
1704
+ save_fields: list[str] = []
1705
+ if confirmed_peers != (self.confirmed_peers or {}):
1706
+ self.confirmed_peers = confirmed_peers
1707
+ save_fields.append("confirmed_peers")
1645
1708
 
1646
1709
  if total_known and self.propagated_to.count() >= total_known:
1647
1710
  self.complete = True
1648
- self.save(update_fields=["complete"] if self.complete else [])
1711
+ save_fields.append("complete")
1712
+
1713
+ if save_fields:
1714
+ self.save(update_fields=save_fields)
1649
1715
 
1650
1716
 
1651
1717
  class ContentSample(Entity):
nodes/rfid_sync.py CHANGED
@@ -45,6 +45,8 @@ def serialize_rfid(tag: RFID) -> dict[str, Any]:
45
45
  "color": tag.color,
46
46
  "kind": tag.kind,
47
47
  "released": tag.released,
48
+ "external_command": tag.external_command,
49
+ "post_auth_command": tag.post_auth_command,
48
50
  "last_seen_on": tag.last_seen_on.isoformat() if tag.last_seen_on else None,
49
51
  "energy_accounts": [account.id for account in accounts],
50
52
  "energy_account_names": [
@@ -64,6 +66,17 @@ def apply_rfid_payload(
64
66
  outcome.error = "Missing RFID value"
65
67
  return outcome
66
68
 
69
+ external_command = entry.get("external_command")
70
+ if not isinstance(external_command, str):
71
+ external_command = ""
72
+ else:
73
+ external_command = external_command.strip()
74
+ post_auth_command = entry.get("post_auth_command")
75
+ if not isinstance(post_auth_command, str):
76
+ post_auth_command = ""
77
+ else:
78
+ post_auth_command = post_auth_command.strip()
79
+
67
80
  defaults: dict[str, Any] = {
68
81
  "custom_label": entry.get("custom_label", ""),
69
82
  "key_a": entry.get("key_a", RFID._meta.get_field("key_a").default),
@@ -75,6 +88,8 @@ def apply_rfid_payload(
75
88
  "color": entry.get("color", RFID.BLACK),
76
89
  "kind": entry.get("kind", RFID.CLASSIC),
77
90
  "released": bool(entry.get("released", False)),
91
+ "external_command": external_command,
92
+ "post_auth_command": post_auth_command,
78
93
  }
79
94
 
80
95
  if origin_node is not None:
nodes/tests.py CHANGED
@@ -1293,6 +1293,51 @@ class NodeRegisterCurrentTests(TestCase):
1293
1293
  PeriodicTask.objects.filter(name="pages_purge_landing_leads").exists()
1294
1294
  )
1295
1295
 
1296
+ def test_ocpp_session_report_task_syncs_with_feature(self):
1297
+ feature, _ = NodeFeature.objects.get_or_create(
1298
+ slug="celery-queue", defaults={"display": "Celery Queue"}
1299
+ )
1300
+ node, _ = Node.objects.update_or_create(
1301
+ mac_address=Node.get_current_mac(),
1302
+ defaults={
1303
+ "hostname": socket.gethostname(),
1304
+ "address": "127.0.0.1",
1305
+ "port": 9400,
1306
+ "base_path": settings.BASE_DIR,
1307
+ },
1308
+ )
1309
+ task_name = "ocpp_send_daily_session_report"
1310
+ PeriodicTask.objects.filter(name=task_name).delete()
1311
+
1312
+ with patch("nodes.models.mailer.can_send_email", return_value=True):
1313
+ NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
1314
+
1315
+ self.assertTrue(PeriodicTask.objects.filter(name=task_name).exists())
1316
+
1317
+ NodeFeatureAssignment.objects.filter(node=node, feature=feature).delete()
1318
+ self.assertFalse(PeriodicTask.objects.filter(name=task_name).exists())
1319
+
1320
+ def test_ocpp_session_report_task_requires_email(self):
1321
+ feature, _ = NodeFeature.objects.get_or_create(
1322
+ slug="celery-queue", defaults={"display": "Celery Queue"}
1323
+ )
1324
+ node, _ = Node.objects.update_or_create(
1325
+ mac_address=Node.get_current_mac(),
1326
+ defaults={
1327
+ "hostname": socket.gethostname(),
1328
+ "address": "127.0.0.1",
1329
+ "port": 9500,
1330
+ "base_path": settings.BASE_DIR,
1331
+ },
1332
+ )
1333
+ task_name = "ocpp_send_daily_session_report"
1334
+ PeriodicTask.objects.filter(name=task_name).delete()
1335
+
1336
+ with patch("nodes.models.mailer.can_send_email", return_value=False):
1337
+ NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
1338
+
1339
+ self.assertFalse(PeriodicTask.objects.filter(name=task_name).exists())
1340
+
1296
1341
 
1297
1342
  class CheckRegistrationReadyCommandTests(TestCase):
1298
1343
  def test_command_completes_successfully(self):
@@ -1348,6 +1393,16 @@ class NodeAdminTests(TestCase):
1348
1393
  self.assertContains(response, f'href="{snapshot_url}"')
1349
1394
  self.assertContains(response, f'href="{stream_url}"')
1350
1395
 
1396
+ def test_node_feature_list_shows_waveform_action_when_enabled(self):
1397
+ node = self._create_local_node()
1398
+ feature, _ = NodeFeature.objects.get_or_create(
1399
+ slug="audio-capture", defaults={"display": "Audio Capture"}
1400
+ )
1401
+ NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
1402
+ response = self.client.get(reverse("admin:nodes_nodefeature_changelist"))
1403
+ action_url = reverse("admin:nodes_nodefeature_view_waveform")
1404
+ self.assertContains(response, f'href="{action_url}"')
1405
+
1351
1406
  def test_node_feature_list_hides_default_action_when_disabled(self):
1352
1407
  self._create_local_node()
1353
1408
  NodeFeature.objects.get_or_create(
@@ -1750,6 +1805,34 @@ class NodeAdminTests(TestCase):
1750
1805
  self.assertEqual(response.context_data["stream_embed"], "unsupported")
1751
1806
  self.assertContains(response, "camera-stream__unsupported")
1752
1807
 
1808
+ def test_view_waveform_requires_enabled_feature(self):
1809
+ self._create_local_node()
1810
+ NodeFeature.objects.get_or_create(
1811
+ slug="audio-capture", defaults={"display": "Audio Capture"}
1812
+ )
1813
+ response = self.client.get(
1814
+ reverse("admin:nodes_nodefeature_view_waveform"), follow=True
1815
+ )
1816
+ self.assertEqual(response.status_code, 200)
1817
+ changelist_url = reverse("admin:nodes_nodefeature_changelist")
1818
+ self.assertEqual(response.wsgi_request.path, changelist_url)
1819
+ self.assertContains(
1820
+ response, "Audio Capture feature is not enabled on this node."
1821
+ )
1822
+
1823
+ def test_view_waveform_renders_when_feature_enabled(self):
1824
+ node = self._create_local_node()
1825
+ feature, _ = NodeFeature.objects.get_or_create(
1826
+ slug="audio-capture", defaults={"display": "Audio Capture"}
1827
+ )
1828
+ NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
1829
+ response = self.client.get(reverse("admin:nodes_nodefeature_view_waveform"))
1830
+ self.assertEqual(response.status_code, 200)
1831
+ response.render()
1832
+ self.assertEqual(response.context_data["feature"], feature)
1833
+ self.assertEqual(response.context_data["title"], "Audio Capture Waveform")
1834
+ self.assertContains(response, "audio-capture__canvas")
1835
+
1753
1836
  @patch("nodes.admin.requests.post")
1754
1837
  def test_import_rfids_action_fetches_and_imports(self, mock_post):
1755
1838
  local = self._create_local_node()
@@ -2496,6 +2579,8 @@ class NetMessagePropagationTests(TestCase):
2496
2579
  def test_propagate_forwards_to_three_and_notifies_local(
2497
2580
  self, mock_notify, mock_post
2498
2581
  ):
2582
+ mock_post.return_value.ok = True
2583
+ mock_post.return_value.status_code = 200
2499
2584
  msg = NetMessage.objects.create(subject="s", body="b", reach=self.role)
2500
2585
  with patch.object(Node, "get_local", return_value=self.local):
2501
2586
  msg.propagate(seen=[str(self.remotes[0].uuid)])
@@ -2512,6 +2597,13 @@ class NetMessagePropagationTests(TestCase):
2512
2597
  self.assertNotIn(sender_addr, targets)
2513
2598
  self.assertEqual(msg.propagated_to.count(), 4)
2514
2599
  self.assertTrue(msg.complete)
2600
+ self.assertEqual(len(msg.confirmed_peers), mock_post.call_count)
2601
+ self.assertTrue(
2602
+ all(entry["status"] == "acknowledged" for entry in msg.confirmed_peers.values())
2603
+ )
2604
+ self.assertTrue(
2605
+ all(entry["status_code"] == 200 for entry in msg.confirmed_peers.values())
2606
+ )
2515
2607
 
2516
2608
  @patch("requests.post")
2517
2609
  @patch("core.notifications.notify", return_value=False)
@@ -2587,6 +2679,21 @@ class NetMessagePropagationTests(TestCase):
2587
2679
  self.assertTrue(NetMessage.objects.filter(pk=old_remote.pk).exists())
2588
2680
  self.assertTrue(NetMessage.objects.filter(pk=msg.pk).exists())
2589
2681
 
2682
+ @patch("core.notifications.notify", return_value=False)
2683
+ def test_propagate_records_error_status(self, mock_notify):
2684
+ msg = NetMessage.objects.create(subject="s", body="b", reach=self.role)
2685
+ with (
2686
+ patch.object(Node, "get_local", return_value=self.local),
2687
+ patch("random.shuffle", side_effect=lambda seq: None),
2688
+ patch("requests.post", side_effect=Exception("boom")),
2689
+ ):
2690
+ msg.propagate()
2691
+
2692
+ self.assertTrue(msg.confirmed_peers)
2693
+ self.assertTrue(
2694
+ all(entry["status"] == "error" for entry in msg.confirmed_peers.values())
2695
+ )
2696
+
2590
2697
 
2591
2698
  class NetMessageSignatureTests(TestCase):
2592
2699
  def setUp(self):
@@ -3196,6 +3303,18 @@ class NodeFeatureTests(TestCase):
3196
3303
  self.assertIn("Take a Snapshot", labels)
3197
3304
  self.assertIn("View stream", labels)
3198
3305
 
3306
+ def test_audio_capture_feature_has_view_waveform_action(self):
3307
+ feature = NodeFeature.objects.create(
3308
+ slug="audio-capture", display="Audio Capture"
3309
+ )
3310
+ actions = feature.get_default_actions()
3311
+ self.assertEqual(len(actions), 1)
3312
+ action = actions[0]
3313
+ self.assertEqual(action.label, "View Waveform")
3314
+ self.assertEqual(
3315
+ action.url_name, "admin:nodes_nodefeature_view_waveform"
3316
+ )
3317
+
3199
3318
  def test_default_action_missing_when_unconfigured(self):
3200
3319
  feature = NodeFeature.objects.create(
3201
3320
  slug="custom-feature", display="Custom Feature"
nodes/utils.py CHANGED
@@ -89,6 +89,7 @@ def save_screenshot(
89
89
  transaction_uuid=None,
90
90
  *,
91
91
  content: str | None = None,
92
+ user=None,
92
93
  ):
93
94
  """Save screenshot file info if not already recorded.
94
95
 
@@ -115,6 +116,8 @@ def save_screenshot(
115
116
  data["transaction_uuid"] = transaction_uuid
116
117
  if content is not None:
117
118
  data["content"] = content
119
+ if user is not None:
120
+ data["user"] = user
118
121
  with suppress_default_classifiers():
119
122
  sample = ContentSample.objects.create(**data)
120
123
  run_default_classifiers(sample)
ocpp/admin.py CHANGED
@@ -29,6 +29,7 @@ from .transactions_io import (
29
29
  import_transactions as import_transactions_data,
30
30
  )
31
31
  from .status_display import STATUS_BADGE_MAP, ERROR_OK_VALUES
32
+ from .views import _charger_state, _live_sessions
32
33
  from core.admin import SaveBeforeChangeAction
33
34
  from core.user_data import EntityModelAdmin
34
35
 
@@ -253,6 +254,8 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
253
254
  actions = [
254
255
  "purge_data",
255
256
  "fetch_cp_configuration",
257
+ "toggle_rfid_authentication",
258
+ "recheck_charger_status",
256
259
  "change_availability_operative",
257
260
  "change_availability_inoperative",
258
261
  "set_availability_state_operative",
@@ -317,16 +320,14 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
317
320
  "charger-status-connector",
318
321
  args=[obj.charger_id, obj.connector_slug],
319
322
  )
320
- label = (obj.last_status or "status").strip() or "status"
321
- status_key = label.lower()
322
- error_code = (obj.last_error_code or "").strip().lower()
323
- if (
324
- self._has_active_session(obj)
325
- and error_code in ERROR_OK_VALUES
326
- and (status_key not in STATUS_BADGE_MAP or status_key == "available")
327
- ):
328
- label = STATUS_BADGE_MAP["charging"][0]
329
- return format_html('<a href="{}" target="_blank">{}</a>', url, label)
323
+ tx_obj = store.get_transaction(obj.charger_id, obj.connector_id)
324
+ state, _ = _charger_state(
325
+ obj,
326
+ tx_obj
327
+ if obj.connector_id is not None
328
+ else (_live_sessions(obj) or None),
329
+ )
330
+ return format_html('<a href="{}" target="_blank">{}</a>', url, state)
330
331
 
331
332
  status_link.short_description = "Status"
332
333
 
@@ -359,6 +360,63 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
359
360
 
360
361
  purge_data.short_description = "Purge data"
361
362
 
363
+ @admin.action(description="Re-check Charger Status")
364
+ def recheck_charger_status(self, request, queryset):
365
+ requested = 0
366
+ for charger in queryset:
367
+ connector_value = charger.connector_id
368
+ ws = store.get_connection(charger.charger_id, connector_value)
369
+ if ws is None:
370
+ self.message_user(
371
+ request,
372
+ f"{charger}: no active connection",
373
+ level=messages.ERROR,
374
+ )
375
+ continue
376
+ payload: dict[str, object] = {"requestedMessage": "StatusNotification"}
377
+ trigger_connector: int | None = None
378
+ if connector_value is not None:
379
+ payload["connectorId"] = connector_value
380
+ trigger_connector = connector_value
381
+ message_id = uuid.uuid4().hex
382
+ msg = json.dumps([2, message_id, "TriggerMessage", payload])
383
+ try:
384
+ async_to_sync(ws.send)(msg)
385
+ except Exception as exc: # pragma: no cover - network error
386
+ self.message_user(
387
+ request,
388
+ f"{charger}: failed to send TriggerMessage ({exc})",
389
+ level=messages.ERROR,
390
+ )
391
+ continue
392
+ log_key = store.identity_key(charger.charger_id, connector_value)
393
+ store.add_log(log_key, f"< {msg}", log_type="charger")
394
+ store.register_pending_call(
395
+ message_id,
396
+ {
397
+ "action": "TriggerMessage",
398
+ "charger_id": charger.charger_id,
399
+ "connector_id": connector_value,
400
+ "log_key": log_key,
401
+ "trigger_target": "StatusNotification",
402
+ "trigger_connector": trigger_connector,
403
+ "requested_at": timezone.now(),
404
+ },
405
+ )
406
+ store.schedule_call_timeout(
407
+ message_id,
408
+ timeout=5.0,
409
+ action="TriggerMessage",
410
+ log_key=log_key,
411
+ message="TriggerMessage StatusNotification timed out",
412
+ )
413
+ requested += 1
414
+ if requested:
415
+ self.message_user(
416
+ request,
417
+ f"Requested status update from {requested} charger(s)",
418
+ )
419
+
362
420
  @admin.action(description="Fetch CP configuration")
363
421
  def fetch_cp_configuration(self, request, queryset):
364
422
  fetched = 0
@@ -413,6 +471,30 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
413
471
  f"Requested configuration from {fetched} charger(s)",
414
472
  )
415
473
 
474
+ @admin.action(description="Toggle RFID Authentication")
475
+ def toggle_rfid_authentication(self, request, queryset):
476
+ enabled = 0
477
+ disabled = 0
478
+ for charger in queryset:
479
+ new_value = not charger.require_rfid
480
+ Charger.objects.filter(pk=charger.pk).update(require_rfid=new_value)
481
+ charger.require_rfid = new_value
482
+ if new_value:
483
+ enabled += 1
484
+ else:
485
+ disabled += 1
486
+ if enabled or disabled:
487
+ changes = []
488
+ if enabled:
489
+ changes.append(f"enabled for {enabled} charger(s)")
490
+ if disabled:
491
+ changes.append(f"disabled for {disabled} charger(s)")
492
+ summary = "; ".join(changes)
493
+ self.message_user(
494
+ request,
495
+ f"Updated RFID authentication: {summary}",
496
+ )
497
+
416
498
  def _dispatch_change_availability(self, request, queryset, availability_type: str):
417
499
  sent = 0
418
500
  for charger in queryset:
ocpp/consumers.py CHANGED
@@ -321,6 +321,44 @@ class CSMSConsumer(AsyncWebsocketConsumer):
321
321
  except (TypeError, ValueError):
322
322
  return
323
323
  if connector_value is None:
324
+ aggregate = self.aggregate_charger
325
+ if (
326
+ not aggregate
327
+ or aggregate.connector_id is not None
328
+ or aggregate.charger_id != self.charger_id
329
+ ):
330
+ aggregate, _ = await database_sync_to_async(
331
+ Charger.objects.get_or_create
332
+ )(
333
+ charger_id=self.charger_id,
334
+ connector_id=None,
335
+ defaults={"last_path": self.scope.get("path", "")},
336
+ )
337
+ await database_sync_to_async(aggregate.refresh_manager_node)()
338
+ self.aggregate_charger = aggregate
339
+ self.charger = self.aggregate_charger
340
+ previous_key = self.store_key
341
+ new_key = store.identity_key(self.charger_id, None)
342
+ if previous_key != new_key:
343
+ existing_consumer = store.connections.get(new_key)
344
+ if existing_consumer is not None and existing_consumer is not self:
345
+ await existing_consumer.close()
346
+ store.reassign_identity(previous_key, new_key)
347
+ store.connections[new_key] = self
348
+ store.logs["charger"].setdefault(new_key, [])
349
+ aggregate_name = await sync_to_async(
350
+ lambda: self.charger.name or self.charger.charger_id
351
+ )()
352
+ friendly_name = aggregate_name or self.charger_id
353
+ store.register_log_name(new_key, friendly_name, log_type="charger")
354
+ store.register_log_name(
355
+ store.identity_key(self.charger_id, None),
356
+ friendly_name,
357
+ log_type="charger",
358
+ )
359
+ store.register_log_name(self.charger_id, friendly_name, log_type="charger")
360
+ self.store_key = new_key
361
+ self.connector_value = None
324
362
  if not self._header_reference_created and self.client_ip:
325
363
  await database_sync_to_async(self._ensure_console_reference)()
326
364
  self._header_reference_created = True
ocpp/models.py CHANGED
@@ -854,11 +854,26 @@ class DataTransferMessage(models.Model):
854
854
  on_delete=models.CASCADE,
855
855
  related_name="data_transfer_messages",
856
856
  )
857
- connector_id = models.PositiveIntegerField(null=True, blank=True)
857
+ connector_id = models.PositiveIntegerField(
858
+ null=True,
859
+ blank=True,
860
+ verbose_name="Connector ID",
861
+ )
858
862
  direction = models.CharField(max_length=16, choices=DIRECTION_CHOICES)
859
- ocpp_message_id = models.CharField(max_length=64)
860
- vendor_id = models.CharField(max_length=255, blank=True)
861
- message_id = models.CharField(max_length=255, blank=True)
863
+ ocpp_message_id = models.CharField(
864
+ max_length=64,
865
+ verbose_name="OCPP message ID",
866
+ )
867
+ vendor_id = models.CharField(
868
+ max_length=255,
869
+ blank=True,
870
+ verbose_name="Vendor ID",
871
+ )
872
+ message_id = models.CharField(
873
+ max_length=255,
874
+ blank=True,
875
+ verbose_name="Message ID",
876
+ )
862
877
  payload = models.JSONField(default=dict, blank=True)
863
878
  status = models.CharField(max_length=64, blank=True)
864
879
  response_data = models.JSONField(null=True, blank=True)