arthexis 0.1.10__py3-none-any.whl → 0.1.12__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 (54) hide show
  1. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/METADATA +36 -26
  2. arthexis-0.1.12.dist-info/RECORD +102 -0
  3. config/context_processors.py +1 -0
  4. config/settings.py +31 -5
  5. config/urls.py +5 -4
  6. core/admin.py +430 -90
  7. core/apps.py +48 -2
  8. core/backends.py +38 -0
  9. core/environment.py +23 -5
  10. core/mailer.py +3 -1
  11. core/models.py +303 -31
  12. core/reference_utils.py +20 -9
  13. core/release.py +4 -0
  14. core/sigil_builder.py +7 -2
  15. core/sigil_resolver.py +35 -4
  16. core/system.py +250 -1
  17. core/tasks.py +92 -40
  18. core/temp_passwords.py +181 -0
  19. core/test_system_info.py +62 -2
  20. core/tests.py +169 -3
  21. core/user_data.py +51 -8
  22. core/views.py +371 -20
  23. nodes/admin.py +453 -8
  24. nodes/backends.py +21 -6
  25. nodes/dns.py +203 -0
  26. nodes/feature_checks.py +133 -0
  27. nodes/models.py +374 -31
  28. nodes/reports.py +411 -0
  29. nodes/tests.py +677 -38
  30. nodes/utils.py +32 -0
  31. nodes/views.py +14 -0
  32. ocpp/admin.py +278 -15
  33. ocpp/consumers.py +517 -16
  34. ocpp/evcs_discovery.py +158 -0
  35. ocpp/models.py +237 -4
  36. ocpp/reference_utils.py +42 -0
  37. ocpp/simulator.py +321 -22
  38. ocpp/store.py +110 -2
  39. ocpp/test_rfid.py +169 -7
  40. ocpp/tests.py +819 -6
  41. ocpp/transactions_io.py +17 -3
  42. ocpp/views.py +233 -19
  43. pages/admin.py +144 -4
  44. pages/context_processors.py +21 -7
  45. pages/defaults.py +13 -0
  46. pages/forms.py +38 -0
  47. pages/models.py +189 -15
  48. pages/tests.py +281 -8
  49. pages/urls.py +4 -0
  50. pages/views.py +137 -21
  51. arthexis-0.1.10.dist-info/RECORD +0 -95
  52. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/WHEEL +0 -0
  53. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/licenses/LICENSE +0 -0
  54. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/top_level.txt +0 -0
ocpp/consumers.py CHANGED
@@ -1,12 +1,14 @@
1
- import asyncio
2
- import json
3
1
  import base64
4
2
  import ipaddress
5
3
  import re
6
4
  from datetime import datetime
5
+ import asyncio
6
+ import inspect
7
+ import json
7
8
  from django.utils import timezone
8
9
  from core.models import EnergyAccount, Reference, RFID as CoreRFID
9
10
  from nodes.models import NetMessage
11
+ from django.core.exceptions import ValidationError
10
12
 
11
13
  from channels.generic.websocket import AsyncWebsocketConsumer
12
14
  from channels.db import database_sync_to_async
@@ -16,7 +18,15 @@ from config.offline import requires_network
16
18
  from . import store
17
19
  from decimal import Decimal
18
20
  from django.utils.dateparse import parse_datetime
19
- from .models import Transaction, Charger, MeterValue
21
+ from .models import Transaction, Charger, MeterValue, DataTransferMessage
22
+ from .reference_utils import host_is_local_loopback
23
+ from .evcs_discovery import (
24
+ DEFAULT_CONSOLE_PORT,
25
+ HTTPS_PORTS,
26
+ build_console_url,
27
+ prioritise_ports,
28
+ scan_open_ports,
29
+ )
20
30
 
21
31
  FORWARDED_PAIR_RE = re.compile(r"for=(?:\"?)(?P<value>[^;,\"\s]+)(?:\"?)", re.IGNORECASE)
