arthexis 0.1.23__py3-none-any.whl → 0.1.25__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.23.dist-info → arthexis-0.1.25.dist-info}/METADATA +39 -18
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/RECORD +31 -30
- config/settings.py +7 -0
- config/urls.py +2 -0
- core/admin.py +140 -213
- core/backends.py +3 -1
- core/models.py +612 -207
- core/system.py +67 -2
- core/tasks.py +25 -0
- core/views.py +0 -3
- nodes/admin.py +465 -292
- nodes/models.py +299 -23
- nodes/tasks.py +13 -16
- nodes/tests.py +291 -130
- nodes/urls.py +11 -0
- nodes/utils.py +9 -2
- nodes/views.py +588 -20
- ocpp/admin.py +729 -175
- ocpp/consumers.py +98 -0
- ocpp/models.py +299 -0
- ocpp/network.py +398 -0
- ocpp/tasks.py +177 -1
- ocpp/tests.py +179 -0
- ocpp/views.py +2 -0
- pages/middleware.py +3 -2
- pages/tests.py +40 -0
- pages/utils.py +70 -0
- pages/views.py +64 -32
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/WHEEL +0 -0
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/top_level.txt +0 -0
ocpp/admin.py
CHANGED
|
@@ -2,11 +2,15 @@ from django.contrib import admin, messages
|
|
|
2
2
|
from django import forms
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
|
+
import base64
|
|
5
6
|
from datetime import datetime, time, timedelta
|
|
6
7
|
import json
|
|
8
|
+
from typing import Any
|
|
7
9
|
|
|
8
10
|
from django.shortcuts import redirect
|
|
9
11
|
from django.utils import formats, timezone, translation
|
|
12
|
+
from django.utils.translation import gettext_lazy as _
|
|
13
|
+
from django.utils.dateparse import parse_datetime
|
|
10
14
|
from django.utils.html import format_html
|
|
11
15
|
from django.urls import path
|
|
12
16
|
from django.http import HttpResponse, HttpResponseRedirect
|
|
@@ -14,6 +18,12 @@ from django.template.response import TemplateResponse
|
|
|
14
18
|
|
|
15
19
|
import uuid
|
|
16
20
|
from asgiref.sync import async_to_sync
|
|
21
|
+
import requests
|
|
22
|
+
from requests import RequestException
|
|
23
|
+
from cryptography.hazmat.primitives import hashes
|
|
24
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
25
|
+
from django.db import transaction
|
|
26
|
+
from django.core.exceptions import ValidationError
|
|
17
27
|
|
|
18
28
|
from .models import (
|
|
19
29
|
Charger,
|
|
@@ -23,6 +33,7 @@ from .models import (
|
|
|
23
33
|
Transaction,
|
|
24
34
|
Location,
|
|
25
35
|
DataTransferMessage,
|
|
36
|
+
CPReservation,
|
|
26
37
|
)
|
|
27
38
|
from .simulator import ChargePointSimulator
|
|
28
39
|
from . import store
|
|
@@ -34,6 +45,7 @@ from .status_display import STATUS_BADGE_MAP, ERROR_OK_VALUES
|
|
|
34
45
|
from .views import _charger_state, _live_sessions
|
|
35
46
|
from core.admin import SaveBeforeChangeAction
|
|
36
47
|
from core.user_data import EntityModelAdmin
|
|
48
|
+
from nodes.models import Node
|
|
37
49
|
|
|
38
50
|
|
|
39
51
|
class LocationAdminForm(forms.ModelForm):
|
|
@@ -66,6 +78,43 @@ class TransactionImportForm(forms.Form):
|
|
|
66
78
|
file = forms.FileField()
|
|
67
79
|
|
|
68
80
|
|
|
81
|
+
class CPReservationForm(forms.ModelForm):
|
|
82
|
+
class Meta:
|
|
83
|
+
model = CPReservation
|
|
84
|
+
fields = [
|
|
85
|
+
"location",
|
|
86
|
+
"account",
|
|
87
|
+
"rfid",
|
|
88
|
+
"id_tag",
|
|
89
|
+
"start_time",
|
|
90
|
+
"duration_minutes",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
def clean(self):
|
|
94
|
+
cleaned = super().clean()
|
|
95
|
+
instance = self.instance
|
|
96
|
+
for field in self.Meta.fields:
|
|
97
|
+
if field in cleaned:
|
|
98
|
+
setattr(instance, field, cleaned[field])
|
|
99
|
+
try:
|
|
100
|
+
instance.allocate_connector(force=bool(instance.pk))
|
|
101
|
+
except ValidationError as exc:
|
|
102
|
+
if exc.message_dict:
|
|
103
|
+
for field, errors in exc.message_dict.items():
|
|
104
|
+
for error in errors:
|
|
105
|
+
self.add_error(field, error)
|
|
106
|
+
raise forms.ValidationError(
|
|
107
|
+
_("Unable to allocate a connector for the selected time window.")
|
|
108
|
+
)
|
|
109
|
+
raise forms.ValidationError(exc.messages or [str(exc)])
|
|
110
|
+
if not instance.id_tag_value:
|
|
111
|
+
message = _("Select an RFID or provide an idTag for the reservation.")
|
|
112
|
+
self.add_error("id_tag", message)
|
|
113
|
+
self.add_error("rfid", message)
|
|
114
|
+
raise forms.ValidationError(message)
|
|
115
|
+
return cleaned
|
|
116
|
+
|
|
117
|
+
|
|
69
118
|
class LogViewAdminMixin:
|
|
70
119
|
"""Mixin providing an admin view to display charger or simulator logs."""
|
|
71
120
|
|
|
@@ -210,6 +259,7 @@ class LocationAdmin(EntityModelAdmin):
|
|
|
210
259
|
form = LocationAdminForm
|
|
211
260
|
list_display = ("name", "zone", "contract_type", "latitude", "longitude")
|
|
212
261
|
change_form_template = "admin/ocpp/location/change_form.html"
|
|
262
|
+
search_fields = ("name",)
|
|
213
263
|
|
|
214
264
|
|
|
215
265
|
@admin.register(DataTransferMessage)
|
|
@@ -250,8 +300,139 @@ class DataTransferMessageAdmin(admin.ModelAdmin):
|
|
|
250
300
|
)
|
|
251
301
|
|
|
252
302
|
|
|
303
|
+
@admin.register(CPReservation)
|
|
304
|
+
class CPReservationAdmin(EntityModelAdmin):
|
|
305
|
+
form = CPReservationForm
|
|
306
|
+
list_display = (
|
|
307
|
+
"location",
|
|
308
|
+
"connector_side_display",
|
|
309
|
+
"start_time",
|
|
310
|
+
"end_time_display",
|
|
311
|
+
"account",
|
|
312
|
+
"id_tag_display",
|
|
313
|
+
"evcs_status",
|
|
314
|
+
"evcs_confirmed",
|
|
315
|
+
)
|
|
316
|
+
list_filter = ("location", "evcs_confirmed")
|
|
317
|
+
search_fields = (
|
|
318
|
+
"location__name",
|
|
319
|
+
"connector__charger_id",
|
|
320
|
+
"connector__display_name",
|
|
321
|
+
"account__name",
|
|
322
|
+
"id_tag",
|
|
323
|
+
"rfid__rfid",
|
|
324
|
+
)
|
|
325
|
+
date_hierarchy = "start_time"
|
|
326
|
+
ordering = ("-start_time",)
|
|
327
|
+
autocomplete_fields = ("location", "account", "rfid")
|
|
328
|
+
readonly_fields = (
|
|
329
|
+
"connector_identity",
|
|
330
|
+
"connector_side_display",
|
|
331
|
+
"evcs_status",
|
|
332
|
+
"evcs_error",
|
|
333
|
+
"evcs_confirmed",
|
|
334
|
+
"evcs_confirmed_at",
|
|
335
|
+
"ocpp_message_id",
|
|
336
|
+
"created_on",
|
|
337
|
+
"updated_on",
|
|
338
|
+
)
|
|
339
|
+
fieldsets = (
|
|
340
|
+
(
|
|
341
|
+
None,
|
|
342
|
+
{
|
|
343
|
+
"fields": (
|
|
344
|
+
"location",
|
|
345
|
+
"account",
|
|
346
|
+
"rfid",
|
|
347
|
+
"id_tag",
|
|
348
|
+
"start_time",
|
|
349
|
+
"duration_minutes",
|
|
350
|
+
)
|
|
351
|
+
},
|
|
352
|
+
),
|
|
353
|
+
(
|
|
354
|
+
_("Assigned connector"),
|
|
355
|
+
{"fields": ("connector_identity", "connector_side_display")},
|
|
356
|
+
),
|
|
357
|
+
(
|
|
358
|
+
_("EVCS response"),
|
|
359
|
+
{
|
|
360
|
+
"fields": (
|
|
361
|
+
"evcs_confirmed",
|
|
362
|
+
"evcs_status",
|
|
363
|
+
"evcs_confirmed_at",
|
|
364
|
+
"evcs_error",
|
|
365
|
+
"ocpp_message_id",
|
|
366
|
+
)
|
|
367
|
+
},
|
|
368
|
+
),
|
|
369
|
+
(
|
|
370
|
+
_("Metadata"),
|
|
371
|
+
{"fields": ("created_on", "updated_on")},
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def save_model(self, request, obj, form, change):
|
|
376
|
+
trigger_fields = {
|
|
377
|
+
"start_time",
|
|
378
|
+
"duration_minutes",
|
|
379
|
+
"location",
|
|
380
|
+
"id_tag",
|
|
381
|
+
"rfid",
|
|
382
|
+
"account",
|
|
383
|
+
}
|
|
384
|
+
changed_data = set(getattr(form, "changed_data", []))
|
|
385
|
+
should_send = not change or bool(trigger_fields.intersection(changed_data))
|
|
386
|
+
with transaction.atomic():
|
|
387
|
+
super().save_model(request, obj, form, change)
|
|
388
|
+
if should_send:
|
|
389
|
+
try:
|
|
390
|
+
obj.send_reservation_request()
|
|
391
|
+
except ValidationError as exc:
|
|
392
|
+
raise ValidationError(exc.message_dict or exc.messages or str(exc))
|
|
393
|
+
else:
|
|
394
|
+
self.message_user(
|
|
395
|
+
request,
|
|
396
|
+
_("Reservation request sent to %(connector)s.")
|
|
397
|
+
% {"connector": self.connector_identity(obj)},
|
|
398
|
+
messages.SUCCESS,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
@admin.display(description=_("Connector"), ordering="connector__connector_id")
|
|
402
|
+
def connector_side_display(self, obj):
|
|
403
|
+
return obj.connector_label or "-"
|
|
404
|
+
|
|
405
|
+
@admin.display(description=_("Connector identity"))
|
|
406
|
+
def connector_identity(self, obj):
|
|
407
|
+
if obj.connector_id:
|
|
408
|
+
return obj.connector.identity_slug()
|
|
409
|
+
return "-"
|
|
410
|
+
|
|
411
|
+
@admin.display(description=_("End time"))
|
|
412
|
+
def end_time_display(self, obj):
|
|
413
|
+
try:
|
|
414
|
+
value = timezone.localtime(obj.end_time)
|
|
415
|
+
except Exception:
|
|
416
|
+
value = obj.end_time
|
|
417
|
+
if not value:
|
|
418
|
+
return "-"
|
|
419
|
+
return formats.date_format(value, "DATETIME_FORMAT")
|
|
420
|
+
|
|
421
|
+
@admin.display(description=_("Id tag"))
|
|
422
|
+
def id_tag_display(self, obj):
|
|
423
|
+
value = obj.id_tag_value
|
|
424
|
+
return value or "-"
|
|
425
|
+
|
|
426
|
+
|
|
253
427
|
@admin.register(Charger)
|
|
254
428
|
class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
429
|
+
_REMOTE_DATETIME_FIELDS = {
|
|
430
|
+
"availability_state_updated_at",
|
|
431
|
+
"availability_requested_at",
|
|
432
|
+
"availability_request_status_at",
|
|
433
|
+
"last_online_at",
|
|
434
|
+
}
|
|
435
|
+
|
|
255
436
|
fieldsets = (
|
|
256
437
|
(
|
|
257
438
|
"General",
|
|
@@ -306,6 +487,20 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
306
487
|
"Configuration",
|
|
307
488
|
{"fields": ("public_display", "require_rfid", "configuration")},
|
|
308
489
|
),
|
|
490
|
+
(
|
|
491
|
+
"Network",
|
|
492
|
+
{
|
|
493
|
+
"fields": (
|
|
494
|
+
"node_origin",
|
|
495
|
+
"manager_node",
|
|
496
|
+
"forwarded_to",
|
|
497
|
+
"forwarding_watermark",
|
|
498
|
+
"allow_remote",
|
|
499
|
+
"export_transactions",
|
|
500
|
+
"last_online_at",
|
|
501
|
+
)
|
|
502
|
+
},
|
|
503
|
+
),
|
|
309
504
|
(
|
|
310
505
|
"References",
|
|
311
506
|
{
|
|
@@ -334,15 +529,19 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
334
529
|
"availability_request_status_at",
|
|
335
530
|
"availability_request_details",
|
|
336
531
|
"configuration",
|
|
532
|
+
"forwarded_to",
|
|
533
|
+
"forwarding_watermark",
|
|
534
|
+
"last_online_at",
|
|
337
535
|
)
|
|
338
536
|
list_display = (
|
|
339
537
|
"display_name_with_fallback",
|
|
340
538
|
"connector_number",
|
|
341
539
|
"charger_name_display",
|
|
540
|
+
"local_indicator",
|
|
342
541
|
"require_rfid_display",
|
|
343
542
|
"public_display",
|
|
344
543
|
"last_heartbeat",
|
|
345
|
-
"
|
|
544
|
+
"today_kw",
|
|
346
545
|
"total_kw_display",
|
|
347
546
|
"page_link",
|
|
348
547
|
"log_link",
|
|
@@ -364,6 +563,159 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
364
563
|
"delete_selected",
|
|
365
564
|
]
|
|
366
565
|
|
|
566
|
+
def _prepare_remote_credentials(self, request):
|
|
567
|
+
local = Node.get_local()
|
|
568
|
+
if not local or not local.uuid:
|
|
569
|
+
self.message_user(
|
|
570
|
+
request,
|
|
571
|
+
"Local node is not registered; remote actions are unavailable.",
|
|
572
|
+
level=messages.ERROR,
|
|
573
|
+
)
|
|
574
|
+
return None, None
|
|
575
|
+
private_key = local.get_private_key()
|
|
576
|
+
if private_key is None:
|
|
577
|
+
self.message_user(
|
|
578
|
+
request,
|
|
579
|
+
"Local node private key is unavailable; remote actions are disabled.",
|
|
580
|
+
level=messages.ERROR,
|
|
581
|
+
)
|
|
582
|
+
return None, None
|
|
583
|
+
return local, private_key
|
|
584
|
+
|
|
585
|
+
def _call_remote_action(
|
|
586
|
+
self,
|
|
587
|
+
request,
|
|
588
|
+
local_node: Node,
|
|
589
|
+
private_key,
|
|
590
|
+
charger: Charger,
|
|
591
|
+
action: str,
|
|
592
|
+
extra: dict[str, Any] | None = None,
|
|
593
|
+
) -> tuple[bool, dict[str, Any]]:
|
|
594
|
+
if not charger.node_origin:
|
|
595
|
+
self.message_user(
|
|
596
|
+
request,
|
|
597
|
+
f"{charger}: remote node information is missing.",
|
|
598
|
+
level=messages.ERROR,
|
|
599
|
+
)
|
|
600
|
+
return False, {}
|
|
601
|
+
origin = charger.node_origin
|
|
602
|
+
if not origin.port:
|
|
603
|
+
self.message_user(
|
|
604
|
+
request,
|
|
605
|
+
f"{charger}: remote node port is not configured.",
|
|
606
|
+
level=messages.ERROR,
|
|
607
|
+
)
|
|
608
|
+
return False, {}
|
|
609
|
+
|
|
610
|
+
if not origin.get_remote_host_candidates():
|
|
611
|
+
self.message_user(
|
|
612
|
+
request,
|
|
613
|
+
f"{charger}: remote node connection details are incomplete.",
|
|
614
|
+
level=messages.ERROR,
|
|
615
|
+
)
|
|
616
|
+
return False, {}
|
|
617
|
+
|
|
618
|
+
payload: dict[str, Any] = {
|
|
619
|
+
"requester": str(local_node.uuid),
|
|
620
|
+
"requester_mac": local_node.mac_address,
|
|
621
|
+
"requester_public_key": local_node.public_key,
|
|
622
|
+
"charger_id": charger.charger_id,
|
|
623
|
+
"connector_id": charger.connector_id,
|
|
624
|
+
"action": action,
|
|
625
|
+
}
|
|
626
|
+
if extra:
|
|
627
|
+
payload.update(extra)
|
|
628
|
+
|
|
629
|
+
payload_json = json.dumps(payload, separators=(",", ":"), sort_keys=True)
|
|
630
|
+
headers = {"Content-Type": "application/json"}
|
|
631
|
+
try:
|
|
632
|
+
signature = private_key.sign(
|
|
633
|
+
payload_json.encode(),
|
|
634
|
+
padding.PKCS1v15(),
|
|
635
|
+
hashes.SHA256(),
|
|
636
|
+
)
|
|
637
|
+
headers["X-Signature"] = base64.b64encode(signature).decode()
|
|
638
|
+
except Exception:
|
|
639
|
+
self.message_user(
|
|
640
|
+
request,
|
|
641
|
+
"Unable to sign remote action payload; remote action aborted.",
|
|
642
|
+
level=messages.ERROR,
|
|
643
|
+
)
|
|
644
|
+
return False, {}
|
|
645
|
+
|
|
646
|
+
url = next(
|
|
647
|
+
origin.iter_remote_urls("/nodes/network/chargers/action/"),
|
|
648
|
+
"",
|
|
649
|
+
)
|
|
650
|
+
if not url:
|
|
651
|
+
self.message_user(
|
|
652
|
+
request,
|
|
653
|
+
f"{charger}: no reachable hosts were reported for the remote node.",
|
|
654
|
+
level=messages.ERROR,
|
|
655
|
+
)
|
|
656
|
+
return False, {}
|
|
657
|
+
try:
|
|
658
|
+
response = requests.post(url, data=payload_json, headers=headers, timeout=5)
|
|
659
|
+
except RequestException as exc:
|
|
660
|
+
self.message_user(
|
|
661
|
+
request,
|
|
662
|
+
f"{charger}: failed to contact remote node ({exc}).",
|
|
663
|
+
level=messages.ERROR,
|
|
664
|
+
)
|
|
665
|
+
return False, {}
|
|
666
|
+
|
|
667
|
+
try:
|
|
668
|
+
data = response.json()
|
|
669
|
+
except ValueError:
|
|
670
|
+
self.message_user(
|
|
671
|
+
request,
|
|
672
|
+
f"{charger}: invalid response from remote node.",
|
|
673
|
+
level=messages.ERROR,
|
|
674
|
+
)
|
|
675
|
+
return False, {}
|
|
676
|
+
|
|
677
|
+
if response.status_code != 200 or data.get("status") != "ok":
|
|
678
|
+
detail = data.get("detail") if isinstance(data, dict) else None
|
|
679
|
+
if not detail:
|
|
680
|
+
detail = response.text or "Remote node rejected the request."
|
|
681
|
+
self.message_user(
|
|
682
|
+
request,
|
|
683
|
+
f"{charger}: {detail}",
|
|
684
|
+
level=messages.ERROR,
|
|
685
|
+
)
|
|
686
|
+
return False, {}
|
|
687
|
+
|
|
688
|
+
updates = data.get("updates", {}) if isinstance(data, dict) else {}
|
|
689
|
+
if not isinstance(updates, dict):
|
|
690
|
+
updates = {}
|
|
691
|
+
return True, updates
|
|
692
|
+
|
|
693
|
+
def _apply_remote_updates(self, charger: Charger, updates: dict[str, Any]) -> None:
|
|
694
|
+
if not updates:
|
|
695
|
+
return
|
|
696
|
+
|
|
697
|
+
applied: dict[str, Any] = {}
|
|
698
|
+
for field, value in updates.items():
|
|
699
|
+
if field in self._REMOTE_DATETIME_FIELDS and isinstance(value, str):
|
|
700
|
+
parsed = parse_datetime(value)
|
|
701
|
+
if parsed and timezone.is_naive(parsed):
|
|
702
|
+
parsed = timezone.make_aware(parsed, timezone.get_current_timezone())
|
|
703
|
+
applied[field] = parsed
|
|
704
|
+
else:
|
|
705
|
+
applied[field] = value
|
|
706
|
+
|
|
707
|
+
Charger.objects.filter(pk=charger.pk).update(**applied)
|
|
708
|
+
for field, value in applied.items():
|
|
709
|
+
setattr(charger, field, value)
|
|
710
|
+
|
|
711
|
+
def get_readonly_fields(self, request, obj=None):
|
|
712
|
+
readonly = list(super().get_readonly_fields(request, obj))
|
|
713
|
+
if obj and not obj.is_local:
|
|
714
|
+
for field in ("allow_remote", "export_transactions"):
|
|
715
|
+
if field not in readonly:
|
|
716
|
+
readonly.append(field)
|
|
717
|
+
return tuple(readonly)
|
|
718
|
+
|
|
367
719
|
def get_view_on_site_url(self, obj=None):
|
|
368
720
|
return obj.get_absolute_url() if obj else None
|
|
369
721
|
|
|
@@ -462,6 +814,10 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
462
814
|
return obj.location.name
|
|
463
815
|
return obj.charger_id
|
|
464
816
|
|
|
817
|
+
@admin.display(boolean=True, description="Local")
|
|
818
|
+
def local_indicator(self, obj):
|
|
819
|
+
return obj.is_local
|
|
820
|
+
|
|
465
821
|
def location_name(self, obj):
|
|
466
822
|
return obj.location.name if obj.location else ""
|
|
467
823
|
|
|
@@ -534,51 +890,82 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
534
890
|
@admin.action(description="Fetch CP configuration")
|
|
535
891
|
def fetch_cp_configuration(self, request, queryset):
|
|
536
892
|
fetched = 0
|
|
893
|
+
local_node = None
|
|
894
|
+
private_key = None
|
|
895
|
+
remote_unavailable = False
|
|
537
896
|
for charger in queryset:
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
897
|
+
if charger.is_local:
|
|
898
|
+
connector_value = charger.connector_id
|
|
899
|
+
ws = store.get_connection(charger.charger_id, connector_value)
|
|
900
|
+
if ws is None:
|
|
901
|
+
self.message_user(
|
|
902
|
+
request,
|
|
903
|
+
f"{charger}: no active connection",
|
|
904
|
+
level=messages.ERROR,
|
|
905
|
+
)
|
|
906
|
+
continue
|
|
907
|
+
message_id = uuid.uuid4().hex
|
|
908
|
+
payload = {}
|
|
909
|
+
msg = json.dumps([2, message_id, "GetConfiguration", payload])
|
|
910
|
+
try:
|
|
911
|
+
async_to_sync(ws.send)(msg)
|
|
912
|
+
except Exception as exc: # pragma: no cover - network error
|
|
913
|
+
self.message_user(
|
|
914
|
+
request,
|
|
915
|
+
f"{charger}: failed to send GetConfiguration ({exc})",
|
|
916
|
+
level=messages.ERROR,
|
|
917
|
+
)
|
|
918
|
+
continue
|
|
919
|
+
log_key = store.identity_key(charger.charger_id, connector_value)
|
|
920
|
+
store.add_log(log_key, f"< {msg}", log_type="charger")
|
|
921
|
+
store.register_pending_call(
|
|
922
|
+
message_id,
|
|
923
|
+
{
|
|
924
|
+
"action": "GetConfiguration",
|
|
925
|
+
"charger_id": charger.charger_id,
|
|
926
|
+
"connector_id": connector_value,
|
|
927
|
+
"log_key": log_key,
|
|
928
|
+
"requested_at": timezone.now(),
|
|
929
|
+
},
|
|
545
930
|
)
|
|
931
|
+
store.schedule_call_timeout(
|
|
932
|
+
message_id,
|
|
933
|
+
timeout=5.0,
|
|
934
|
+
action="GetConfiguration",
|
|
935
|
+
log_key=log_key,
|
|
936
|
+
message=(
|
|
937
|
+
"GetConfiguration timed out: charger did not respond"
|
|
938
|
+
" (operation may not be supported)"
|
|
939
|
+
),
|
|
940
|
+
)
|
|
941
|
+
fetched += 1
|
|
546
942
|
continue
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
msg = json.dumps([2, message_id, "GetConfiguration", payload])
|
|
550
|
-
try:
|
|
551
|
-
async_to_sync(ws.send)(msg)
|
|
552
|
-
except Exception as exc: # pragma: no cover - network error
|
|
943
|
+
|
|
944
|
+
if not charger.allow_remote:
|
|
553
945
|
self.message_user(
|
|
554
946
|
request,
|
|
555
|
-
f"{charger}:
|
|
947
|
+
f"{charger}: remote administration is disabled.",
|
|
556
948
|
level=messages.ERROR,
|
|
557
949
|
)
|
|
558
950
|
continue
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
message_id,
|
|
573
|
-
timeout=5.0,
|
|
574
|
-
action="GetConfiguration",
|
|
575
|
-
log_key=log_key,
|
|
576
|
-
message=(
|
|
577
|
-
"GetConfiguration timed out: charger did not respond"
|
|
578
|
-
" (operation may not be supported)"
|
|
579
|
-
),
|
|
951
|
+
if remote_unavailable:
|
|
952
|
+
continue
|
|
953
|
+
if local_node is None:
|
|
954
|
+
local_node, private_key = self._prepare_remote_credentials(request)
|
|
955
|
+
if not local_node or not private_key:
|
|
956
|
+
remote_unavailable = True
|
|
957
|
+
continue
|
|
958
|
+
success, updates = self._call_remote_action(
|
|
959
|
+
request,
|
|
960
|
+
local_node,
|
|
961
|
+
private_key,
|
|
962
|
+
charger,
|
|
963
|
+
"get-configuration",
|
|
580
964
|
)
|
|
581
|
-
|
|
965
|
+
if success:
|
|
966
|
+
self._apply_remote_updates(charger, updates)
|
|
967
|
+
fetched += 1
|
|
968
|
+
|
|
582
969
|
if fetched:
|
|
583
970
|
self.message_user(
|
|
584
971
|
request,
|
|
@@ -589,14 +976,49 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
589
976
|
def toggle_rfid_authentication(self, request, queryset):
|
|
590
977
|
enabled = 0
|
|
591
978
|
disabled = 0
|
|
979
|
+
local_node = None
|
|
980
|
+
private_key = None
|
|
981
|
+
remote_unavailable = False
|
|
592
982
|
for charger in queryset:
|
|
593
983
|
new_value = not charger.require_rfid
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
984
|
+
if charger.is_local:
|
|
985
|
+
Charger.objects.filter(pk=charger.pk).update(require_rfid=new_value)
|
|
986
|
+
charger.require_rfid = new_value
|
|
987
|
+
if new_value:
|
|
988
|
+
enabled += 1
|
|
989
|
+
else:
|
|
990
|
+
disabled += 1
|
|
991
|
+
continue
|
|
992
|
+
|
|
993
|
+
if not charger.allow_remote:
|
|
994
|
+
self.message_user(
|
|
995
|
+
request,
|
|
996
|
+
f"{charger}: remote administration is disabled.",
|
|
997
|
+
level=messages.ERROR,
|
|
998
|
+
)
|
|
999
|
+
continue
|
|
1000
|
+
if remote_unavailable:
|
|
1001
|
+
continue
|
|
1002
|
+
if local_node is None:
|
|
1003
|
+
local_node, private_key = self._prepare_remote_credentials(request)
|
|
1004
|
+
if not local_node or not private_key:
|
|
1005
|
+
remote_unavailable = True
|
|
1006
|
+
continue
|
|
1007
|
+
success, updates = self._call_remote_action(
|
|
1008
|
+
request,
|
|
1009
|
+
local_node,
|
|
1010
|
+
private_key,
|
|
1011
|
+
charger,
|
|
1012
|
+
"toggle-rfid",
|
|
1013
|
+
{"enable": new_value},
|
|
1014
|
+
)
|
|
1015
|
+
if success:
|
|
1016
|
+
self._apply_remote_updates(charger, updates)
|
|
1017
|
+
if charger.require_rfid:
|
|
1018
|
+
enabled += 1
|
|
1019
|
+
else:
|
|
1020
|
+
disabled += 1
|
|
1021
|
+
|
|
600
1022
|
if enabled or disabled:
|
|
601
1023
|
changes = []
|
|
602
1024
|
if enabled:
|
|
@@ -611,53 +1033,85 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
611
1033
|
|
|
612
1034
|
def _dispatch_change_availability(self, request, queryset, availability_type: str):
|
|
613
1035
|
sent = 0
|
|
1036
|
+
local_node = None
|
|
1037
|
+
private_key = None
|
|
1038
|
+
remote_unavailable = False
|
|
614
1039
|
for charger in queryset:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1040
|
+
if charger.is_local:
|
|
1041
|
+
connector_value = charger.connector_id
|
|
1042
|
+
ws = store.get_connection(charger.charger_id, connector_value)
|
|
1043
|
+
if ws is None:
|
|
1044
|
+
self.message_user(
|
|
1045
|
+
request,
|
|
1046
|
+
f"{charger}: no active connection",
|
|
1047
|
+
level=messages.ERROR,
|
|
1048
|
+
)
|
|
1049
|
+
continue
|
|
1050
|
+
connector_id = connector_value if connector_value is not None else 0
|
|
1051
|
+
message_id = uuid.uuid4().hex
|
|
1052
|
+
payload = {"connectorId": connector_id, "type": availability_type}
|
|
1053
|
+
msg = json.dumps([2, message_id, "ChangeAvailability", payload])
|
|
1054
|
+
try:
|
|
1055
|
+
async_to_sync(ws.send)(msg)
|
|
1056
|
+
except Exception as exc: # pragma: no cover - network error
|
|
1057
|
+
self.message_user(
|
|
1058
|
+
request,
|
|
1059
|
+
f"{charger}: failed to send ChangeAvailability ({exc})",
|
|
1060
|
+
level=messages.ERROR,
|
|
1061
|
+
)
|
|
1062
|
+
continue
|
|
1063
|
+
log_key = store.identity_key(charger.charger_id, connector_value)
|
|
1064
|
+
store.add_log(log_key, f"< {msg}", log_type="charger")
|
|
1065
|
+
timestamp = timezone.now()
|
|
1066
|
+
store.register_pending_call(
|
|
1067
|
+
message_id,
|
|
1068
|
+
{
|
|
1069
|
+
"action": "ChangeAvailability",
|
|
1070
|
+
"charger_id": charger.charger_id,
|
|
1071
|
+
"connector_id": connector_value,
|
|
1072
|
+
"availability_type": availability_type,
|
|
1073
|
+
"requested_at": timestamp,
|
|
1074
|
+
},
|
|
622
1075
|
)
|
|
1076
|
+
updates = {
|
|
1077
|
+
"availability_requested_state": availability_type,
|
|
1078
|
+
"availability_requested_at": timestamp,
|
|
1079
|
+
"availability_request_status": "",
|
|
1080
|
+
"availability_request_status_at": None,
|
|
1081
|
+
"availability_request_details": "",
|
|
1082
|
+
}
|
|
1083
|
+
Charger.objects.filter(pk=charger.pk).update(**updates)
|
|
1084
|
+
for field, value in updates.items():
|
|
1085
|
+
setattr(charger, field, value)
|
|
1086
|
+
sent += 1
|
|
623
1087
|
continue
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
payload = {"connectorId": connector_id, "type": availability_type}
|
|
627
|
-
msg = json.dumps([2, message_id, "ChangeAvailability", payload])
|
|
628
|
-
try:
|
|
629
|
-
async_to_sync(ws.send)(msg)
|
|
630
|
-
except Exception as exc: # pragma: no cover - network error
|
|
1088
|
+
|
|
1089
|
+
if not charger.allow_remote:
|
|
631
1090
|
self.message_user(
|
|
632
1091
|
request,
|
|
633
|
-
f"{charger}:
|
|
1092
|
+
f"{charger}: remote administration is disabled.",
|
|
634
1093
|
level=messages.ERROR,
|
|
635
1094
|
)
|
|
636
1095
|
continue
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1096
|
+
if remote_unavailable:
|
|
1097
|
+
continue
|
|
1098
|
+
if local_node is None:
|
|
1099
|
+
local_node, private_key = self._prepare_remote_credentials(request)
|
|
1100
|
+
if not local_node or not private_key:
|
|
1101
|
+
remote_unavailable = True
|
|
1102
|
+
continue
|
|
1103
|
+
success, updates = self._call_remote_action(
|
|
1104
|
+
request,
|
|
1105
|
+
local_node,
|
|
1106
|
+
private_key,
|
|
1107
|
+
charger,
|
|
1108
|
+
"change-availability",
|
|
1109
|
+
{"availability_type": availability_type},
|
|
649
1110
|
)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
"availability_request_status_at": None,
|
|
655
|
-
"availability_request_details": "",
|
|
656
|
-
}
|
|
657
|
-
Charger.objects.filter(pk=charger.pk).update(**updates)
|
|
658
|
-
for field, value in updates.items():
|
|
659
|
-
setattr(charger, field, value)
|
|
660
|
-
sent += 1
|
|
1111
|
+
if success:
|
|
1112
|
+
self._apply_remote_updates(charger, updates)
|
|
1113
|
+
sent += 1
|
|
1114
|
+
|
|
661
1115
|
if sent:
|
|
662
1116
|
self.message_user(
|
|
663
1117
|
request,
|
|
@@ -675,17 +1129,49 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
675
1129
|
def _set_availability_state(
|
|
676
1130
|
self, request, queryset, availability_state: str
|
|
677
1131
|
) -> None:
|
|
678
|
-
timestamp = timezone.now()
|
|
679
1132
|
updated = 0
|
|
1133
|
+
local_node = None
|
|
1134
|
+
private_key = None
|
|
1135
|
+
remote_unavailable = False
|
|
680
1136
|
for charger in queryset:
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1137
|
+
if charger.is_local:
|
|
1138
|
+
timestamp = timezone.now()
|
|
1139
|
+
updates = {
|
|
1140
|
+
"availability_state": availability_state,
|
|
1141
|
+
"availability_state_updated_at": timestamp,
|
|
1142
|
+
}
|
|
1143
|
+
Charger.objects.filter(pk=charger.pk).update(**updates)
|
|
1144
|
+
for field, value in updates.items():
|
|
1145
|
+
setattr(charger, field, value)
|
|
1146
|
+
updated += 1
|
|
1147
|
+
continue
|
|
1148
|
+
|
|
1149
|
+
if not charger.allow_remote:
|
|
1150
|
+
self.message_user(
|
|
1151
|
+
request,
|
|
1152
|
+
f"{charger}: remote administration is disabled.",
|
|
1153
|
+
level=messages.ERROR,
|
|
1154
|
+
)
|
|
1155
|
+
continue
|
|
1156
|
+
if remote_unavailable:
|
|
1157
|
+
continue
|
|
1158
|
+
if local_node is None:
|
|
1159
|
+
local_node, private_key = self._prepare_remote_credentials(request)
|
|
1160
|
+
if not local_node or not private_key:
|
|
1161
|
+
remote_unavailable = True
|
|
1162
|
+
continue
|
|
1163
|
+
success, updates = self._call_remote_action(
|
|
1164
|
+
request,
|
|
1165
|
+
local_node,
|
|
1166
|
+
private_key,
|
|
1167
|
+
charger,
|
|
1168
|
+
"set-availability-state",
|
|
1169
|
+
{"availability_state": availability_state},
|
|
1170
|
+
)
|
|
1171
|
+
if success:
|
|
1172
|
+
self._apply_remote_updates(charger, updates)
|
|
1173
|
+
updated += 1
|
|
1174
|
+
|
|
689
1175
|
if updated:
|
|
690
1176
|
self.message_user(
|
|
691
1177
|
request,
|
|
@@ -703,55 +1189,86 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
703
1189
|
@admin.action(description="Remote stop active transaction")
|
|
704
1190
|
def remote_stop_transaction(self, request, queryset):
|
|
705
1191
|
stopped = 0
|
|
1192
|
+
local_node = None
|
|
1193
|
+
private_key = None
|
|
1194
|
+
remote_unavailable = False
|
|
706
1195
|
for charger in queryset:
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1196
|
+
if charger.is_local:
|
|
1197
|
+
connector_value = charger.connector_id
|
|
1198
|
+
ws = store.get_connection(charger.charger_id, connector_value)
|
|
1199
|
+
if ws is None:
|
|
1200
|
+
self.message_user(
|
|
1201
|
+
request,
|
|
1202
|
+
f"{charger}: no active connection",
|
|
1203
|
+
level=messages.ERROR,
|
|
1204
|
+
)
|
|
1205
|
+
continue
|
|
1206
|
+
tx_obj = store.get_transaction(charger.charger_id, connector_value)
|
|
1207
|
+
if tx_obj is None:
|
|
1208
|
+
self.message_user(
|
|
1209
|
+
request,
|
|
1210
|
+
f"{charger}: no active transaction",
|
|
1211
|
+
level=messages.ERROR,
|
|
1212
|
+
)
|
|
1213
|
+
continue
|
|
1214
|
+
message_id = uuid.uuid4().hex
|
|
1215
|
+
payload = {"transactionId": tx_obj.pk}
|
|
1216
|
+
msg = json.dumps([
|
|
1217
|
+
2,
|
|
1218
|
+
message_id,
|
|
1219
|
+
"RemoteStopTransaction",
|
|
1220
|
+
payload,
|
|
1221
|
+
])
|
|
1222
|
+
try:
|
|
1223
|
+
async_to_sync(ws.send)(msg)
|
|
1224
|
+
except Exception as exc: # pragma: no cover - network error
|
|
1225
|
+
self.message_user(
|
|
1226
|
+
request,
|
|
1227
|
+
f"{charger}: failed to send RemoteStopTransaction ({exc})",
|
|
1228
|
+
level=messages.ERROR,
|
|
1229
|
+
)
|
|
1230
|
+
continue
|
|
1231
|
+
log_key = store.identity_key(charger.charger_id, connector_value)
|
|
1232
|
+
store.add_log(log_key, f"< {msg}", log_type="charger")
|
|
1233
|
+
store.register_pending_call(
|
|
1234
|
+
message_id,
|
|
1235
|
+
{
|
|
1236
|
+
"action": "RemoteStopTransaction",
|
|
1237
|
+
"charger_id": charger.charger_id,
|
|
1238
|
+
"connector_id": connector_value,
|
|
1239
|
+
"transaction_id": tx_obj.pk,
|
|
1240
|
+
"log_key": log_key,
|
|
1241
|
+
"requested_at": timezone.now(),
|
|
1242
|
+
},
|
|
714
1243
|
)
|
|
1244
|
+
stopped += 1
|
|
715
1245
|
continue
|
|
716
|
-
|
|
717
|
-
if
|
|
1246
|
+
|
|
1247
|
+
if not charger.allow_remote:
|
|
718
1248
|
self.message_user(
|
|
719
1249
|
request,
|
|
720
|
-
f"{charger}:
|
|
1250
|
+
f"{charger}: remote administration is disabled.",
|
|
721
1251
|
level=messages.ERROR,
|
|
722
1252
|
)
|
|
723
1253
|
continue
|
|
724
|
-
|
|
725
|
-
payload = {"transactionId": tx_obj.pk}
|
|
726
|
-
msg = json.dumps([
|
|
727
|
-
2,
|
|
728
|
-
message_id,
|
|
729
|
-
"RemoteStopTransaction",
|
|
730
|
-
payload,
|
|
731
|
-
])
|
|
732
|
-
try:
|
|
733
|
-
async_to_sync(ws.send)(msg)
|
|
734
|
-
except Exception as exc: # pragma: no cover - network error
|
|
735
|
-
self.message_user(
|
|
736
|
-
request,
|
|
737
|
-
f"{charger}: failed to send RemoteStopTransaction ({exc})",
|
|
738
|
-
level=messages.ERROR,
|
|
739
|
-
)
|
|
1254
|
+
if remote_unavailable:
|
|
740
1255
|
continue
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
},
|
|
1256
|
+
if local_node is None:
|
|
1257
|
+
local_node, private_key = self._prepare_remote_credentials(request)
|
|
1258
|
+
if not local_node or not private_key:
|
|
1259
|
+
remote_unavailable = True
|
|
1260
|
+
continue
|
|
1261
|
+
success, updates = self._call_remote_action(
|
|
1262
|
+
request,
|
|
1263
|
+
local_node,
|
|
1264
|
+
private_key,
|
|
1265
|
+
charger,
|
|
1266
|
+
"remote-stop",
|
|
753
1267
|
)
|
|
754
|
-
|
|
1268
|
+
if success:
|
|
1269
|
+
self._apply_remote_updates(charger, updates)
|
|
1270
|
+
stopped += 1
|
|
1271
|
+
|
|
755
1272
|
if stopped:
|
|
756
1273
|
self.message_user(
|
|
757
1274
|
request,
|
|
@@ -761,56 +1278,95 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
761
1278
|
@admin.action(description="Reset charger (soft)")
|
|
762
1279
|
def reset_chargers(self, request, queryset):
|
|
763
1280
|
reset = 0
|
|
1281
|
+
local_node = None
|
|
1282
|
+
private_key = None
|
|
1283
|
+
remote_unavailable = False
|
|
764
1284
|
for charger in queryset:
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1285
|
+
if charger.is_local:
|
|
1286
|
+
connector_value = charger.connector_id
|
|
1287
|
+
ws = store.get_connection(charger.charger_id, connector_value)
|
|
1288
|
+
if ws is None:
|
|
1289
|
+
self.message_user(
|
|
1290
|
+
request,
|
|
1291
|
+
f"{charger}: no active connection",
|
|
1292
|
+
level=messages.ERROR,
|
|
1293
|
+
)
|
|
1294
|
+
continue
|
|
1295
|
+
tx_obj = store.get_transaction(charger.charger_id, connector_value)
|
|
1296
|
+
if tx_obj is not None:
|
|
1297
|
+
self.message_user(
|
|
1298
|
+
request,
|
|
1299
|
+
(
|
|
1300
|
+
f"{charger}: reset skipped because a session is active; "
|
|
1301
|
+
"stop the session first."
|
|
1302
|
+
),
|
|
1303
|
+
level=messages.WARNING,
|
|
1304
|
+
)
|
|
1305
|
+
continue
|
|
1306
|
+
message_id = uuid.uuid4().hex
|
|
1307
|
+
msg = json.dumps([
|
|
1308
|
+
2,
|
|
1309
|
+
message_id,
|
|
1310
|
+
"Reset",
|
|
1311
|
+
{"type": "Soft"},
|
|
1312
|
+
])
|
|
1313
|
+
try:
|
|
1314
|
+
async_to_sync(ws.send)(msg)
|
|
1315
|
+
except Exception as exc: # pragma: no cover - network error
|
|
1316
|
+
self.message_user(
|
|
1317
|
+
request,
|
|
1318
|
+
f"{charger}: failed to send Reset ({exc})",
|
|
1319
|
+
level=messages.ERROR,
|
|
1320
|
+
)
|
|
1321
|
+
continue
|
|
1322
|
+
log_key = store.identity_key(charger.charger_id, connector_value)
|
|
1323
|
+
store.add_log(log_key, f"< {msg}", log_type="charger")
|
|
1324
|
+
store.register_pending_call(
|
|
1325
|
+
message_id,
|
|
1326
|
+
{
|
|
1327
|
+
"action": "Reset",
|
|
1328
|
+
"charger_id": charger.charger_id,
|
|
1329
|
+
"connector_id": connector_value,
|
|
1330
|
+
"log_key": log_key,
|
|
1331
|
+
"requested_at": timezone.now(),
|
|
1332
|
+
},
|
|
772
1333
|
)
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
f"{charger}: reset skipped because a session is active; "
|
|
780
|
-
"stop the session first."
|
|
781
|
-
),
|
|
782
|
-
level=messages.WARNING,
|
|
1334
|
+
store.schedule_call_timeout(
|
|
1335
|
+
message_id,
|
|
1336
|
+
timeout=5.0,
|
|
1337
|
+
action="Reset",
|
|
1338
|
+
log_key=log_key,
|
|
1339
|
+
message="Reset timed out: charger did not respond",
|
|
783
1340
|
)
|
|
1341
|
+
reset += 1
|
|
784
1342
|
continue
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
2,
|
|
788
|
-
message_id,
|
|
789
|
-
"Reset",
|
|
790
|
-
{"type": "Soft"},
|
|
791
|
-
])
|
|
792
|
-
try:
|
|
793
|
-
async_to_sync(ws.send)(msg)
|
|
794
|
-
except Exception as exc: # pragma: no cover - network error
|
|
1343
|
+
|
|
1344
|
+
if not charger.allow_remote:
|
|
795
1345
|
self.message_user(
|
|
796
1346
|
request,
|
|
797
|
-
f"{charger}:
|
|
1347
|
+
f"{charger}: remote administration is disabled.",
|
|
798
1348
|
level=messages.ERROR,
|
|
799
1349
|
)
|
|
800
1350
|
continue
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1351
|
+
if remote_unavailable:
|
|
1352
|
+
continue
|
|
1353
|
+
if local_node is None:
|
|
1354
|
+
local_node, private_key = self._prepare_remote_credentials(request)
|
|
1355
|
+
if not local_node or not private_key:
|
|
1356
|
+
remote_unavailable = True
|
|
1357
|
+
continue
|
|
1358
|
+
success, updates = self._call_remote_action(
|
|
1359
|
+
request,
|
|
1360
|
+
local_node,
|
|
1361
|
+
private_key,
|
|
1362
|
+
charger,
|
|
1363
|
+
"reset",
|
|
1364
|
+
{"reset_type": "Soft"},
|
|
812
1365
|
)
|
|
813
|
-
|
|
1366
|
+
if success:
|
|
1367
|
+
self._apply_remote_updates(charger, updates)
|
|
1368
|
+
reset += 1
|
|
1369
|
+
|
|
814
1370
|
if reset:
|
|
815
1371
|
self.message_user(
|
|
816
1372
|
request,
|
|
@@ -826,13 +1382,11 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
826
1382
|
|
|
827
1383
|
total_kw_display.short_description = "Total kW"
|
|
828
1384
|
|
|
829
|
-
def
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return round(tx.kw, 2)
|
|
833
|
-
return 0.0
|
|
1385
|
+
def today_kw(self, obj):
|
|
1386
|
+
start, end = self._today_range()
|
|
1387
|
+
return round(obj.total_kw_for_range(start, end), 2)
|
|
834
1388
|
|
|
835
|
-
|
|
1389
|
+
today_kw.short_description = "Today kW"
|
|
836
1390
|
|
|
837
1391
|
def changelist_view(self, request, extra_context=None):
|
|
838
1392
|
response = super().changelist_view(request, extra_context=extra_context)
|