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.

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
- re_path(r"^(?:.*/)?(?P<cid>[^/]+)/?$", consumers.CSMSConsumer.as_asgi()),
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(msg[1], payload, send, recv)
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(msg[1], payload, send)
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(msg[1], payload, send, recv)
317
+ await self._handle_trigger_message(message_id, payload, send, recv)
315
318
  return True
316
- return False
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
- await recv()
529
- await self._maybe_send_door_event(send, recv)
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
+