22
32
 
@@ -129,7 +139,20 @@ class CSMSConsumer(AsyncWebsocketConsumer):
129
139
 
130
140
  @requires_network
131
141
  async def connect(self):
132
- self.charger_id = self.scope["url_route"]["kwargs"].get("cid", "")
142
+ raw_serial = self.scope["url_route"]["kwargs"].get("cid", "")
143
+ try:
144
+ self.charger_id = Charger.validate_serial(raw_serial)
145
+ except ValidationError as exc:
146
+ serial = Charger.normalize_serial(raw_serial)
147
+ store_key = store.pending_key(serial)
148
+ message = exc.messages[0] if exc.messages else "Invalid Serial Number"
149
+ store.add_log(
150
+ store_key,
151
+ f"Rejected connection: {message}",
152
+ log_type="charger",
153
+ )
154
+ await self.close(code=4003)
155
+ return
133
156
  self.connector_value: int | None = None
134
157
  self.store_key = store.pending_key(self.charger_id)
135
158
  self.aggregate_charger: Charger | None = None
@@ -287,19 +310,26 @@ class CSMSConsumer(AsyncWebsocketConsumer):
287
310
  serial = (self.charger_id or "").strip()
288
311
  if not ip or not serial:
289
312
  return
313
+ if host_is_local_loopback(ip):
314
+ return
290
315
  host = ip
291
- if ":" in host and not host.startswith("["):
292
- host = f"[{host}]"
293
- url = f"http://{host}:8900"
316
+ ports = scan_open_ports(host)
317
+ if ports:
318
+ ordered_ports = prioritise_ports(ports)
319
+ else:
320
+ ordered_ports = prioritise_ports([DEFAULT_CONSOLE_PORT])
321
+ port = ordered_ports[0] if ordered_ports else DEFAULT_CONSOLE_PORT
322
+ secure = port in HTTPS_PORTS
323
+ url = build_console_url(host, port, secure)
294
324
  alt_text = f"{serial} Console"
295
- reference, _ = Reference.objects.get_or_create(
296
- alt_text=alt_text,
297
- defaults={
298
- "value": url,
299
- "show_in_header": True,
300
- "method": "link",
301
- },
302
- )
325
+ reference = Reference.objects.filter(alt_text=alt_text).order_by("id").first()
326
+ if reference is None:
327
+ reference = Reference.objects.create(
328
+ alt_text=alt_text,
329
+ value=url,
330
+ show_in_header=True,
331
+ method="link",
332
+ )
303
333
  updated_fields: list[str] = []
304
334
  if reference.value != url:
305
335
  reference.value = url
@@ -550,6 +580,435 @@ class CSMSConsumer(AsyncWebsocketConsumer):
550
580
  task.add_done_callback(lambda _: setattr(self, "_consumption_task", None))
551
581
  self._consumption_task = task
552
582
 
