arthexis 0.1.7__py3-none-any.whl → 0.1.9__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 (82) hide show
  1. arthexis-0.1.9.dist-info/METADATA +168 -0
  2. arthexis-0.1.9.dist-info/RECORD +92 -0
  3. arthexis-0.1.9.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 +134 -16
  10. config/urls.py +71 -3
  11. core/admin.py +1331 -165
  12. core/admin_history.py +50 -0
  13. core/admindocs.py +151 -0
  14. core/apps.py +158 -3
  15. core/backends.py +46 -4
  16. core/entity.py +62 -48
  17. core/fields.py +6 -1
  18. core/github_helper.py +25 -0
  19. core/github_issues.py +172 -0
  20. core/lcd_screen.py +1 -0
  21. core/liveupdate.py +25 -0
  22. core/log_paths.py +100 -0
  23. core/mailer.py +83 -0
  24. core/middleware.py +57 -0
  25. core/models.py +1136 -259
  26. core/notifications.py +11 -1
  27. core/public_wifi.py +227 -0
  28. core/release.py +27 -20
  29. core/sigil_builder.py +131 -0
  30. core/sigil_context.py +20 -0
  31. core/sigil_resolver.py +284 -0
  32. core/system.py +129 -10
  33. core/tasks.py +118 -19
  34. core/test_system_info.py +22 -0
  35. core/tests.py +445 -58
  36. core/tests_liveupdate.py +17 -0
  37. core/urls.py +2 -2
  38. core/user_data.py +329 -167
  39. core/views.py +383 -57
  40. core/widgets.py +51 -0
  41. core/workgroup_urls.py +17 -0
  42. core/workgroup_views.py +94 -0
  43. nodes/actions.py +0 -2
  44. nodes/admin.py +159 -284
  45. nodes/apps.py +9 -15
  46. nodes/backends.py +53 -0
  47. nodes/lcd.py +24 -10
  48. nodes/models.py +375 -178
  49. nodes/tasks.py +1 -5
  50. nodes/tests.py +524 -129
  51. nodes/utils.py +13 -2
  52. nodes/views.py +66 -23
  53. ocpp/admin.py +150 -61
  54. ocpp/apps.py +4 -3
  55. ocpp/consumers.py +432 -69
  56. ocpp/evcs.py +25 -8
  57. ocpp/models.py +408 -68
  58. ocpp/simulator.py +13 -6
  59. ocpp/store.py +258 -30
  60. ocpp/tasks.py +11 -7
  61. ocpp/test_export_import.py +8 -7
  62. ocpp/test_rfid.py +211 -16
  63. ocpp/tests.py +1198 -135
  64. ocpp/transactions_io.py +68 -22
  65. ocpp/urls.py +35 -2
  66. ocpp/views.py +654 -101
  67. pages/admin.py +173 -13
  68. pages/checks.py +0 -1
  69. pages/context_processors.py +19 -6
  70. pages/middleware.py +153 -0
  71. pages/models.py +37 -9
  72. pages/tests.py +759 -40
  73. pages/urls.py +3 -0
  74. pages/utils.py +0 -1
  75. pages/views.py +576 -25
  76. arthexis-0.1.7.dist-info/METADATA +0 -126
  77. arthexis-0.1.7.dist-info/RECORD +0 -77
  78. arthexis-0.1.7.dist-info/licenses/LICENSE +0 -21
  79. config/workgroup_app.py +0 -7
  80. core/checks.py +0 -29
  81. {arthexis-0.1.7.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
  82. {arthexis-0.1.7.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
nodes/utils.py CHANGED
@@ -14,8 +14,12 @@ SCREENSHOT_DIR = settings.LOG_DIR / "screenshots"
14
14
  logger = logging.getLogger(__name__)
15
15
 
16
16
 
17
- def capture_screenshot(url: str) -> Path:
18
- """Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`."""
17
+ def capture_screenshot(url: str, cookies=None) -> Path:
18
+ """Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`.
19
+
20
+ ``cookies`` can be an iterable of Selenium cookie mappings which will be
21
+ applied after the initial navigation and before the screenshot is taken.
22
+ """
19
23
  options = Options()
20
24
  options.add_argument("-headless")
21
25
  try:
@@ -27,6 +31,13 @@ def capture_screenshot(url: str) -> Path:
27
31
  browser.get(url)
28
32
  except WebDriverException as exc:
29
33
  logger.error("Failed to load %s: %s", url, exc)
34
+ if cookies:
35
+ for cookie in cookies:
36
+ try:
37
+ browser.add_cookie(cookie)
38
+ except WebDriverException as exc:
39
+ logger.error("Failed to apply cookie for %s: %s", url, exc)
40
+ browser.get(url)
30
41
  if not browser.save_screenshot(str(filename)):
31
42
  raise RuntimeError("Screenshot capture failed")
32
43
  return filename
nodes/views.py CHANGED
@@ -6,6 +6,7 @@ from django.views.decorators.csrf import csrf_exempt
6
6
  from django.shortcuts import get_object_or_404
7
7
  from django.conf import settings
8
8
  from pathlib import Path
9
+ from django.utils.cache import patch_vary_headers
9
10
 
10
11
  from utils.api import api_login_required
11
12
 
@@ -20,15 +21,16 @@ from .utils import capture_screenshot, save_screenshot
20
21
  def node_list(request):
21
22
  """Return a JSON list of all known nodes."""
22
23
 
23
- nodes = list(
24
- Node.objects.values(
25
- "hostname",
26
- "address",
27
- "port",
28
- "last_seen",
29
- "has_lcd_screen",
30
- )
31
- )
24
+ nodes = [
25
+ {
26
+ "hostname": node.hostname,
27
+ "address": node.address,
28
+ "port": node.port,
29
+ "last_seen": node.last_seen,
30
+ "features": list(node.features.values_list("slug", flat=True)),
31
+ }
32
+ for node in Node.objects.prefetch_related("features")
33
+ ]
32
34
  return JsonResponse({"nodes": nodes})
33
35
 
34
36
 
@@ -47,7 +49,7 @@ def node_info(request):
47
49
  "port": node.port,
48
50
  "mac_address": node.mac_address,
49
51
  "public_key": node.public_key,
50
- "has_lcd_screen": node.has_lcd_screen,
52
+ "features": list(node.features.values_list("slug", flat=True)),
51
53
  }
52
54
 
53
55
  if token:
@@ -74,19 +76,49 @@ def node_info(request):
74
76
  return response
75
77
 
76
78
 
79
+ def _add_cors_headers(request, response):
80
+ origin = request.headers.get("Origin")
81
+ if origin:
82
+ response["Access-Control-Allow-Origin"] = origin
83
+ response["Access-Control-Allow-Credentials"] = "true"
84
+ allow_headers = request.headers.get(
85
+ "Access-Control-Request-Headers", "Content-Type"
86
+ )
87
+ response["Access-Control-Allow-Headers"] = allow_headers
88
+ response["Access-Control-Allow-Methods"] = "POST, OPTIONS"
89
+ patch_vary_headers(response, ["Origin"])
90
+ return response
91
+
92
+
77
93
  @csrf_exempt
78
94
  @api_login_required
79
95
  def register_node(request):
80
96
  """Register or update a node from POSTed JSON data."""
81
97
 
98
+ if request.method == "OPTIONS":
99
+ response = JsonResponse({"detail": "ok"})
100
+ return _add_cors_headers(request, response)
101
+
82
102
  if request.method != "POST":
83
- return JsonResponse({"detail": "POST required"}, status=400)
103
+ response = JsonResponse({"detail": "POST required"}, status=400)
104
+ return _add_cors_headers(request, response)
84
105
 
85
106
  try:
86
107
  data = json.loads(request.body.decode())
87
108
  except json.JSONDecodeError:
88
109
  data = request.POST
89
110
 
111
+ if hasattr(data, "getlist"):
112
+ raw_features = data.getlist("features")
113
+ if not raw_features:
114
+ features = None
115
+ elif len(raw_features) == 1:
116
+ features = raw_features[0]
117
+ else:
118
+ features = raw_features
119
+ else:
120
+ features = data.get("features")
121
+
90
122
  hostname = data.get("hostname")
91
123
  address = data.get("address")
92
124
  port = data.get("port", 8000)
@@ -94,12 +126,12 @@ def register_node(request):
94
126
  public_key = data.get("public_key")
95
127
  token = data.get("token")
96
128
  signature = data.get("signature")
97
- has_lcd_screen = data.get("has_lcd_screen")
98
129
 
99
130
  if not hostname or not address or not mac_address:
100
- return JsonResponse(
131
+ response = JsonResponse(
101
132
  {"detail": "hostname, address and mac_address required"}, status=400
102
133
  )
134
+ return _add_cors_headers(request, response)
103
135
 
104
136
  verified = False
105
137
  if public_key and token and signature:
@@ -113,14 +145,14 @@ def register_node(request):
113
145
  )
114
146
  verified = True
115
147
  except Exception:
116
- return JsonResponse({"detail": "invalid signature"}, status=403)
148
+ response = JsonResponse({"detail": "invalid signature"}, status=403)
149
+ return _add_cors_headers(request, response)
117
150
 
118
151
  mac_address = mac_address.lower()
119
152
  defaults = {
120
153
  "hostname": hostname,
121
154
  "address": address,
122
155
  "port": port,
123
- "has_lcd_screen": bool(has_lcd_screen),
124
156
  }
125
157
  if verified:
126
158
  defaults["public_key"] = public_key
@@ -137,15 +169,27 @@ def register_node(request):
137
169
  if verified:
138
170
  node.public_key = public_key
139
171
  update_fields.append("public_key")
140
- if has_lcd_screen is not None:
141
- node.has_lcd_screen = bool(has_lcd_screen)
142
- update_fields.append("has_lcd_screen")
143
172
  node.save(update_fields=update_fields)
144
- return JsonResponse(
173
+ if features is not None:
174
+ if isinstance(features, (str, bytes)):
175
+ feature_list = [features]
176
+ else:
177
+ feature_list = list(features)
178
+ node.update_manual_features(feature_list)
179
+ response = JsonResponse(
145
180
  {"id": node.id, "detail": f"Node already exists (id: {node.id})"}
146
181
  )
182
+ return _add_cors_headers(request, response)
147
183
 
148
- return JsonResponse({"id": node.id})
184
+ if features is not None:
185
+ if isinstance(features, (str, bytes)):
186
+ feature_list = [features]
187
+ else:
188
+ feature_list = list(features)
189
+ node.update_manual_features(feature_list)
190
+
191
+ response = JsonResponse({"id": node.id})
192
+ return _add_cors_headers(request, response)
149
193
 
150
194
 
151
195
  @api_login_required
@@ -172,9 +216,7 @@ def public_node_endpoint(request, endpoint):
172
216
  - ``POST`` broadcasts the request body as a :class:`NetMessage`.
173
217
  """
174
218
 
175
- node = get_object_or_404(
176
- Node, public_endpoint=endpoint, enable_public_api=True
177
- )
219
+ node = get_object_or_404(Node, public_endpoint=endpoint, enable_public_api=True)
178
220
 
179
221
  if request.method == "GET":
180
222
  data = {
@@ -183,6 +225,7 @@ def public_node_endpoint(request, endpoint):
183
225
  "port": node.port,
184
226
  "badge_color": node.badge_color,
185
227
  "last_seen": node.last_seen,
228
+ "features": list(node.features.values_list("slug", flat=True)),
186
229
  }
187
230
  return JsonResponse(data)
188
231
 
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",
128
+ )
129
+ },
130
+ ),
131
+ (
132
+ "Diagnostics",
133
+ {
134
+ "fields": (
135
+ "diagnostics_status",
136
+ "diagnostics_timestamp",
137
+ "diagnostics_location",
83
138
  )
84
139
  },
85
140
  ),
141
+ (
142
+ "Configuration",
143
+ {"fields": ("require_rfid",)},
144
+ ),
86
145
  (
87
146
  "References",
88
147
  {
@@ -90,13 +149,21 @@ 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",
99
164
  "last_heartbeat",
165
+ "firmware_status",
166
+ "firmware_timestamp",
100
167
  "session_kw",
101
168
  "total_kw_display",
102
169
  "page_link",
@@ -109,6 +176,12 @@ class ChargerAdmin(admin.ModelAdmin):
109
176
  def get_view_on_site_url(self, obj=None):
110
177
  return obj.get_absolute_url() if obj else None
111
178
 
179
+ def require_rfid_display(self, obj):
180
+ return obj.require_rfid
181
+
182
+ require_rfid_display.boolean = True
183
+ require_rfid_display.short_description = "RFID Auth"
184
+
112
185
  def page_link(self, obj):
113
186
  from django.utils.html import format_html
114
187
 
@@ -116,7 +189,7 @@ class ChargerAdmin(admin.ModelAdmin):
116
189
  '<a href="{}" target="_blank">open</a>', obj.get_absolute_url()
117
190
  )
118
191
 
119
- page_link.short_description = "Landing Page"
192
+ page_link.short_description = "Landing"
120
193
 
121
194
  def qr_link(self, obj):
122
195
  from django.utils.html import format_html
@@ -133,19 +206,25 @@ class ChargerAdmin(admin.ModelAdmin):
133
206
  from django.utils.html import format_html
134
207
  from django.urls import reverse
135
208
 
136
- url = reverse("charger-log", args=[obj.charger_id]) + "?type=charger"
209
+ url = reverse("admin:ocpp_charger_log", args=[obj.pk])
137
210
  return format_html('<a href="{}" target="_blank">view</a>', url)
138
211
 
139
212
  log_link.short_description = "Log"
140
-
213
+
214
+ def get_log_identifier(self, obj):
215
+ return store.identity_key(obj.charger_id, obj.connector_id)
216
+
141
217
  def status_link(self, obj):
142
218
  from django.utils.html import format_html
143
219
  from django.urls import reverse
144
220
 
145
- url = reverse("charger-status", args=[obj.charger_id])
221
+ url = reverse(
222
+ "charger-status-connector",
223
+ args=[obj.charger_id, obj.connector_slug],
224
+ )
146
225
  return format_html('<a href="{}" target="_blank">status</a>', url)
147
226
 
148
- status_link.short_description = "Status Page"
227
+ status_link.short_description = "Status"
149
228
 
150
229
  def location_name(self, obj):
151
230
  return obj.location.name if obj.location else ""
@@ -169,7 +248,7 @@ class ChargerAdmin(admin.ModelAdmin):
169
248
  total_kw_display.short_description = "Total kW"
170
249
 
171
250
  def session_kw(self, obj):
172
- tx = store.transactions.get(obj.charger_id)
251
+ tx = store.get_transaction(obj.charger_id, obj.connector_id)
173
252
  if tx:
174
253
  return round(tx.kw, 2)
175
254
  return 0.0
@@ -178,7 +257,7 @@ class ChargerAdmin(admin.ModelAdmin):
178
257
 
179
258
 
180
259
  @admin.register(Simulator)
181
- class SimulatorAdmin(admin.ModelAdmin):
260
+ class SimulatorAdmin(LogViewAdminMixin, EntityModelAdmin):
182
261
  list_display = (
183
262
  "name",
184
263
  "cp_path",
@@ -202,12 +281,17 @@ class SimulatorAdmin(admin.ModelAdmin):
202
281
  )
203
282
  actions = ("start_simulator", "stop_simulator")
204
283
 
284
+ log_type = "simulator"
285
+
205
286
  def running(self, obj):
206
287
  return obj.pk in store.simulators
207
288
 
208
289
  running.boolean = True
209
290
 
210
291
  def start_simulator(self, request, queryset):
292
+ from django.urls import reverse
293
+ from django.utils.html import format_html
294
+
211
295
  for obj in queryset:
212
296
  if obj.pk in store.simulators:
213
297
  self.message_user(request, f"{obj.name}: already running")
@@ -217,8 +301,16 @@ class SimulatorAdmin(admin.ModelAdmin):
217
301
  started, status, log_file = sim.start()
218
302
  if started:
219
303
  store.simulators[obj.pk] = sim
304
+ log_url = reverse("admin:ocpp_simulator_log", args=[obj.pk])
220
305
  self.message_user(
221
- request, f"{obj.name}: {status}. Log: {log_file}"
306
+ request,
307
+ format_html(
308
+ '{}: {}. Log: <code>{}</code> (<a href="{}" target="_blank">View Log</a>)',
309
+ obj.name,
310
+ status,
311
+ log_file,
312
+ log_url,
313
+ ),
222
314
  )
223
315
 
224
316
  start_simulator.short_description = "Start selected simulators"
@@ -239,22 +331,35 @@ class SimulatorAdmin(admin.ModelAdmin):
239
331
  from django.utils.html import format_html
240
332
  from django.urls import reverse
241
333
 
242
- url = reverse("charger-log", args=[obj.cp_path]) + "?type=simulator"
334
+ url = reverse("admin:ocpp_simulator_log", args=[obj.pk])
243
335
  return format_html('<a href="{}" target="_blank">view</a>', url)
244
336
 
245
337
  log_link.short_description = "Log"
246
338
 
339
+ def get_log_identifier(self, obj):
340
+ return obj.cp_path
247
341
 
248
- class MeterReadingInline(admin.TabularInline):
249
- model = MeterReading
342
+
343
+ class MeterValueInline(admin.TabularInline):
344
+ model = MeterValue
250
345
  extra = 0
251
- fields = ("timestamp", "value", "unit", "measurand", "connector_id")
346
+ fields = (
347
+ "timestamp",
348
+ "context",
349
+ "energy",
350
+ "voltage",
351
+ "current_import",
352
+ "current_offered",
353
+ "temperature",
354
+ "soc",
355
+ "connector_id",
356
+ )
252
357
  readonly_fields = fields
253
358
  can_delete = False
254
359
 
255
360
 
256
361
  @admin.register(Transaction)
257
- class TransactionAdmin(admin.ModelAdmin):
362
+ class TransactionAdmin(EntityModelAdmin):
258
363
  change_list_template = "admin/ocpp/transaction/change_list.html"
259
364
  list_display = (
260
365
  "charger",
@@ -269,7 +374,7 @@ class TransactionAdmin(admin.ModelAdmin):
269
374
  readonly_fields = ("kw",)
270
375
  list_filter = ("charger", "account")
271
376
  date_hierarchy = "start_time"
272
- inlines = [MeterReadingInline]
377
+ inlines = [MeterValueInline]
273
378
 
274
379
  def get_urls(self):
275
380
  urls = super().get_urls()
@@ -301,17 +406,15 @@ class TransactionAdmin(admin.ModelAdmin):
301
406
  json.dumps(data, indent=2, ensure_ascii=False),
302
407
  content_type="application/json",
303
408
  )
304
- response[
305
- "Content-Disposition"
306
- ] = "attachment; filename=transactions.json"
409
+ response["Content-Disposition"] = (
410
+ "attachment; filename=transactions.json"
411
+ )
307
412
  return response
308
413
  else:
309
414
  form = TransactionExportForm()
310
415
  context = self.admin_site.each_context(request)
311
416
  context["form"] = form
312
- return TemplateResponse(
313
- request, "admin/ocpp/transaction/export.html", context
314
- )
417
+ return TemplateResponse(request, "admin/ocpp/transaction/export.html", context)
315
418
 
316
419
  def import_view(self, request):
317
420
  if request.method == "POST":
@@ -325,12 +428,10 @@ class TransactionAdmin(admin.ModelAdmin):
325
428
  form = TransactionImportForm()
326
429
  context = self.admin_site.each_context(request)
327
430
  context["form"] = form
328
- return TemplateResponse(
329
- request, "admin/ocpp/transaction/import.html", context
330
- )
431
+ return TemplateResponse(request, "admin/ocpp/transaction/import.html", context)
331
432
 
332
433
 
333
- class MeterReadingDateFilter(admin.SimpleListFilter):
434
+ class MeterValueDateFilter(admin.SimpleListFilter):
334
435
  title = "Timestamp"
335
436
  parameter_name = "timestamp_range"
336
437
 
@@ -361,32 +462,20 @@ class MeterReadingDateFilter(admin.SimpleListFilter):
361
462
  return queryset
362
463
 
363
464
 
364
- @admin.register(MeterReading)
365
- class MeterReadingAdmin(admin.ModelAdmin):
465
+ @admin.register(MeterValue)
466
+ class MeterValueAdmin(EntityModelAdmin):
366
467
  list_display = (
367
468
  "charger",
368
469
  "timestamp",
369
- "value",
370
- "unit",
470
+ "context",
471
+ "energy",
472
+ "voltage",
473
+ "current_import",
474
+ "current_offered",
475
+ "temperature",
476
+ "soc",
371
477
  "connector_id",
372
478
  "transaction",
373
479
  )
374
480
  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
-
481
+ list_filter = ("charger", MeterValueDateFilter)
ocpp/apps.py CHANGED
@@ -6,11 +6,12 @@ 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
- lock = Path(settings.BASE_DIR) / "locks" / "control.lck"
13
- if not lock.exists():
12
+ control_lock = Path(settings.BASE_DIR) / "locks" / "control.lck"
13
+ rfid_lock = Path(settings.BASE_DIR) / "locks" / "rfid.lck"
14
+ if not (control_lock.exists() and rfid_lock.exists()):
14
15
  return
15
16
  from .rfid.background_reader import start
16
17
  from .rfid.signals import tag_scanned