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.
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/METADATA +36 -26
- arthexis-0.1.12.dist-info/RECORD +102 -0
- config/context_processors.py +1 -0
- config/settings.py +31 -5
- config/urls.py +5 -4
- core/admin.py +430 -90
- core/apps.py +48 -2
- core/backends.py +38 -0
- core/environment.py +23 -5
- core/mailer.py +3 -1
- core/models.py +303 -31
- core/reference_utils.py +20 -9
- core/release.py +4 -0
- core/sigil_builder.py +7 -2
- core/sigil_resolver.py +35 -4
- core/system.py +250 -1
- core/tasks.py +92 -40
- core/temp_passwords.py +181 -0
- core/test_system_info.py +62 -2
- core/tests.py +169 -3
- core/user_data.py +51 -8
- core/views.py +371 -20
- nodes/admin.py +453 -8
- nodes/backends.py +21 -6
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/models.py +374 -31
- nodes/reports.py +411 -0
- nodes/tests.py +677 -38
- nodes/utils.py +32 -0
- nodes/views.py +14 -0
- ocpp/admin.py +278 -15
- ocpp/consumers.py +517 -16
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +237 -4
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +321 -22
- ocpp/store.py +110 -2
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +819 -6
- ocpp/transactions_io.py +17 -3
- ocpp/views.py +233 -19
- pages/admin.py +144 -4
- pages/context_processors.py +21 -7
- pages/defaults.py +13 -0
- pages/forms.py +38 -0
- pages/models.py +189 -15
- pages/tests.py +281 -8
- pages/urls.py +4 -0
- pages/views.py +137 -21
- arthexis-0.1.10.dist-info/RECORD +0 -95
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/WHEEL +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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)
|
|
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)
|