583
+ async def _handle_call_result(self, message_id: str, payload: dict | None) -> None:
584
+ metadata = store.pop_pending_call(message_id)
585
+ if not metadata:
586
+ return
587
+ if metadata.get("charger_id") and metadata.get("charger_id") != self.charger_id:
588
+ return
589
+ action = metadata.get("action")
590
+ log_key = metadata.get("log_key") or self.store_key
591
+ if action == "DataTransfer":
592
+ message_pk = metadata.get("message_pk")
593
+ if not message_pk:
594
+ return
595
+
596
+ def _apply():
597
+ message = DataTransferMessage.objects.filter(pk=message_pk).first()
598
+ if not message:
599
+ return
600
+ status_value = str((payload or {}).get("status") or "").strip()
601
+ message.status = status_value
602
+ message.response_data = (payload or {}).get("data")
603
+ message.error_code = ""
604
+ message.error_description = ""
605
+ message.error_details = None
606
+ message.responded_at = timezone.now()
607
+ message.save(
608
+ update_fields=[
609
+ "status",
610
+ "response_data",
611
+ "error_code",
612
+ "error_description",
613
+ "error_details",
614
+ "responded_at",
615
+ "updated_at",
616
+ ]
617
+ )
618
+
619
+ await database_sync_to_async(_apply)()
620
+ return
621
+ if action == "GetConfiguration":
622
+ payload_data = payload if isinstance(payload, dict) else {}
623
+ try:
624
+ payload_text = json.dumps(
625
+ payload_data, sort_keys=True, ensure_ascii=False
626
+ )
627
+ except TypeError:
628
+ payload_text = str(payload_data)
629
+ store.add_log(
630
+ log_key,
631
+ f"GetConfiguration result: {payload_text}",
632
+ log_type="charger",
633
+ )
634
+ return
635
+ if action == "TriggerMessage":
636
+ payload_data = payload if isinstance(payload, dict) else {}
637
+ status_value = str(payload_data.get("status") or "").strip()
638
+ target = metadata.get("trigger_target") or metadata.get("follow_up_action")
639
+ connector_value = metadata.get("trigger_connector")
640
+ message = "TriggerMessage result"
641
+ if target:
642
+ message = f"TriggerMessage {target} result"
643
+ if status_value:
644
+ message += f": status={status_value}"
645
+ if connector_value:
646
+ message += f", connector={connector_value}"
647
+ store.add_log(log_key, message, log_type="charger")
648
+ if status_value == "Accepted" and target:
649
+ store.register_triggered_followup(
650
+ self.charger_id,
651
+ str(target),
652
+ connector=connector_value,
653
+ log_key=log_key,
654
+ target=str(target),
655
+ )
656
+ return
657
+ if action == "RemoteStartTransaction":
658
+ payload_data = payload if isinstance(payload, dict) else {}
659
+ status_value = str(payload_data.get("status") or "").strip()
660
+ message = "RemoteStartTransaction result"
661
+ if status_value:
662
+ message += f": status={status_value}"
663
+ store.add_log(log_key, message, log_type="charger")
664
+ return
665
+ if action == "RemoteStopTransaction":
666
+ payload_data = payload if isinstance(payload, dict) else {}
667
+ status_value = str(payload_data.get("status") or "").strip()
668
+ message = "RemoteStopTransaction result"
669
+ if status_value:
670
+ message += f": status={status_value}"
671
+ store.add_log(log_key, message, log_type="charger")
672
+ return
673
+ if action == "Reset":
674
+ payload_data = payload if isinstance(payload, dict) else {}
675
+ status_value = str(payload_data.get("status") or "").strip()
676
+ message = "Reset result"
677
+ if status_value:
678
+ message += f": status={status_value}"
679
+ store.add_log(log_key, message, log_type="charger")
680
+ return
681
+ if action != "ChangeAvailability":
682
+ return
683
+ status = str((payload or {}).get("status") or "").strip()
684
+ requested_type = metadata.get("availability_type")
685
+ connector_value = metadata.get("connector_id")
686
+ requested_at = metadata.get("requested_at")
687
+ await self._update_change_availability_state(
688
+ connector_value,
689
+ requested_type,
690
+ status,
691
+ requested_at,
692
+ details="",
693
+ )
694
+
695
+ async def _handle_call_error(
696
+ self,
697
+ message_id: str,
698
+ error_code: str | None,
699
+ description: str | None,
700
+ details: dict | None,
701
+ ) -> None:
702
+ metadata = store.pop_pending_call(message_id)
703
+ if not metadata:
704
+ return
705
+ if metadata.get("charger_id") and metadata.get("charger_id") != self.charger_id:
706
+ return
707
+ action = metadata.get("action")
708
+ log_key = metadata.get("log_key") or self.store_key
709
+ if action == "DataTransfer":
710
+ message_pk = metadata.get("message_pk")
711
+ if not message_pk:
712
+ return
713
+
714
+ def _apply():
715
+ message = DataTransferMessage.objects.filter(pk=message_pk).first()
716
+ if not message:
717
+ return
718
+ status_value = (error_code or "Error").strip() or "Error"
719
+ message.status = status_value
720
+ message.response_data = None
721
+ message.error_code = (error_code or "").strip()
722
+ message.error_description = (description or "").strip()
723
+ message.error_details = details
724
+ message.responded_at = timezone.now()
725
+ message.save(
726
+ update_fields=[
727
+ "status",
728
+ "response_data",
729
+ "error_code",
730
+ "error_description",
731
+ "error_details",
732
+ "responded_at",
733
+ "updated_at",
734
+ ]
735
+ )
736
+
737
+ await database_sync_to_async(_apply)()
738
+ return
739
+ if action == "GetConfiguration":
740
+ parts: list[str] = []
741
+ code_text = (error_code or "").strip()
742
+ if code_text:
743
+ parts.append(f"code={code_text}")
744
+ description_text = (description or "").strip()
745
+ if description_text:
746
+ parts.append(f"description={description_text}")
747
+ if details:
748
+ try:
749
+ details_text = json.dumps(details, sort_keys=True, ensure_ascii=False)
750
+ except TypeError:
751
+ details_text = str(details)
752
+ if details_text:
753
+ parts.append(f"details={details_text}")
754
+ if parts:
755
+ message = "GetConfiguration error: " + ", ".join(parts)
756
+ else:
757
+ message = "GetConfiguration error"
758
+ store.add_log(log_key, message, log_type="charger")
759
+ return
760
+ if action == "TriggerMessage":
761
+ target = metadata.get("trigger_target") or metadata.get("follow_up_action")
762
+ connector_value = metadata.get("trigger_connector")
763
+ parts: list[str] = []
764
+ if error_code:
765
+ parts.append(f"code={str(error_code).strip()}")
766
+ if description:
767
+ parts.append(f"description={str(description).strip()}")
768
+ if details:
769
+ try:
770
+ parts.append(
771
+ "details="
772
+ + json.dumps(details, sort_keys=True, ensure_ascii=False)
773
+ )
774
+ except TypeError:
775
+ parts.append(f"details={details}")
776
+ label = f"TriggerMessage {target}" if target else "TriggerMessage"
777
+ message = label + " error"
778
+ if parts:
779
+ message += ": " + ", ".join(parts)
780
+ if connector_value:
781
+ message += f", connector={connector_value}"
782
+ store.add_log(log_key, message, log_type="charger")
783
+ return
784
+ if action == "RemoteStartTransaction":
785
+ message = "RemoteStartTransaction error"
786
+ if error_code:
787
+ message += f": code={str(error_code).strip()}"
788
+ if description:
789
+ suffix = str(description).strip()
790
+ if suffix:
791
+ message += f", description={suffix}"
792
+ store.add_log(log_key, message, log_type="charger")
793
+ return
794
+ if action == "RemoteStopTransaction":
795
+ message = "RemoteStopTransaction error"
796
+ if error_code:
797
+ message += f": code={str(error_code).strip()}"
798
+ if description:
799
+ suffix = str(description).strip()
800
+ if suffix:
801
+ message += f", description={suffix}"
802
+ store.add_log(log_key, message, log_type="charger")
803
+ return
804
+ if action == "Reset":
805
+ message = "Reset error"
806
+ if error_code:
807
+ message += f": code={str(error_code).strip()}"
808
+ if description:
809
+ suffix = str(description).strip()
810
+ if suffix:
811
+ message += f", description={suffix}"
812
+ store.add_log(log_key, message, log_type="charger")
813
+ return
814
+ if action != "ChangeAvailability":
815
+ return
816
+ detail_text = (description or "").strip()
817
+ if not detail_text and details:
818
+ try:
819
+ detail_text = json.dumps(details, sort_keys=True)
820
+ except Exception:
821
+ detail_text = str(details)
822
+ if not detail_text:
823
+ detail_text = (error_code or "").strip() or "Error"
824
+ requested_type = metadata.get("availability_type")
825
+ connector_value = metadata.get("connector_id")
826
+ requested_at = metadata.get("requested_at")
827
+ await self._update_change_availability_state(
828
+ connector_value,
829
+ requested_type,
830
+ "Rejected",
831
+ requested_at,
832
+ details=detail_text,
833
+ )
834
+
835
+ async def _handle_data_transfer(
836
+ self, message_id: str, payload: dict | None
837
+ ) -> dict[str, object]:
838
+ payload = payload if isinstance(payload, dict) else {}
839
+ vendor_id = str(payload.get("vendorId") or "").strip()
840
+ vendor_message_id = payload.get("messageId")
841
+ if vendor_message_id is None:
842
+ vendor_message_id_text = ""
843
+ elif isinstance(vendor_message_id, str):
844
+ vendor_message_id_text = vendor_message_id.strip()
845
+ else:
846
+ vendor_message_id_text = str(vendor_message_id)
847
+ connector_value = self.connector_value
848
+
849
+ def _get_or_create_charger():
850
+ if self.charger and getattr(self.charger, "pk", None):
851
+ return self.charger
852
+ if connector_value is None:
853
+ charger, _ = Charger.objects.get_or_create(
854
+ charger_id=self.charger_id,
855
+ connector_id=None,
856
+ defaults={"last_path": self.scope.get("path", "")},
857
+ )
858
+ return charger
859
+ charger, _ = Charger.objects.get_or_create(
860
+ charger_id=self.charger_id,
861
+ connector_id=connector_value,
862
+ defaults={"last_path": self.scope.get("path", "")},
863
+ )
864
+ return charger
865
+
866
+ charger_obj = await database_sync_to_async(_get_or_create_charger)()
867
+ message = await database_sync_to_async(DataTransferMessage.objects.create)(
868
+ charger=charger_obj,
869
+ connector_id=connector_value,
870
+ direction=DataTransferMessage.DIRECTION_CP_TO_CSMS,
871
+ ocpp_message_id=message_id,
872
+ vendor_id=vendor_id,
873
+ message_id=vendor_message_id_text,
874
+ payload=payload or {},
875
+ status="Pending",
876
+ )
877
+
878
+ status = "Rejected" if not vendor_id else "UnknownVendorId"
879
+ response_data = None
880
+ error_code = ""
881
+ error_description = ""
882
+ error_details = None
883
+
884
+ handler = self._resolve_data_transfer_handler(vendor_id) if vendor_id else None
885
+ if handler:
886
+ try:
887
+ result = handler(message, payload)
888
+ if inspect.isawaitable(result):
889
+ result = await result
890
+ except Exception as exc: # pragma: no cover - defensive guard
891
+ status = "Rejected"
892
+ error_code = "InternalError"
893
+ error_description = str(exc)
894
+ else:
895
+ if isinstance(result, tuple):
896
+ status = str(result[0]) if result else status
897
+ if len(result) > 1:
898
+ response_data = result[1]
899
+ elif isinstance(result, dict):
900
+ status = str(result.get("status", status))
901
+ if "data" in result:
902
+ response_data = result["data"]
903
+ elif isinstance(result, str):
904
+ status = result
905
+ final_status = status or "Rejected"
906
+
907
+ def _finalise():
908
+ DataTransferMessage.objects.filter(pk=message.pk).update(
909
+ status=final_status,
910
+ response_data=response_data,
911
+ error_code=error_code,
912
+ error_description=error_description,
913
+ error_details=error_details,
914
+ responded_at=timezone.now(),
915
+ )
916
+
917
+ await database_sync_to_async(_finalise)()
918
+
919
+ reply_payload: dict[str, object] = {"status": final_status}
920
+ if response_data is not None:
921
+ reply_payload["data"] = response_data
922
+ return reply_payload
923
+
924
+ def _resolve_data_transfer_handler(self, vendor_id: str):
925
+ if not vendor_id:
926
+ return None
927
+ candidate = f"handle_data_transfer_{vendor_id.lower()}"
928
+ return getattr(self, candidate, None)
929
+
930
+ async def _update_change_availability_state(
931
+ self,
932
+ connector_value: int | None,
933
+ requested_type: str | None,
934
+ status: str,
935
+ requested_at,
936
+ *,
937
+ details: str = "",
938
+ ) -> None:
939
+ status_value = status or ""
940
+ now = timezone.now()
941
+
942
+ def _apply():
943
+ filters: dict[str, object] = {"charger_id": self.charger_id}
944
+ if connector_value is None:
945
+ filters["connector_id__isnull"] = True
946
+ else:
947
+ filters["connector_id"] = connector_value
948
+ targets = list(Charger.objects.filter(**filters))
949
+ if not targets:
950
+ return
951
+ for target in targets:
952
+ updates: dict[str, object] = {
953
+ "availability_request_status": status_value,
954
+ "availability_request_status_at": now,
955
+ "availability_request_details": details,
956
+ }
957
+ if requested_type:
958
+ updates["availability_requested_state"] = requested_type
959
+ if requested_at:
960
+ updates["availability_requested_at"] = requested_at
961
+ elif requested_type:
962
+ updates["availability_requested_at"] = now
963
+ if status_value == "Accepted" and requested_type:
964
+ updates["availability_state"] = requested_type
965
+ updates["availability_state_updated_at"] = now
966
+ Charger.objects.filter(pk=target.pk).update(**updates)
967
+ for field, value in updates.items():
968
+ setattr(target, field, value)
969
+ if self.charger and self.charger.pk == target.pk:
970
+ for field, value in updates.items():
971
+ setattr(self.charger, field, value)
972
+ if self.aggregate_charger and self.aggregate_charger.pk == target.pk:
973
+ for field, value in updates.items():
974
+ setattr(self.aggregate_charger, field, value)
975
+
976
+ await database_sync_to_async(_apply)()
977
+
978
+ async def _update_availability_state(
979
+ self,
980
+ state: str,
981
+ timestamp: datetime,
982
+ connector_value: int | None,
983
+ ) -> None:
984
+ def _apply():
985
+ filters: dict[str, object] = {"charger_id": self.charger_id}
986
+ if connector_value is None:
987
+ filters["connector_id__isnull"] = True
988
+ else:
989
+ filters["connector_id"] = connector_value
990
+ updates = {
991
+ "availability_state": state,
992
+ "availability_state_updated_at": timestamp,
993
+ }
994
+ targets = list(Charger.objects.filter(**filters))
995
+ if not targets:
996
+ return
997
+ Charger.objects.filter(pk__in=[target.pk for target in targets]).update(
998
+ **updates
999
+ )
1000
+ for target in targets:
1001
+ for field, value in updates.items():
1002
+ setattr(target, field, value)
1003
+ if self.charger and self.charger.pk == target.pk:
1004
+ for field, value in updates.items():
1005
+ setattr(self.charger, field, value)
1006
+ if self.aggregate_charger and self.aggregate_charger.pk == target.pk:
1007
+ for field, value in updates.items():
1008
+ setattr(self.aggregate_charger, field, value)
1009
+
1010
+ await database_sync_to_async(_apply)()
1011
+
553
1012
  async def disconnect(self, close_code):
