arthexis 0.1.12__py3-none-any.whl → 0.1.13__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.12.dist-info → arthexis-0.1.13.dist-info}/METADATA +2 -2
- {arthexis-0.1.12.dist-info → arthexis-0.1.13.dist-info}/RECORD +37 -34
- config/asgi.py +15 -1
- config/celery.py +8 -1
- config/settings.py +42 -76
- config/settings_helpers.py +109 -0
- core/admin.py +47 -10
- core/auto_upgrade.py +2 -2
- core/form_fields.py +75 -0
- core/models.py +182 -59
- core/release.py +38 -20
- core/tests.py +11 -1
- core/views.py +47 -12
- core/widgets.py +43 -0
- nodes/admin.py +277 -14
- nodes/apps.py +15 -0
- nodes/models.py +224 -43
- nodes/tests.py +629 -10
- nodes/urls.py +1 -0
- nodes/views.py +173 -5
- ocpp/admin.py +146 -2
- ocpp/consumers.py +125 -8
- ocpp/evcs.py +7 -94
- ocpp/models.py +2 -0
- ocpp/routing.py +4 -2
- ocpp/simulator.py +29 -8
- ocpp/status_display.py +26 -0
- ocpp/tests.py +625 -16
- ocpp/transactions_io.py +10 -0
- ocpp/views.py +122 -22
- pages/admin.py +3 -0
- pages/forms.py +30 -1
- pages/tests.py +118 -1
- pages/views.py +12 -4
- {arthexis-0.1.12.dist-info → arthexis-0.1.13.dist-info}/WHEEL +0 -0
- {arthexis-0.1.12.dist-info → arthexis-0.1.13.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.12.dist-info → arthexis-0.1.13.dist-info}/top_level.txt +0 -0
ocpp/evcs.py
CHANGED
|
@@ -186,6 +186,8 @@ async def simulate_cp(
|
|
|
186
186
|
interval: float = 5.0,
|
|
187
187
|
username: Optional[str] = None,
|
|
188
188
|
password: Optional[str] = None,
|
|
189
|
+
*,
|
|
190
|
+
sim_state: SimulatorState | None = None,
|
|
189
191
|
) -> None:
|
|
190
192
|
"""Simulate one charge point session.
|
|
191
193
|
|
|
@@ -206,7 +208,7 @@ async def simulate_cp(
|
|
|
206
208
|
b64 = base64.b64encode(userpass.encode("utf-8")).decode("ascii")
|
|
207
209
|
headers["Authorization"] = f"Basic {b64}"
|
|
208
210
|
|
|
209
|
-
state = _simulators.get(cp_idx + 1, _simulators[1])
|
|
211
|
+
state = sim_state or _simulators.get(cp_idx + 1, _simulators[1])
|
|
210
212
|
|
|
211
213
|
loop_count = 0
|
|
212
214
|
while loop_count < session_count and state.running:
|
|
@@ -247,99 +249,6 @@ async def simulate_cp(
|
|
|
247
249
|
stop_event = asyncio.Event()
|
|
248
250
|
reset_event = asyncio.Event()
|
|
249
251
|
|
|
250
|
-
async def listen():
|
|
251
|
-
try:
|
|
252
|
-
while True:
|
|
253
|
-
raw = await _recv()
|
|
254
|
-
try:
|
|
255
|
-
msg = json.loads(raw)
|
|
256
|
-
except json.JSONDecodeError:
|
|
257
|
-
continue
|
|
258
|
-
|
|
259
|
-
if isinstance(msg, list) and msg and msg[0] == 2:
|
|
260
|
-
msg_id, action = msg[1], msg[2]
|
|
261
|
-
await _send([3, msg_id, {}])
|
|
262
|
-
if action == "RemoteStopTransaction":
|
|
263
|
-
state.last_message = "RemoteStopTransaction"
|
|
264
|
-
stop_event.set()
|
|
265
|
-
elif action == "Reset":
|
|
266
|
-
state.last_message = "Reset"
|
|
267
|
-
reset_event.set()
|
|
268
|
-
stop_event.set()
|
|
269
|
-
except websockets.ConnectionClosed:
|
|
270
|
-
stop_event.set()
|
|
271
|
-
|
|
272
|
-
# boot notification / authorise
|
|
273
|
-
await _send(
|
|
274
|
-
[
|
|
275
|
-
2,
|
|
276
|
-
"boot",
|
|
277
|
-
"BootNotification",
|
|
278
|
-
{
|
|
279
|
-
"chargePointModel": "Simulator",
|
|
280
|
-
"chargePointVendor": "SimVendor",
|
|
281
|
-
"serialNumber": serial_number,
|
|
282
|
-
},
|
|
283
|
-
]
|
|
284
|
-
)
|
|
285
|
-
state.last_message = "BootNotification"
|
|
286
|
-
await _recv()
|
|
287
|
-
await _send([2, "auth", "Authorize", {"idTag": rfid}])
|
|
288
|
-
state.last_message = "Authorize"
|
|
289
|
-
await _recv()
|
|
290
|
-
|
|
291
|
-
state.phase = "Available"
|
|
292
|
-
|
|
293
|
-
meter_start = random.randint(1000, 2000)
|
|
294
|
-
actual_duration = random.uniform(duration * 0.75, duration * 1.25)
|
|
295
|
-
steps = max(1, int(actual_duration / interval))
|
|
296
|
-
step_min = max(1, int((kw_min * 1000) / steps))
|
|
297
|
-
step_max = max(1, int((kw_max * 1000) / steps))
|
|
298
|
-
|
|
299
|
-
# optional pre‑charge delay while still sending heartbeats
|
|
300
|
-
if pre_charge_delay > 0:
|
|
301
|
-
start_delay = time.monotonic()
|
|
302
|
-
next_meter = meter_start
|
|
303
|
-
last_mv = time.monotonic()
|
|
304
|
-
while time.monotonic() - start_delay < pre_charge_delay:
|
|
305
|
-
await _send([2, "hb", "Heartbeat", {}])
|
|
306
|
-
state.last_message = "Heartbeat"
|
|
307
|
-
await _recv()
|
|
308
|
-
await asyncio.sleep(5)
|
|
309
|
-
if time.monotonic() - last_mv >= 30:
|
|
310
|
-
idle_step = max(2, int(step_max / 100))
|
|
311
|
-
next_meter += random.randint(0, idle_step)
|
|
312
|
-
next_kw = next_meter / 1000.0
|
|
313
|
-
await _send(
|
|
314
|
-
[
|
|
315
|
-
2,
|
|
316
|
-
"meter",
|
|
317
|
-
"MeterValues",
|
|
318
|
-
{
|
|
319
|
-
"connectorId": connector_id,
|
|
320
|
-
"meterValue": [
|
|
321
|
-
{
|
|
322
|
-
"timestamp": time.strftime(
|
|
323
|
-
"%Y-%m-%dT%H:%M:%S"
|
|
324
|
-
)
|
|
325
|
-
+ "Z",
|
|
326
|
-
"sampledValue": [
|
|
327
|
-
{
|
|
328
|
-
"value": f"{next_kw:.3f}",
|
|
329
|
-
"measurand": "Energy.Active.Import.Register",
|
|
330
|
-
"unit": "kW",
|
|
331
|
-
"context": "Sample.Clock",
|
|
332
|
-
}
|
|
333
|
-
],
|
|
334
|
-
}
|
|
335
|
-
],
|
|
336
|
-
},
|
|
337
|
-
]
|
|
338
|
-
)
|
|
339
|
-
state.last_message = "MeterValues"
|
|
340
|
-
await _recv()
|
|
341
|
-
last_mv = time.monotonic()
|
|
342
|
-
|
|
343
252
|
async def listen():
|
|
344
253
|
try:
|
|
345
254
|
while True:
|
|
@@ -663,6 +572,7 @@ def simulate(
|
|
|
663
572
|
interval,
|
|
664
573
|
username,
|
|
665
574
|
password,
|
|
575
|
+
sim_state=state,
|
|
666
576
|
)
|
|
667
577
|
|
|
668
578
|
def run_thread(idx: int) -> None:
|
|
@@ -685,6 +595,7 @@ def simulate(
|
|
|
685
595
|
interval,
|
|
686
596
|
username,
|
|
687
597
|
password,
|
|
598
|
+
sim_state=state,
|
|
688
599
|
)
|
|
689
600
|
)
|
|
690
601
|
|
|
@@ -737,6 +648,7 @@ def simulate(
|
|
|
737
648
|
interval,
|
|
738
649
|
username,
|
|
739
650
|
password,
|
|
651
|
+
sim_state=state,
|
|
740
652
|
)
|
|
741
653
|
)
|
|
742
654
|
else:
|
|
@@ -764,6 +676,7 @@ def simulate(
|
|
|
764
676
|
username,
|
|
765
677
|
password,
|
|
766
678
|
),
|
|
679
|
+
kwargs={"sim_state": state},
|
|
767
680
|
daemon=True,
|
|
768
681
|
)
|
|
769
682
|
t.start()
|
ocpp/models.py
CHANGED
|
@@ -628,6 +628,8 @@ class Transaction(Entity):
|
|
|
628
628
|
)
|
|
629
629
|
start_time = models.DateTimeField()
|
|
630
630
|
stop_time = models.DateTimeField(null=True, blank=True)
|
|
631
|
+
received_start_time = models.DateTimeField(null=True, blank=True)
|
|
632
|
+
received_stop_time = models.DateTimeField(null=True, blank=True)
|
|
631
633
|
|
|
632
634
|
def __str__(self) -> str: # pragma: no cover - simple representation
|
|
633
635
|
return f"{self.charger}:{self.pk}"
|
ocpp/routing.py
CHANGED
|
@@ -4,6 +4,8 @@ from . import consumers
|
|
|
4
4
|
|
|
5
5
|
websocket_urlpatterns = [
|
|
6
6
|
re_path(r"^ws/sink/$", consumers.SinkConsumer.as_asgi()),
|
|
7
|
-
# Accept connections at any path; the last segment is the charger ID
|
|
8
|
-
|
|
7
|
+
# Accept connections at any path; the last segment is the charger ID.
|
|
8
|
+
# Some charge points omit the final segment and only provide the
|
|
9
|
+
# identifier via query parameters, so allow an empty match here.
|
|
10
|
+
re_path(r"^(?:.*/)?(?P<cid>[^/]*)/?$", consumers.CSMSConsumer.as_asgi()),
|
|
9
11
|
]
|
ocpp/simulator.py
CHANGED
|
@@ -302,18 +302,39 @@ class ChargePointSimulator:
|
|
|
302
302
|
async def _handle_csms_call(self, msg, send, recv) -> bool:
|
|
303
303
|
if not isinstance(msg, list) or not msg or msg[0] != 2:
|
|
304
304
|
return False
|
|
305
|
+
message_id = msg[1] if len(msg) > 1 else ""
|
|
306
|
+
if not isinstance(message_id, str):
|
|
307
|
+
message_id = str(message_id)
|
|
305
308
|
action = msg[2]
|
|
306
309
|
payload = msg[3] if len(msg) > 3 else {}
|
|
307
310
|
if action == "ChangeAvailability":
|
|
308
|
-
await self._handle_change_availability(
|
|
311
|
+
await self._handle_change_availability(message_id, payload, send, recv)
|
|
309
312
|
return True
|
|
310
313
|
if action == "GetConfiguration":
|
|
311
|
-
await self._handle_get_configuration(
|
|
314
|
+
await self._handle_get_configuration(message_id, payload, send)
|
|
312
315
|
return True
|
|
313
316
|
if action == "TriggerMessage":
|
|
314
|
-
await self._handle_trigger_message(
|
|
317
|
+
await self._handle_trigger_message(message_id, payload, send, recv)
|
|
315
318
|
return True
|
|
316
|
-
|
|
319
|
+
cfg = self.config
|
|
320
|
+
action_name = str(action)
|
|
321
|
+
store.add_log(
|
|
322
|
+
cfg.cp_path,
|
|
323
|
+
f"Received unsupported action '{action_name}', replying with CallError",
|
|
324
|
+
log_type="simulator",
|
|
325
|
+
)
|
|
326
|
+
await send(
|
|
327
|
+
json.dumps(
|
|
328
|
+
[
|
|
329
|
+
4,
|
|
330
|
+
message_id,
|
|
331
|
+
"NotSupported",
|
|
332
|
+
f"Simulator does not implement {action_name}",
|
|
333
|
+
{},
|
|
334
|
+
]
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
return True
|
|
317
338
|
|
|
318
339
|
async def _handle_get_configuration(self, message_id: str, payload, send) -> None:
|
|
319
340
|
cfg = self.config
|
|
@@ -523,11 +544,11 @@ class ChargePointSimulator:
|
|
|
523
544
|
],
|
|
524
545
|
},
|
|
525
546
|
]
|
|
547
|
+
)
|
|
526
548
|
)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
await asyncio.sleep(cfg.interval)
|
|
549
|
+
await recv()
|
|
550
|
+
await self._maybe_send_door_event(send, recv)
|
|
551
|
+
await asyncio.sleep(cfg.interval)
|
|
531
552
|
|
|
532
553
|
if not await self._wait_until_operative(send, recv):
|
|
533
554
|
return
|
ocpp/status_display.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Shared status display constants for charger views and admin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.utils.translation import gettext_lazy as _
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Map of normalized OCPP status values to human readable labels and colors.
|
|
9
|
+
STATUS_BADGE_MAP: dict[str, tuple[str, str]] = {
|
|
10
|
+
"available": (_("Available"), "#0d6efd"),
|
|
11
|
+
"preparing": (_("Preparing"), "#0d6efd"),
|
|
12
|
+
"charging": (_("Charging"), "#198754"),
|
|
13
|
+
"suspendedevse": (_("Suspended (EVSE)"), "#fd7e14"),
|
|
14
|
+
"suspendedev": (_("Suspended (EV)"), "#fd7e14"),
|
|
15
|
+
"finishing": (_("Finishing"), "#20c997"),
|
|
16
|
+
"faulted": (_("Faulted"), "#dc3545"),
|
|
17
|
+
"unavailable": (_("Unavailable"), "#6c757d"),
|
|
18
|
+
"reserved": (_("Reserved"), "#6f42c1"),
|
|
19
|
+
"occupied": (_("Occupied"), "#0dcaf0"),
|
|
20
|
+
"outofservice": (_("Out of Service"), "#6c757d"),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Error codes that indicate "no error" according to the OCPP specification.
|
|
25
|
+
ERROR_OK_VALUES = {"", "noerror", "no_error"}
|
|
26
|
+
|