arthexis 0.1.11__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.11.dist-info → arthexis-0.1.12.dist-info}/METADATA +2 -2
- {arthexis-0.1.11.dist-info → arthexis-0.1.12.dist-info}/RECORD +38 -35
- config/settings.py +7 -2
- core/admin.py +246 -68
- core/apps.py +21 -0
- core/models.py +41 -8
- core/reference_utils.py +1 -1
- core/release.py +4 -0
- core/system.py +6 -3
- core/tasks.py +92 -40
- core/tests.py +64 -0
- core/views.py +131 -17
- nodes/admin.py +316 -6
- nodes/feature_checks.py +133 -0
- nodes/models.py +83 -26
- nodes/reports.py +411 -0
- nodes/tests.py +365 -36
- nodes/utils.py +32 -0
- ocpp/admin.py +278 -15
- ocpp/consumers.py +506 -8
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +234 -4
- ocpp/simulator.py +321 -22
- ocpp/store.py +110 -2
- ocpp/tests.py +789 -6
- ocpp/transactions_io.py +17 -3
- ocpp/views.py +225 -19
- pages/admin.py +135 -3
- pages/context_processors.py +15 -1
- pages/defaults.py +1 -2
- pages/forms.py +38 -0
- pages/models.py +136 -1
- pages/tests.py +262 -4
- pages/urls.py +1 -0
- pages/views.py +52 -3
- {arthexis-0.1.11.dist-info → arthexis-0.1.12.dist-info}/WHEEL +0 -0
- {arthexis-0.1.11.dist-info → arthexis-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.11.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,8 +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
|
|
20
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
|
+
)
|
|
21
30
|
|
|
22
31
|
FORWARDED_PAIR_RE = re.compile(r"for=(?:\"?)(?P<value>[^;,\"\s]+)(?:\"?)", re.IGNORECASE)
|
|
23
32
|
|
|
@@ -130,7 +139,20 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
130
139
|
|
|
131
140
|
@requires_network
|
|
132
141
|
async def connect(self):
|
|
133
|
-
|
|
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
|
|
134
156
|
self.connector_value: int | None = None
|
|
135
157
|
self.store_key = store.pending_key(self.charger_id)
|
|
136
158
|
self.aggregate_charger: Charger | None = None
|
|
@@ -291,9 +313,14 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
291
313
|
if host_is_local_loopback(ip):
|
|
292
314
|
return
|
|
293
315
|
host = ip
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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)
|
|
297
324
|
alt_text = f"{serial} Console"
|
|
298
325
|
reference = Reference.objects.filter(alt_text=alt_text).order_by("id").first()
|
|
299
326
|
if reference is None:
|
|
@@ -553,6 +580,435 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
553
580
|
task.add_done_callback(lambda _: setattr(self, "_consumption_task", None))
|
|
554
581
|
self._consumption_task = task
|
|
555
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
|
+
|
|
556
1012
|
async def disconnect(self, close_code):
|
|
557
1013
|
store.release_ip_connection(getattr(self, "client_ip", None), self)
|
|
558
1014
|
tx_obj = None
|
|
@@ -567,6 +1023,7 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
567
1023
|
store.connections.pop(pending_key, None)
|
|
568
1024
|
store.end_session_log(self.store_key)
|
|
569
1025
|
store.stop_session_lock()
|
|
1026
|
+
store.clear_pending_calls(self.charger_id)
|
|
570
1027
|
store.add_log(self.store_key, f"Closed (code={close_code})", log_type="charger")
|
|
571
1028
|
|
|
572
1029
|
async def receive(self, text_data=None, bytes_data=None):
|
|
@@ -581,10 +1038,34 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
581
1038
|
msg = json.loads(raw)
|
|
582
1039
|
except json.JSONDecodeError:
|
|
583
1040
|
return
|
|
584
|
-
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:
|
|
585
1045
|
msg_id, action = msg[1], msg[2]
|
|
586
1046
|
payload = msg[3] if len(msg) > 3 else {}
|
|
587
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
|
+
)
|
|
588
1069
|
await self._assign_connector(payload.get("connectorId"))
|
|
589
1070
|
if action == "BootNotification":
|
|
590
1071
|
reply_payload = {
|
|
@@ -592,6 +1073,8 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
592
1073
|
"interval": 300,
|
|
593
1074
|
"status": "Accepted",
|
|
594
1075
|
}
|
|
1076
|
+
elif action == "DataTransfer":
|
|
1077
|
+
reply_payload = await self._handle_data_transfer(msg_id, payload)
|
|
595
1078
|
elif action == "Heartbeat":
|
|
596
1079
|
reply_payload = {"currentTime": datetime.utcnow().isoformat() + "Z"}
|
|
597
1080
|
now = timezone.now()
|
|
@@ -655,6 +1138,11 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
655
1138
|
f"StatusNotification processed: {json.dumps(payload, sort_keys=True)}",
|
|
656
1139
|
log_type="charger",
|
|
657
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
|
+
)
|
|
658
1146
|
reply_payload = {}
|
|
659
1147
|
elif action == "Authorize":
|
|
660
1148
|
account = await self._get_account(payload.get("idTag"))
|
|
@@ -842,3 +1330,13 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
842
1330
|
store.add_log(
|
|
843
1331
|
self.store_key, f"< {json.dumps(response)}", log_type="charger"
|
|
844
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)
|