554
1013
  store.release_ip_connection(getattr(self, "client_ip", None), self)
555
1014
  tx_obj = None
@@ -564,6 +1023,7 @@ class CSMSConsumer(AsyncWebsocketConsumer):
564
1023
  store.connections.pop(pending_key, None)
565
1024
  store.end_session_log(self.store_key)
566
1025
  store.stop_session_lock()
1026
+ store.clear_pending_calls(self.charger_id)
567
1027
  store.add_log(self.store_key, f"Closed (code={close_code})", log_type="charger")
568
1028
 
569
1029
  async def receive(self, text_data=None, bytes_data=None):
@@ -578,10 +1038,34 @@ class CSMSConsumer(AsyncWebsocketConsumer):
578
1038
  msg = json.loads(raw)
579
1039
  except json.JSONDecodeError:
580
1040
  return
581
- if isinstance(msg, list) and msg and msg[0] == 2:
1041
+ if not isinstance(msg, list) or not msg:
1042
+ return
1043
+ message_type = msg[0]
1044
+ if message_type == 2:
582
1045
  msg_id, action = msg[1], msg[2]
583
1046
  payload = msg[3] if len(msg) > 3 else {}
584
1047
  reply_payload = {}
1048
+ connector_hint = None
1049
+ if isinstance(payload, dict):
1050
+ connector_hint = payload.get("connectorId")
1051
+ follow_up = store.consume_triggered_followup(
1052
+ self.charger_id, action, connector_hint
1053
+ )
1054
+ if follow_up:
1055
+ follow_up_log_key = follow_up.get("log_key") or self.store_key
1056
+ target_label = follow_up.get("target") or action
1057
+ connector_slug_value = follow_up.get("connector")
1058
+ suffix = ""
1059
+ if (
1060
+ connector_slug_value
1061
+ and connector_slug_value != store.AGGREGATE_SLUG
1062
+ ):
1063
+ suffix = f" (connector {connector_slug_value})"
1064
+ store.add_log(
1065
+ follow_up_log_key,
1066
+ f"TriggerMessage follow-up received: {target_label}{suffix}",
1067
+ log_type="charger",
1068
+ )
585
1069
  await self._assign_connector(payload.get("connectorId"))
