arthexis 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arthexis might be problematic. Click here for more details.

Files changed (84) hide show
  1. {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/METADATA +87 -6
  2. arthexis-0.1.10.dist-info/RECORD +95 -0
  3. arthexis-0.1.10.dist-info/licenses/LICENSE +674 -0
  4. config/__init__.py +0 -1
  5. config/auth_app.py +0 -1
  6. config/celery.py +1 -2
  7. config/context_processors.py +1 -1
  8. config/offline.py +2 -0
  9. config/settings.py +352 -37
  10. config/urls.py +71 -6
  11. core/admin.py +1601 -200
  12. core/admin_history.py +50 -0
  13. core/admindocs.py +108 -1
  14. core/apps.py +161 -3
  15. core/auto_upgrade.py +57 -0
  16. core/backends.py +123 -7
  17. core/entity.py +62 -48
  18. core/fields.py +98 -0
  19. core/github_helper.py +25 -0
  20. core/github_issues.py +172 -0
  21. core/lcd_screen.py +1 -0
  22. core/liveupdate.py +25 -0
  23. core/log_paths.py +100 -0
  24. core/mailer.py +83 -0
  25. core/middleware.py +57 -0
  26. core/models.py +1279 -267
  27. core/notifications.py +11 -1
  28. core/public_wifi.py +227 -0
  29. core/reference_utils.py +97 -0
  30. core/release.py +27 -20
  31. core/sigil_builder.py +144 -0
  32. core/sigil_context.py +20 -0
  33. core/sigil_resolver.py +284 -0
  34. core/system.py +162 -29
  35. core/tasks.py +269 -27
  36. core/test_system_info.py +59 -1
  37. core/tests.py +644 -73
  38. core/tests_liveupdate.py +17 -0
  39. core/urls.py +2 -2
  40. core/user_data.py +425 -168
  41. core/views.py +627 -59
  42. core/widgets.py +51 -0
  43. core/workgroup_urls.py +7 -3
  44. core/workgroup_views.py +43 -6
  45. nodes/actions.py +0 -2
  46. nodes/admin.py +168 -285
  47. nodes/apps.py +9 -15
  48. nodes/backends.py +145 -0
  49. nodes/lcd.py +24 -10
  50. nodes/models.py +579 -179
  51. nodes/tasks.py +1 -5
  52. nodes/tests.py +894 -130
  53. nodes/utils.py +13 -2
  54. nodes/views.py +204 -28
  55. ocpp/admin.py +212 -63
  56. ocpp/apps.py +1 -1
  57. ocpp/consumers.py +642 -68
  58. ocpp/evcs.py +30 -10
  59. ocpp/models.py +452 -70
  60. ocpp/simulator.py +75 -11
  61. ocpp/store.py +288 -30
  62. ocpp/tasks.py +11 -7
  63. ocpp/test_export_import.py +8 -7
  64. ocpp/test_rfid.py +211 -16
  65. ocpp/tests.py +1576 -137
  66. ocpp/transactions_io.py +68 -22
  67. ocpp/urls.py +35 -2
  68. ocpp/views.py +701 -123
  69. pages/admin.py +173 -13
  70. pages/checks.py +0 -1
  71. pages/context_processors.py +39 -6
  72. pages/forms.py +131 -0
  73. pages/middleware.py +153 -0
  74. pages/models.py +37 -9
  75. pages/tests.py +1182 -42
  76. pages/urls.py +4 -0
  77. pages/utils.py +0 -1
  78. pages/views.py +844 -51
  79. arthexis-0.1.8.dist-info/RECORD +0 -80
  80. arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
  81. config/workgroup_app.py +0 -7
  82. core/checks.py +0 -29
  83. {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
  84. {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
ocpp/admin.py CHANGED
@@ -1,10 +1,11 @@
1
- from django.contrib import admin
1
+ from django.contrib import admin, messages
2
2
  from django import forms
3
3
 
4
4
  import asyncio
5
5
  from datetime import timedelta
6
6
  import json
7
7
 
8
+ from django.shortcuts import redirect
8
9
  from django.utils import timezone
9
10
  from django.urls import path
10
11
  from django.http import HttpResponse, HttpResponseRedirect
@@ -13,10 +14,9 @@ from django.template.response import TemplateResponse
13
14
  from .models import (
14
15
  Charger,
15
16
  Simulator,
16
- MeterReading,
17
+ MeterValue,
17
18
  Transaction,
18
19
  Location,
19
- ElectricVehicle,
20
20
  )
21
21
  from .simulator import ChargePointSimulator
22
22
  from . import store
@@ -24,8 +24,7 @@ from .transactions_io import (
24
24
  export_transactions,
25
25
  import_transactions as import_transactions_data,
26
26
  )
27
- from core.admin import RFIDAdmin
28
- from .models import RFID
27
+ from core.user_data import EntityModelAdmin
29
28
 
30
29
 
31
30
  class LocationAdminForm(forms.ModelForm):
@@ -39,9 +38,7 @@ class LocationAdminForm(forms.ModelForm):
39
38
  }
40
39
 
41
40
  class Media:
42
- css = {
43
- "all": ("https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",)
44
- }
41
+ css = {"all": ("https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",)}
45
42
  js = (
46
43
  "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
47
44
  "ocpp/charger_map.js",
@@ -60,29 +57,91 @@ class TransactionImportForm(forms.Form):
60
57
  file = forms.FileField()
61
58
 
62
59
 
60
+ class LogViewAdminMixin:
61
+ """Mixin providing an admin view to display charger or simulator logs."""
62
+
63
+ log_type = "charger"
64
+ log_template_name = "admin/ocpp/log_view.html"
65
+
66
+ def get_log_identifier(self, obj): # pragma: no cover - mixin hook
67
+ raise NotImplementedError
68
+
69
+ def get_log_title(self, obj):
70
+ return f"Log for {obj}"
71
+
72
+ def get_urls(self):
73
+ urls = super().get_urls()
74
+ info = self.model._meta.app_label, self.model._meta.model_name
75
+ custom = [
76
+ path(
77
+ "<path:object_id>/log/",
78
+ self.admin_site.admin_view(self.log_view),
79
+ name=f"{info[0]}_{info[1]}_log",
80
+ ),
81
+ ]
82
+ return custom + urls
83
+
84
+ def log_view(self, request, object_id):
85
+ obj = self.get_object(request, object_id)
86
+ if obj is None:
87
+ self.message_user(request, "Log is not available.", messages.ERROR)
88
+ return redirect("..")
89
+ identifier = self.get_log_identifier(obj)
90
+ log_entries = store.get_logs(identifier, log_type=self.log_type)
91
+ log_file = store._file_path(identifier, log_type=self.log_type)
92
+ context = {
93
+ **self.admin_site.each_context(request),
94
+ "opts": self.model._meta,
95
+ "original": obj,
96
+ "title": self.get_log_title(obj),
97
+ "log_entries": log_entries,
98
+ "log_file": str(log_file),
99
+ "log_identifier": identifier,
100
+ }
101
+ return TemplateResponse(request, self.log_template_name, context)
102
+
103
+
63
104
  @admin.register(Location)
64
- class LocationAdmin(admin.ModelAdmin):
105
+ class LocationAdmin(EntityModelAdmin):
65
106
  form = LocationAdminForm
66
107
  list_display = ("name", "latitude", "longitude")
108
+ change_form_template = "admin/ocpp/location/change_form.html"
67
109
 
68
110
 
69
111
  @admin.register(Charger)
70
- class ChargerAdmin(admin.ModelAdmin):
112
+ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
71
113
  fieldsets = (
72
114
  (
73
115
  "General",
74
116
  {
75
117
  "fields": (
76
118
  "charger_id",
119
+ "display_name",
77
120
  "connector_id",
78
- "require_rfid",
121
+ "location",
122
+ "last_path",
79
123
  "last_heartbeat",
80
124
  "last_meter_values",
81
- "last_path",
82
- "location",
125
+ "firmware_status",
126
+ "firmware_status_info",
127
+ "firmware_timestamp",
83
128
  )
84
129
  },
85
130
  ),
131
+ (
132
+ "Diagnostics",
133
+ {
134
+ "fields": (
135
+ "diagnostics_status",
136
+ "diagnostics_timestamp",
137
+ "diagnostics_location",
138
+ )
139
+ },
140
+ ),
141
+ (
142
+ "Configuration",
143
+ {"fields": ("public_display", "require_rfid")},
144
+ ),
86
145
  (
87
146
  "References",
88
147
  {
@@ -90,13 +149,22 @@ class ChargerAdmin(admin.ModelAdmin):
90
149
  },
91
150
  ),
92
151
  )
93
- readonly_fields = ("last_heartbeat", "last_meter_values")
152
+ readonly_fields = (
153
+ "last_heartbeat",
154
+ "last_meter_values",
155
+ "firmware_status",
156
+ "firmware_status_info",
157
+ "firmware_timestamp",
158
+ )
94
159
  list_display = (
95
160
  "charger_id",
96
161
  "connector_id",
97
162
  "location_name",
98
- "require_rfid",
163
+ "require_rfid_display",
164
+ "public_display",
99
165
  "last_heartbeat",
166
+ "firmware_status",
167
+ "firmware_timestamp",
100
168
  "session_kw",
101
169
  "total_kw_display",
102
170
  "page_link",
@@ -109,6 +177,12 @@ class ChargerAdmin(admin.ModelAdmin):
109
177
  def get_view_on_site_url(self, obj=None):
110
178
  return obj.get_absolute_url() if obj else None
111
179
 
180
+ def require_rfid_display(self, obj):
181
+ return obj.require_rfid
182
+
183
+ require_rfid_display.boolean = True
184
+ require_rfid_display.short_description = "RFID Auth"
185
+
112
186
  def page_link(self, obj):
113
187
  from django.utils.html import format_html
114
188
 
@@ -116,7 +190,7 @@ class ChargerAdmin(admin.ModelAdmin):
116
190
  '<a href="{}" target="_blank">open</a>', obj.get_absolute_url()
117
191
  )
118
192
 
119
- page_link.short_description = "Landing Page"
193
+ page_link.short_description = "Landing"
120
194
 
121
195
  def qr_link(self, obj):
122
196
  from django.utils.html import format_html
@@ -133,19 +207,25 @@ class ChargerAdmin(admin.ModelAdmin):
133
207
  from django.utils.html import format_html
134
208
  from django.urls import reverse
135
209
 
136
- url = reverse("charger-log", args=[obj.charger_id]) + "?type=charger"
210
+ url = reverse("admin:ocpp_charger_log", args=[obj.pk])
137
211
  return format_html('<a href="{}" target="_blank">view</a>', url)
138
212
 
139
213
  log_link.short_description = "Log"
140
-
214
+
215
+ def get_log_identifier(self, obj):
216
+ return store.identity_key(obj.charger_id, obj.connector_id)
217
+
141
218
  def status_link(self, obj):
142
219
  from django.utils.html import format_html
143
220
  from django.urls import reverse
144
221
 
145
- url = reverse("charger-status", args=[obj.charger_id])
222
+ url = reverse(
223
+ "charger-status-connector",
224
+ args=[obj.charger_id, obj.connector_slug],
225
+ )
146
226
  return format_html('<a href="{}" target="_blank">status</a>', url)
147
227
 
148
- status_link.short_description = "Status Page"
228
+ status_link.short_description = "Status"
149
229
 
150
230
  def location_name(self, obj):
151
231
  return obj.location.name if obj.location else ""
@@ -169,7 +249,7 @@ class ChargerAdmin(admin.ModelAdmin):
169
249
  total_kw_display.short_description = "Total kW"
170
250
 
171
251
  def session_kw(self, obj):
172
- tx = store.transactions.get(obj.charger_id)
252
+ tx = store.get_transaction(obj.charger_id, obj.connector_id)
173
253
  if tx:
174
254
  return round(tx.kw, 2)
175
255
  return 0.0
@@ -178,7 +258,7 @@ class ChargerAdmin(admin.ModelAdmin):
178
258
 
179
259
 
180
260
  @admin.register(Simulator)
181
- class SimulatorAdmin(admin.ModelAdmin):
261
+ class SimulatorAdmin(LogViewAdminMixin, EntityModelAdmin):
182
262
  list_display = (
183
263
  "name",
184
264
  "cp_path",
@@ -197,28 +277,100 @@ class SimulatorAdmin(admin.ModelAdmin):
197
277
  "rfid",
198
278
  ("duration", "interval", "pre_charge_delay"),
199
279
  "kw_max",
200
- "repeat",
280
+ ("repeat", "door_open"),
201
281
  ("username", "password"),
202
282
  )
203
- actions = ("start_simulator", "stop_simulator")
283
+ actions = ("start_simulator", "stop_simulator", "send_open_door")
284
+
285
+ log_type = "simulator"
286
+
287
+ def save_model(self, request, obj, form, change):
288
+ previous_door_open = False
289
+ if change and obj.pk:
290
+ previous_door_open = (
291
+ type(obj)
292
+ .objects.filter(pk=obj.pk)
293
+ .values_list("door_open", flat=True)
294
+ .first()
295
+ or False
296
+ )
297
+ super().save_model(request, obj, form, change)
298
+ if obj.door_open and not previous_door_open:
299
+ triggered = self._queue_door_open(request, obj)
300
+ if not triggered:
301
+ type(obj).objects.filter(pk=obj.pk).update(door_open=False)
302
+ obj.door_open = False
303
+
304
+ def _queue_door_open(self, request, obj) -> bool:
305
+ sim = store.simulators.get(obj.pk)
306
+ if not sim:
307
+ self.message_user(
308
+ request,
309
+ f"{obj.name}: simulator is not running",
310
+ level=messages.ERROR,
311
+ )
312
+ return False
313
+ type(obj).objects.filter(pk=obj.pk).update(door_open=True)
314
+ obj.door_open = True
315
+ store.add_log(
316
+ obj.cp_path,
317
+ "Door open event requested from admin",
318
+ log_type="simulator",
319
+ )
320
+ if hasattr(sim, "trigger_door_open"):
321
+ sim.trigger_door_open()
322
+ else: # pragma: no cover - unexpected condition
323
+ self.message_user(
324
+ request,
325
+ f"{obj.name}: simulator cannot send door open event",
326
+ level=messages.ERROR,
327
+ )
328
+ type(obj).objects.filter(pk=obj.pk).update(door_open=False)
329
+ obj.door_open = False
330
+ return False
331
+ type(obj).objects.filter(pk=obj.pk).update(door_open=False)
332
+ obj.door_open = False
333
+ self.message_user(
334
+ request,
335
+ f"{obj.name}: DoorOpen status notification sent",
336
+ )
337
+ return True
204
338
 
205
339
  def running(self, obj):
206
340
  return obj.pk in store.simulators
207
341
 
208
342
  running.boolean = True
209
343
 
344
+ @admin.action(description="Send Open Door")
345
+ def send_open_door(self, request, queryset):
346
+ for obj in queryset:
347
+ self._queue_door_open(request, obj)
348
+
210
349
  def start_simulator(self, request, queryset):
350
+ from django.urls import reverse
351
+ from django.utils.html import format_html
352
+
211
353
  for obj in queryset:
212
354
  if obj.pk in store.simulators:
213
355
  self.message_user(request, f"{obj.name}: already running")
214
356
  continue
357
+ type(obj).objects.filter(pk=obj.pk).update(door_open=False)
358
+ obj.door_open = False
215
359
  store.register_log_name(obj.cp_path, obj.name, log_type="simulator")
216
360
  sim = ChargePointSimulator(obj.as_config())
217
361
  started, status, log_file = sim.start()
218
362
  if started:
219
363
  store.simulators[obj.pk] = sim
364
+ log_url = reverse("admin:ocpp_simulator_log", args=[obj.pk])
220
365
  self.message_user(
221
- request, f"{obj.name}: {status}. Log: {log_file}"
366
+ request,
367
+ format_html(
368
+ '{}: {}. Log: <code>{}</code> (<a href="{}" target="_blank">View Log</a>)',
369
+ obj.name,
370
+ status,
371
+ log_file,
372
+ log_url,
373
+ ),
222
374
  )
223
375
 
224
376
  start_simulator.short_description = "Start selected simulators"
@@ -239,22 +391,35 @@ class SimulatorAdmin(admin.ModelAdmin):
239
391
  from django.utils.html import format_html
240
392
  from django.urls import reverse
241
393
 
242
- url = reverse("charger-log", args=[obj.cp_path]) + "?type=simulator"
394
+ url = reverse("admin:ocpp_simulator_log", args=[obj.pk])
243
395
  return format_html('<a href="{}" target="_blank">view</a>', url)
244
396
 
245
397
  log_link.short_description = "Log"
246
398
 
399
+ def get_log_identifier(self, obj):
400
+ return obj.cp_path
247
401
 
248
- class MeterReadingInline(admin.TabularInline):
249
- model = MeterReading
402
+
403
+ class MeterValueInline(admin.TabularInline):
404
+ model = MeterValue
250
405
  extra = 0
251
- fields = ("timestamp", "value", "unit", "measurand", "connector_id")
406
+ fields = (
407
+ "timestamp",
408
+ "context",
409
+ "energy",
410
+ "voltage",
411
+ "current_import",
412
+ "current_offered",
413
+ "temperature",
414
+ "soc",
415
+ "connector_id",
416
+ )
252
417
  readonly_fields = fields
253
418
  can_delete = False
254
419
 
255
420
 
256
421
  @admin.register(Transaction)
257
- class TransactionAdmin(admin.ModelAdmin):
422
+ class TransactionAdmin(EntityModelAdmin):
258
423
  change_list_template = "admin/ocpp/transaction/change_list.html"
259
424
  list_display = (
260
425
  "charger",
@@ -269,7 +434,7 @@ class TransactionAdmin(admin.ModelAdmin):
269
434
  readonly_fields = ("kw",)
270
435
  list_filter = ("charger", "account")
271
436
  date_hierarchy = "start_time"
272
- inlines = [MeterReadingInline]
437
+ inlines = [MeterValueInline]
273
438
 
274
439
  def get_urls(self):
275
440
  urls = super().get_urls()
@@ -301,17 +466,15 @@ class TransactionAdmin(admin.ModelAdmin):
301
466
  json.dumps(data, indent=2, ensure_ascii=False),
302
467
  content_type="application/json",
303
468
  )
304
- response[
305
- "Content-Disposition"
306
- ] = "attachment; filename=transactions.json"
469
+ response["Content-Disposition"] = (
470
+ "attachment; filename=transactions.json"
471
+ )
307
472
  return response
308
473
  else:
309
474
  form = TransactionExportForm()
310
475
  context = self.admin_site.each_context(request)
311
476
  context["form"] = form
312
- return TemplateResponse(
313
- request, "admin/ocpp/transaction/export.html", context
314
- )
477
+ return TemplateResponse(request, "admin/ocpp/transaction/export.html", context)
315
478
 
316
479
  def import_view(self, request):
317
480
  if request.method == "POST":
@@ -325,12 +488,10 @@ class TransactionAdmin(admin.ModelAdmin):
325
488
  form = TransactionImportForm()
326
489
  context = self.admin_site.each_context(request)
327
490
  context["form"] = form
328
- return TemplateResponse(
329
- request, "admin/ocpp/transaction/import.html", context
330
- )
491
+ return TemplateResponse(request, "admin/ocpp/transaction/import.html", context)
331
492
 
332
493
 
333
- class MeterReadingDateFilter(admin.SimpleListFilter):
494
+ class MeterValueDateFilter(admin.SimpleListFilter):
334
495
  title = "Timestamp"
335
496
  parameter_name = "timestamp_range"
336
497
 
@@ -361,32 +522,20 @@ class MeterReadingDateFilter(admin.SimpleListFilter):
361
522
  return queryset
362
523
 
363
524
 
364
- @admin.register(MeterReading)
365
- class MeterReadingAdmin(admin.ModelAdmin):
525
+ @admin.register(MeterValue)
526
+ class MeterValueAdmin(EntityModelAdmin):
366
527
  list_display = (
367
528
  "charger",
368
529
  "timestamp",
369
- "value",
370
- "unit",
530
+ "context",
531
+ "energy",
532
+ "voltage",
533
+ "current_import",
534
+ "current_offered",
535
+ "temperature",
536
+ "soc",
371
537
  "connector_id",
372
538
  "transaction",
373
539
  )
374
540
  date_hierarchy = "timestamp"
375
- list_filter = ("charger", MeterReadingDateFilter)
376
-
377
-
378
- @admin.register(ElectricVehicle)
379
- class ElectricVehicleAdmin(admin.ModelAdmin):
380
- list_display = ("vin", "license_plate", "brand", "model", "account")
381
- search_fields = (
382
- "vin",
383
- "license_plate",
384
- "brand__name",
385
- "model__name",
386
- "account__name",
387
- )
388
- fields = ("account", "vin", "license_plate", "brand", "model")
389
-
390
-
391
- admin.site.register(RFID, RFIDAdmin)
392
-
541
+ list_filter = ("charger", MeterValueDateFilter)
ocpp/apps.py CHANGED
@@ -6,7 +6,7 @@ from django.conf import settings
6
6
  class OcppConfig(AppConfig):
7
7
  default_auto_field = "django.db.models.BigAutoField"
8
8
  name = "ocpp"
9
- verbose_name = "3. Protocols"
9
+ verbose_name = "3. Protocol"
10
10
 
11
11
  def ready(self): # pragma: no cover - startup side effects
12
12
  control_lock = Path(settings.BASE_DIR) / "locks" / "control.lck"