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
nodes/utils.py
CHANGED
|
@@ -2,6 +2,8 @@ from datetime import datetime
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
import hashlib
|
|
4
4
|
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
5
7
|
|
|
6
8
|
from django.conf import settings
|
|
7
9
|
from selenium import webdriver
|
|
@@ -11,6 +13,7 @@ from selenium.common.exceptions import WebDriverException
|
|
|
11
13
|
from .models import ContentSample
|
|
12
14
|
|
|
13
15
|
SCREENSHOT_DIR = settings.LOG_DIR / "screenshots"
|
|
16
|
+
CAMERA_DIR = settings.LOG_DIR / "camera"
|
|
14
17
|
logger = logging.getLogger(__name__)
|
|
15
18
|
|
|
16
19
|
|
|
@@ -46,6 +49,35 @@ def capture_screenshot(url: str, cookies=None) -> Path:
|
|
|
46
49
|
raise RuntimeError(f"Screenshot capture failed: {exc}") from exc
|
|
47
50
|
|
|
48
51
|
|
|
52
|
+
def capture_rpi_snapshot(timeout: int = 10) -> Path:
|
|
53
|
+
"""Capture a snapshot using the Raspberry Pi camera stack."""
|
|
54
|
+
|
|
55
|
+
tool_path = shutil.which("rpicam-still")
|
|
56
|
+
if not tool_path:
|
|
57
|
+
raise RuntimeError("rpicam-still is not available")
|
|
58
|
+
CAMERA_DIR.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
filename = CAMERA_DIR / f"{datetime.utcnow():%Y%m%d%H%M%S}.jpg"
|
|
60
|
+
try:
|
|
61
|
+
result = subprocess.run(
|
|
62
|
+
[tool_path, "-o", str(filename), "-t", "1"],
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
check=False,
|
|
66
|
+
timeout=timeout,
|
|
67
|
+
)
|
|
68
|
+
except Exception as exc: # pragma: no cover - depends on camera stack
|
|
69
|
+
logger.error("Failed to invoke %s: %s", tool_path, exc)
|
|
70
|
+
raise RuntimeError(f"Snapshot capture failed: {exc}") from exc
|
|
71
|
+
if result.returncode != 0:
|
|
72
|
+
error = (result.stderr or result.stdout or "Snapshot capture failed").strip()
|
|
73
|
+
logger.error("rpicam-still exited with %s: %s", result.returncode, error)
|
|
74
|
+
raise RuntimeError(error)
|
|
75
|
+
if not filename.exists():
|
|
76
|
+
logger.error("Snapshot file %s was not created", filename)
|
|
77
|
+
raise RuntimeError("Snapshot capture failed")
|
|
78
|
+
return filename
|
|
79
|
+
|
|
80
|
+
|
|
49
81
|
def save_screenshot(path: Path, node=None, method: str = "", transaction_uuid=None):
|
|
50
82
|
"""Save screenshot file info if not already recorded.
|
|
51
83
|
|
nodes/views.py
CHANGED
|
@@ -204,6 +204,15 @@ def register_node(request):
|
|
|
204
204
|
signature = data.get("signature")
|
|
205
205
|
installed_version = data.get("installed_version")
|
|
206
206
|
installed_revision = data.get("installed_revision")
|
|
207
|
+
relation_present = False
|
|
208
|
+
if hasattr(data, "getlist"):
|
|
209
|
+
relation_present = "current_relation" in data
|
|
210
|
+
else:
|
|
211
|
+
relation_present = "current_relation" in data
|
|
212
|
+
raw_relation = data.get("current_relation")
|
|
213
|
+
relation_value = (
|
|
214
|
+
Node.normalize_relation(raw_relation) if relation_present else None
|
|
215
|
+
)
|
|
207
216
|
|
|
208
217
|
if not hostname or not address or not mac_address:
|
|
209
218
|
response = JsonResponse(
|
|
@@ -242,6 +251,8 @@ def register_node(request):
|
|
|
242
251
|
defaults["installed_version"] = str(installed_version)[:20]
|
|
243
252
|
if installed_revision is not None:
|
|
244
253
|
defaults["installed_revision"] = str(installed_revision)[:40]
|
|
254
|
+
if relation_value is not None:
|
|
255
|
+
defaults["current_relation"] = relation_value
|
|
245
256
|
|
|
246
257
|
node, created = Node.objects.get_or_create(
|
|
247
258
|
mac_address=mac_address,
|
|
@@ -265,6 +276,9 @@ def register_node(request):
|
|
|
265
276
|
node.installed_revision = str(installed_revision)[:40]
|
|
266
277
|
if "installed_revision" not in update_fields:
|
|
267
278
|
update_fields.append("installed_revision")
|
|
279
|
+
if relation_value is not None and node.current_relation != relation_value:
|
|
280
|
+
node.current_relation = relation_value
|
|
281
|
+
update_fields.append("current_relation")
|
|
268
282
|
node.save(update_fields=update_fields)
|
|
269
283
|
current_version = (node.installed_version or "").strip()
|
|
270
284
|
current_revision = (node.installed_revision or "").strip()
|
ocpp/admin.py
CHANGED
|
@@ -11,12 +11,16 @@ from django.urls import path
|
|
|
11
11
|
from django.http import HttpResponse, HttpResponseRedirect
|
|
12
12
|
from django.template.response import TemplateResponse
|
|
13
13
|
|
|
14
|
+
import uuid
|
|
15
|
+
from asgiref.sync import async_to_sync
|
|
16
|
+
|
|
14
17
|
from .models import (
|
|
15
18
|
Charger,
|
|
16
19
|
Simulator,
|
|
17
20
|
MeterValue,
|
|
18
21
|
Transaction,
|
|
19
22
|
Location,
|
|
23
|
+
DataTransferMessage,
|
|
20
24
|
)
|
|
21
25
|
from .simulator import ChargePointSimulator
|
|
22
26
|
from . import store
|
|
@@ -108,6 +112,44 @@ class LocationAdmin(EntityModelAdmin):
|
|
|
108
112
|
change_form_template = "admin/ocpp/location/change_form.html"
|
|
109
113
|
|
|
110
114
|
|
|
115
|
+
@admin.register(DataTransferMessage)
|
|
116
|
+
class DataTransferMessageAdmin(admin.ModelAdmin):
|
|
117
|
+
list_display = (
|
|
118
|
+
"charger",
|
|
119
|
+
"connector_id",
|
|
120
|
+
"direction",
|
|
121
|
+
"vendor_id",
|
|
122
|
+
"message_id",
|
|
123
|
+
"status",
|
|
124
|
+
"created_at",
|
|
125
|
+
"responded_at",
|
|
126
|
+
)
|
|
127
|
+
list_filter = ("direction", "status")
|
|
128
|
+
search_fields = (
|
|
129
|
+
"charger__charger_id",
|
|
130
|
+
"ocpp_message_id",
|
|
131
|
+
"vendor_id",
|
|
132
|
+
"message_id",
|
|
133
|
+
)
|
|
134
|
+
readonly_fields = (
|
|
135
|
+
"charger",
|
|
136
|
+
"connector_id",
|
|
137
|
+
"direction",
|
|
138
|
+
"ocpp_message_id",
|
|
139
|
+
"vendor_id",
|
|
140
|
+
"message_id",
|
|
141
|
+
"payload",
|
|
142
|
+
"status",
|
|
143
|
+
"response_data",
|
|
144
|
+
"error_code",
|
|
145
|
+
"error_description",
|
|
146
|
+
"error_details",
|
|
147
|
+
"responded_at",
|
|
148
|
+
"created_at",
|
|
149
|
+
"updated_at",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
111
153
|
@admin.register(Charger)
|
|
112
154
|
class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
113
155
|
fieldsets = (
|
|
@@ -122,6 +164,13 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
122
164
|
"last_path",
|
|
123
165
|
"last_heartbeat",
|
|
124
166
|
"last_meter_values",
|
|
167
|
+
)
|
|
168
|
+
},
|
|
169
|
+
),
|
|
170
|
+
(
|
|
171
|
+
"Firmware",
|
|
172
|
+
{
|
|
173
|
+
"fields": (
|
|
125
174
|
"firmware_status",
|
|
126
175
|
"firmware_status_info",
|
|
127
176
|
"firmware_timestamp",
|
|
@@ -138,6 +187,20 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
138
187
|
)
|
|
139
188
|
},
|
|
140
189
|
),
|
|
190
|
+
(
|
|
191
|
+
"Availability",
|
|
192
|
+
{
|
|
193
|
+
"fields": (
|
|
194
|
+
"availability_state",
|
|
195
|
+
"availability_state_updated_at",
|
|
196
|
+
"availability_requested_state",
|
|
197
|
+
"availability_requested_at",
|
|
198
|
+
"availability_request_status",
|
|
199
|
+
"availability_request_status_at",
|
|
200
|
+
"availability_request_details",
|
|
201
|
+
)
|
|
202
|
+
},
|
|
203
|
+
),
|
|
141
204
|
(
|
|
142
205
|
"Configuration",
|
|
143
206
|
{"fields": ("public_display", "require_rfid")},
|
|
@@ -148,6 +211,13 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
148
211
|
"fields": ("reference",),
|
|
149
212
|
},
|
|
150
213
|
),
|
|
214
|
+
(
|
|
215
|
+
"Owner",
|
|
216
|
+
{
|
|
217
|
+
"fields": ("owner_users", "owner_groups"),
|
|
218
|
+
"classes": ("collapse",),
|
|
219
|
+
},
|
|
220
|
+
),
|
|
151
221
|
)
|
|
152
222
|
readonly_fields = (
|
|
153
223
|
"last_heartbeat",
|
|
@@ -155,16 +225,21 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
155
225
|
"firmware_status",
|
|
156
226
|
"firmware_status_info",
|
|
157
227
|
"firmware_timestamp",
|
|
228
|
+
"availability_state",
|
|
229
|
+
"availability_state_updated_at",
|
|
230
|
+
"availability_requested_state",
|
|
231
|
+
"availability_requested_at",
|
|
232
|
+
"availability_request_status",
|
|
233
|
+
"availability_request_status_at",
|
|
234
|
+
"availability_request_details",
|
|
158
235
|
)
|
|
159
236
|
list_display = (
|
|
160
237
|
"charger_id",
|
|
161
|
-
"
|
|
238
|
+
"connector_number",
|
|
162
239
|
"location_name",
|
|
163
240
|
"require_rfid_display",
|
|
164
241
|
"public_display",
|
|
165
242
|
"last_heartbeat",
|
|
166
|
-
"firmware_status",
|
|
167
|
-
"firmware_timestamp",
|
|
168
243
|
"session_kw",
|
|
169
244
|
"total_kw_display",
|
|
170
245
|
"page_link",
|
|
@@ -172,7 +247,16 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
172
247
|
"status_link",
|
|
173
248
|
)
|
|
174
249
|
search_fields = ("charger_id", "connector_id", "location__name")
|
|
175
|
-
|
|
250
|
+
filter_horizontal = ("owner_users", "owner_groups")
|
|
251
|
+
actions = [
|
|
252
|
+
"purge_data",
|
|
253
|
+
"fetch_cp_configuration",
|
|
254
|
+
"change_availability_operative",
|
|
255
|
+
"change_availability_inoperative",
|
|
256
|
+
"set_availability_state_operative",
|
|
257
|
+
"set_availability_state_inoperative",
|
|
258
|
+
"delete_selected",
|
|
259
|
+
]
|
|
176
260
|
|
|
177
261
|
def get_view_on_site_url(self, obj=None):
|
|
178
262
|
return obj.get_absolute_url() if obj else None
|
|
@@ -215,6 +299,12 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
215
299
|
def get_log_identifier(self, obj):
|
|
216
300
|
return store.identity_key(obj.charger_id, obj.connector_id)
|
|
217
301
|
|
|
302
|
+
def connector_number(self, obj):
|
|
303
|
+
return obj.connector_id if obj.connector_id is not None else ""
|
|
304
|
+
|
|
305
|
+
connector_number.short_description = "#"
|
|
306
|
+
connector_number.admin_order_field = "connector_id"
|
|
307
|
+
|
|
218
308
|
def status_link(self, obj):
|
|
219
309
|
from django.utils.html import format_html
|
|
220
310
|
from django.urls import reverse
|
|
@@ -223,7 +313,8 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
223
313
|
"charger-status-connector",
|
|
224
314
|
args=[obj.charger_id, obj.connector_slug],
|
|
225
315
|
)
|
|
226
|
-
|
|
316
|
+
label = (obj.last_status or "status").strip() or "status"
|
|
317
|
+
return format_html('<a href="{}" target="_blank">{}</a>', url, label)
|
|
227
318
|
|
|
228
319
|
status_link.short_description = "Status"
|
|
229
320
|
|
|
@@ -239,6 +330,151 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
239
330
|
|
|
240
331
|
purge_data.short_description = "Purge data"
|
|
241
332
|
|
|
333
|
+
@admin.action(description="Fetch CP configuration")
|
|
334
|
+
def fetch_cp_configuration(self, request, queryset):
|
|
335
|
+
fetched = 0
|
|
336
|
+
for charger in queryset:
|
|
337
|
+
connector_value = charger.connector_id
|
|
338
|
+
ws = store.get_connection(charger.charger_id, connector_value)
|
|
339
|
+
if ws is None:
|
|
340
|
+
self.message_user(
|
|
341
|
+
request,
|
|
342
|
+
f"{charger}: no active connection",
|
|
343
|
+
level=messages.ERROR,
|
|
344
|
+
)
|
|
345
|
+
continue
|
|
346
|
+
message_id = uuid.uuid4().hex
|
|
347
|
+
payload = {}
|
|
348
|
+
msg = json.dumps([2, message_id, "GetConfiguration", payload])
|
|
349
|
+
try:
|
|
350
|
+
async_to_sync(ws.send)(msg)
|
|
351
|
+
except Exception as exc: # pragma: no cover - network error
|
|
352
|
+
self.message_user(
|
|
353
|
+
request,
|
|
354
|
+
f"{charger}: failed to send GetConfiguration ({exc})",
|
|
355
|
+
level=messages.ERROR,
|
|
356
|
+
)
|
|
357
|
+
continue
|
|
358
|
+
log_key = store.identity_key(charger.charger_id, connector_value)
|
|
359
|
+
store.add_log(log_key, f"< {msg}", log_type="charger")
|
|
360
|
+
store.register_pending_call(
|
|
361
|
+
message_id,
|
|
362
|
+
{
|
|
363
|
+
"action": "GetConfiguration",
|
|
364
|
+
"charger_id": charger.charger_id,
|
|
365
|
+
"connector_id": connector_value,
|
|
366
|
+
"log_key": log_key,
|
|
367
|
+
"requested_at": timezone.now(),
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
store.schedule_call_timeout(
|
|
371
|
+
message_id,
|
|
372
|
+
timeout=5.0,
|
|
373
|
+
action="GetConfiguration",
|
|
374
|
+
log_key=log_key,
|
|
375
|
+
message=(
|
|
376
|
+
"GetConfiguration timed out: charger did not respond"
|
|
377
|
+
" (operation may not be supported)"
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
fetched += 1
|
|
381
|
+
if fetched:
|
|
382
|
+
self.message_user(
|
|
383
|
+
request,
|
|
384
|
+
f"Requested configuration from {fetched} charger(s)",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
def _dispatch_change_availability(self, request, queryset, availability_type: str):
|
|
388
|
+
sent = 0
|
|
389
|
+
for charger in queryset:
|
|
390
|
+
connector_value = charger.connector_id
|
|
391
|
+
ws = store.get_connection(charger.charger_id, connector_value)
|
|
392
|
+
if ws is None:
|
|
393
|
+
self.message_user(
|
|
394
|
+
request,
|
|
395
|
+
f"{charger}: no active connection",
|
|
396
|
+
level=messages.ERROR,
|
|
397
|
+
)
|
|
398
|
+
continue
|
|
399
|
+
connector_id = connector_value if connector_value is not None else 0
|
|
400
|
+
message_id = uuid.uuid4().hex
|
|
401
|
+
payload = {"connectorId": connector_id, "type": availability_type}
|
|
402
|
+
msg = json.dumps([2, message_id, "ChangeAvailability", payload])
|
|
403
|
+
try:
|
|
404
|
+
async_to_sync(ws.send)(msg)
|
|
405
|
+
except Exception as exc: # pragma: no cover - network error
|
|
406
|
+
self.message_user(
|
|
407
|
+
request,
|
|
408
|
+
f"{charger}: failed to send ChangeAvailability ({exc})",
|
|
409
|
+
level=messages.ERROR,
|
|
410
|
+
)
|
|
411
|
+
continue
|
|
412
|
+
log_key = store.identity_key(charger.charger_id, connector_value)
|
|
413
|
+
store.add_log(log_key, f"< {msg}", log_type="charger")
|
|
414
|
+
timestamp = timezone.now()
|
|
415
|
+
store.register_pending_call(
|
|
416
|
+
message_id,
|
|
417
|
+
{
|
|
418
|
+
"action": "ChangeAvailability",
|
|
419
|
+
"charger_id": charger.charger_id,
|
|
420
|
+
"connector_id": connector_value,
|
|
421
|
+
"availability_type": availability_type,
|
|
422
|
+
"requested_at": timestamp,
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
updates = {
|
|
426
|
+
"availability_requested_state": availability_type,
|
|
427
|
+
"availability_requested_at": timestamp,
|
|
428
|
+
"availability_request_status": "",
|
|
429
|
+
"availability_request_status_at": None,
|
|
430
|
+
"availability_request_details": "",
|
|
431
|
+
}
|
|
432
|
+
Charger.objects.filter(pk=charger.pk).update(**updates)
|
|
433
|
+
for field, value in updates.items():
|
|
434
|
+
setattr(charger, field, value)
|
|
435
|
+
sent += 1
|
|
436
|
+
if sent:
|
|
437
|
+
self.message_user(
|
|
438
|
+
request,
|
|
439
|
+
f"Sent ChangeAvailability ({availability_type}) to {sent} charger(s)",
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
@admin.action(description="Set availability to Operative")
|
|
443
|
+
def change_availability_operative(self, request, queryset):
|
|
444
|
+
self._dispatch_change_availability(request, queryset, "Operative")
|
|
445
|
+
|
|
446
|
+
@admin.action(description="Set availability to Inoperative")
|
|
447
|
+
def change_availability_inoperative(self, request, queryset):
|
|
448
|
+
self._dispatch_change_availability(request, queryset, "Inoperative")
|
|
449
|
+
|
|
450
|
+
def _set_availability_state(
|
|
451
|
+
self, request, queryset, availability_state: str
|
|
452
|
+
) -> None:
|
|
453
|
+
timestamp = timezone.now()
|
|
454
|
+
updated = 0
|
|
455
|
+
for charger in queryset:
|
|
456
|
+
updates = {
|
|
457
|
+
"availability_state": availability_state,
|
|
458
|
+
"availability_state_updated_at": timestamp,
|
|
459
|
+
}
|
|
460
|
+
Charger.objects.filter(pk=charger.pk).update(**updates)
|
|
461
|
+
for field, value in updates.items():
|
|
462
|
+
setattr(charger, field, value)
|
|
463
|
+
updated += 1
|
|
464
|
+
if updated:
|
|
465
|
+
self.message_user(
|
|
466
|
+
request,
|
|
467
|
+
f"Updated availability to {availability_state} for {updated} charger(s)",
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
@admin.action(description="Mark availability as Operative")
|
|
471
|
+
def set_availability_state_operative(self, request, queryset):
|
|
472
|
+
self._set_availability_state(request, queryset, "Operative")
|
|
473
|
+
|
|
474
|
+
@admin.action(description="Mark availability as Inoperative")
|
|
475
|
+
def set_availability_state_inoperative(self, request, queryset):
|
|
476
|
+
self._set_availability_state(request, queryset, "Inoperative")
|
|
477
|
+
|
|
242
478
|
def delete_queryset(self, request, queryset):
|
|
243
479
|
for obj in queryset:
|
|
244
480
|
obj.delete()
|
|
@@ -270,15 +506,36 @@ class SimulatorAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
270
506
|
"running",
|
|
271
507
|
"log_link",
|
|
272
508
|
)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
509
|
+
fieldsets = (
|
|
510
|
+
(
|
|
511
|
+
None,
|
|
512
|
+
{
|
|
513
|
+
"fields": (
|
|
514
|
+
"name",
|
|
515
|
+
"cp_path",
|
|
516
|
+
("host", "ws_port"),
|
|
517
|
+
"rfid",
|
|
518
|
+
("duration", "interval", "pre_charge_delay"),
|
|
519
|
+
"kw_max",
|
|
520
|
+
("repeat", "door_open"),
|
|
521
|
+
("username", "password"),
|
|
522
|
+
)
|
|
523
|
+
},
|
|
524
|
+
),
|
|
525
|
+
(
|
|
526
|
+
"Configuration",
|
|
527
|
+
{
|
|
528
|
+
"fields": (
|
|
529
|
+
"configuration_keys",
|
|
530
|
+
"configuration_unknown_keys",
|
|
531
|
+
),
|
|
532
|
+
"classes": ("collapse",),
|
|
533
|
+
"description": (
|
|
534
|
+
"Provide JSON lists for configurationKey entries and "
|
|
535
|
+
"unknownKey values returned by GetConfiguration."
|
|
536
|
+
),
|
|
537
|
+
},
|
|
538
|
+
),
|
|
282
539
|
)
|
|
283
540
|
actions = ("start_simulator", "stop_simulator", "send_open_door")
|
|
284
541
|
|
|
@@ -382,7 +639,13 @@ class SimulatorAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
382
639
|
if sim:
|
|
383
640
|
await sim.stop()
|
|
384
641
|
|
|
385
|
-
|
|
642
|
+
objs = list(queryset)
|
|
643
|
+
try:
|
|
644
|
+
loop = asyncio.get_running_loop()
|
|
645
|
+
except RuntimeError:
|
|
646
|
+
asyncio.run(_stop(objs))
|
|
647
|
+
else:
|
|
648
|
+
loop.create_task(_stop(objs))
|
|
386
649
|
self.message_user(request, "Stopping simulators")
|
|
387
650
|
|
|
388
651
|
stop_simulator.short_description = "Stop selected simulators"
|