586
1070
  if action == "BootNotification":
587
1071
  reply_payload = {
@@ -589,6 +1073,8 @@ class CSMSConsumer(AsyncWebsocketConsumer):
589
1073
  "interval": 300,
590
1074
  "status": "Accepted",
591
1075
  }
1076
+ elif action == "DataTransfer":
1077
+ reply_payload = await self._handle_data_transfer(msg_id, payload)
592
1078
  elif action == "Heartbeat":
593
1079
  reply_payload = {"currentTime": datetime.utcnow().isoformat() + "Z"}
594
1080
  now = timezone.now()
@@ -652,6 +1138,11 @@ class CSMSConsumer(AsyncWebsocketConsumer):
652
1138
  f"StatusNotification processed: {json.dumps(payload, sort_keys=True)}",
653
1139
  log_type="charger",
654
1140
  )
1141
+ availability_state = Charger.availability_state_from_status(status)
1142
+ if availability_state:
1143
+ await self._update_availability_state(
1144
+ availability_state, status_timestamp, self.connector_value
1145
+ )
655
1146
  reply_payload = {}
656
1147
  elif action == "Authorize":
657
1148
  account = await self._get_account(payload.get("idTag"))
@@ -839,3 +1330,13 @@ class CSMSConsumer(AsyncWebsocketConsumer):
839
1330
  store.add_log(
840
1331
  self.store_key, f"< {json.dumps(response)}", log_type="charger"
841
1332
  )
1333
+ elif message_type == 3:
1334
+ msg_id = msg[1] if len(msg) > 1 else ""
1335
+ payload = msg[2] if len(msg) > 2 else {}
1336
+ await self._handle_call_result(msg_id, payload)
1337
+ elif message_type == 4:
1338
+ msg_id = msg[1] if len(msg) > 1 else ""
1339
+ error_code = msg[2] if len(msg) > 2 else ""
1340
+ description = msg[3] if len(msg) > 3 else ""
1341
+ details = msg[4] if len(msg) > 4 else {}
1342
+ await self._handle_call_error(msg_id, error_code, description, details)