Skip to content

Commit bcfe97b

Browse files
author
Jan Thunqvist
committed
Add test for v2x multi connector. Handle meter values properly. max_current working per connector.
1 parent 5791af0 commit bcfe97b

File tree

10 files changed

+1042
-306
lines changed

10 files changed

+1042
-306
lines changed

custom_components/ocpp/api.py

Lines changed: 174 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import contextlib
66
import json
77
import logging
8+
import re
89
import ssl
910

1011
from functools import partial
@@ -90,6 +91,10 @@
9091
)
9192

9293

94+
def _norm(s: str) -> str:
95+
return re.sub(r"[^a-z0-9]", "", str(s).lower())
96+
97+
9398
class CentralSystem:
9499
"""Server for handling OCPP connections."""
95100

@@ -192,7 +197,7 @@ async def create(hass: HomeAssistant, entry: ConfigEntry):
192197

193198
@staticmethod
194199
def _norm_conn(connector_id: int | None) -> int:
195-
if connector_id in (None, 0):
200+
if connector_id is None:
196201
return 0
197202
try:
198203
return int(connector_id)
@@ -293,20 +298,50 @@ def _get_metrics(self, id: str):
293298
def get_metric(self, id: str, measurand: str, connector_id: int | None = None):
294299
"""Return last known value for given measurand."""
295300
# allow id to be either cpid or cp_id
296-
cp_id, m = self._get_metrics(id)
297-
298-
if m is None:
301+
cp_id = self.cpids.get(id, id)
302+
if cp_id not in self.charge_points:
299303
return None
300304

301-
conn = self._norm_conn(connector_id)
302-
try:
303-
return m[(conn, measurand)].value
304-
except Exception:
305-
if conn == 0:
306-
with contextlib.suppress(Exception):
307-
return m[measurand].value
305+
cp = self.charge_points[cp_id]
306+
m = cp._metrics
307+
n_connectors = getattr(cp, "num_connectors", 1) or 1
308+
309+
def _try_val(key):
310+
with contextlib.suppress(Exception):
311+
val = m[key].value
312+
return val
308313
return None
309314

315+
# 1) Explicit connector_id (including 0): just get it
316+
if connector_id is not None:
317+
conn = 0 if connector_id == 0 else connector_id
318+
return _try_val((conn, measurand))
319+
320+
# 2) No connector_id: try CHARGER level (conn=0)
321+
val = _try_val((0, measurand))
322+
if val is not None:
323+
return val
324+
325+
# 3) Legacy "flat" key (before the connector support)
326+
with contextlib.suppress(Exception):
327+
val = m[measurand].value
328+
if val is not None:
329+
return val
330+
331+
# 4) Fallback to connector 1 (old tests often expect this)
332+
if n_connectors >= 1:
333+
val = _try_val((1, measurand))
334+
if val is not None:
335+
return val
336+
337+
# 5) Last resort: find the first connector 2..N with value
338+
for c in range(2, int(n_connectors) + 1):
339+
val = _try_val((c, measurand))
340+
if val is not None:
341+
return val
342+
343+
return None
344+
310345
def del_metric(self, id: str, measurand: str, connector_id: int | None = None):
311346
"""Set given measurand to None."""
312347
# allow id to be either cpid or cp_id
@@ -326,47 +361,125 @@ def del_metric(self, id: str, measurand: str, connector_id: int | None = None):
326361
def get_unit(self, id: str, measurand: str, connector_id: int | None = None):
327362
"""Return unit of given measurand."""
328363
# allow id to be either cpid or cp_id
329-
cp_id, m = self._get_metrics(id)
330-
if m is None:
364+
cp_id = self.cpids.get(id, id)
365+
if cp_id not in self.charge_points:
331366
return None
332-
conn = self._norm_conn(connector_id)
333-
try:
334-
return m[(conn, measurand)].unit
335-
except Exception:
336-
if conn == 0:
337-
with contextlib.suppress(Exception):
338-
return m[measurand].unit
367+
368+
cp = self.charge_points[cp_id]
369+
m = cp._metrics
370+
n_connectors = getattr(cp, "num_connectors", 1) or 1
371+
372+
def _try_unit(key):
373+
with contextlib.suppress(Exception):
374+
return m[key].unit
339375
return None
340376

377+
if connector_id is not None:
378+
conn = 0 if connector_id == 0 else connector_id
379+
return _try_unit((conn, measurand))
380+
381+
val = _try_unit((0, measurand))
382+
if val is not None:
383+
return val
384+
385+
with contextlib.suppress(Exception):
386+
val = m[measurand].unit
387+
if val is not None:
388+
return val
389+
390+
if n_connectors >= 1:
391+
val = _try_unit((1, measurand))
392+
if val is not None:
393+
return val
394+
395+
for c in range(2, int(n_connectors) + 1):
396+
val = _try_unit((c, measurand))
397+
if val is not None:
398+
return val
399+
400+
return None
401+
341402
def get_ha_unit(self, id: str, measurand: str, connector_id: int | None = None):
342403
"""Return home assistant unit of given measurand."""
343-
cp_id, m = self._get_metrics(id)
344-
if m is None:
404+
cp_id = self.cpids.get(id, id)
405+
if cp_id not in self.charge_points:
345406
return None
346-
conn = self._norm_conn(connector_id)
347-
try:
348-
return m[(conn, measurand)].ha_unit
349-
except Exception:
350-
if conn == 0:
351-
with contextlib.suppress(Exception):
352-
return m[measurand].ha_unit
407+
408+
cp = self.charge_points[cp_id]
409+
m = cp._metrics
410+
n_connectors = getattr(cp, "num_connectors", 1) or 1
411+
412+
def _try_ha_unit(key):
413+
with contextlib.suppress(Exception):
414+
return m[key].ha_unit
353415
return None
354416

417+
if connector_id is not None:
418+
conn = 0 if connector_id == 0 else connector_id
419+
return _try_ha_unit((conn, measurand))
420+
421+
val = _try_ha_unit((0, measurand))
422+
if val is not None:
423+
return val
424+
425+
with contextlib.suppress(Exception):
426+
val = m[measurand].ha_unit
427+
if val is not None:
428+
return val
429+
430+
if n_connectors >= 1:
431+
val = _try_ha_unit((1, measurand))
432+
if val is not None:
433+
return val
434+
435+
for c in range(2, int(n_connectors) + 1):
436+
val = _try_ha_unit((c, measurand))
437+
if val is not None:
438+
return val
439+
440+
return None
441+
355442
def get_extra_attr(self, id: str, measurand: str, connector_id: int | None = None):
356-
"""Return last known extra attributes for given measurand."""
443+
"""Return extra attributes for given measurand."""
357444
# allow id to be either cpid or cp_id
358-
cp_id, m = self._get_metrics(id)
359-
if m is None:
445+
cp_id = self.cpids.get(id, id)
446+
if cp_id not in self.charge_points:
360447
return None
361-
conn = self._norm_conn(connector_id)
362-
try:
363-
return m[(conn, measurand)].extra_attr
364-
except Exception:
365-
if conn == 0:
366-
with contextlib.suppress(Exception):
367-
return m[measurand].extra_attr
448+
449+
cp = self.charge_points[cp_id]
450+
m = cp._metrics
451+
n_connectors = getattr(cp, "num_connectors", 1) or 1
452+
453+
def _try_extra(key):
454+
with contextlib.suppress(Exception):
455+
return m[key].extra_attr
368456
return None
369457

458+
if connector_id is not None:
459+
conn = 0 if connector_id == 0 else connector_id
460+
return _try_extra((conn, measurand))
461+
462+
val = _try_extra((0, measurand))
463+
if val is not None:
464+
return val
465+
466+
with contextlib.suppress(Exception):
467+
val = m[measurand].extra_attr
468+
if val is not None:
469+
return val
470+
471+
if n_connectors >= 1:
472+
val = _try_extra((1, measurand))
473+
if val is not None:
474+
return val
475+
476+
for c in range(2, int(n_connectors) + 1):
477+
val = _try_extra((c, measurand))
478+
if val is not None:
479+
return val
480+
481+
return None
482+
370483
def get_available(self, id: str, connector_id: int | None = None):
371484
"""Return whether the charger (or a specific connector) is available."""
372485
# allow id to be either cpid or cp_id
@@ -397,7 +510,7 @@ def get_available(self, id: str, connector_id: int | None = None):
397510
if not status_val:
398511
return cp.status == STATE_OK
399512

400-
ok_statuses = {
513+
ok_statuses_norm = {
401514
"available",
402515
"preparing",
403516
"charging",
@@ -408,7 +521,7 @@ def get_available(self, id: str, connector_id: int | None = None):
408521
"reserved",
409522
}
410523

411-
ret = str(status_val).lower() in ok_statuses
524+
ret = _norm(status_val) in ok_statuses_norm
412525
return ret
413526

414527
def get_supported_features(self, id: str):
@@ -420,32 +533,46 @@ def get_supported_features(self, id: str):
420533
return self.charge_points[cp_id].supported_features
421534
return 0
422535

423-
async def set_max_charge_rate_amps(self, id: str, value: float):
536+
async def set_max_charge_rate_amps(
537+
self, id: str, value: float, connector_id: int = 0
538+
):
424539
"""Set the maximum charge rate in amps."""
425540
# allow id to be either cpid or cp_id
426541
cp_id = self.cpids.get(id, id)
427542

428543
if cp_id in self.charge_points:
429-
return await self.charge_points[cp_id].set_charge_rate(limit_amps=value)
544+
return await self.charge_points[cp_id].set_charge_rate(
545+
limit_amps=value, conn_id=connector_id
546+
)
430547
return False
431548

432-
async def set_charger_state(self, id: str, service_name: str, state: bool = True):
549+
async def set_charger_state(
550+
self,
551+
id: str,
552+
service_name: str,
553+
state: bool = True,
554+
connector_id: int | None = 1,
555+
):
433556
"""Carry out requested service/state change on connected charger."""
434557
# allow id to be either cpid or cp_id
435558
cp_id = self.cpids.get(id, id)
436559

437560
resp = False
438561
if cp_id in self.charge_points:
439562
if service_name == csvcs.service_availability.name:
440-
resp = await self.charge_points[cp_id].set_availability(state)
563+
resp = await self.charge_points[cp_id].set_availability(
564+
state, connector_id=connector_id
565+
)
441566
if service_name == csvcs.service_charge_start.name:
442-
resp = await self.charge_points[cp_id].start_transaction()
567+
resp = await self.charge_points[cp_id].start_transaction(
568+
connector_id=connector_id
569+
)
443570
if service_name == csvcs.service_charge_stop.name:
444571
resp = await self.charge_points[cp_id].stop_transaction()
445572
if service_name == csvcs.service_reset.name:
446573
resp = await self.charge_points[cp_id].reset()
447574
if service_name == csvcs.service_unlock.name:
448-
resp = await self.charge_points[cp_id].unlock()
575+
resp = await self.charge_points[cp_id].unlock(connector_id=connector_id)
449576
return resp
450577

451578
def device_info(self):

custom_components/ocpp/button.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async def async_setup_entry(hass, entry, async_add_devices):
5858
num_connectors = int(cp_id_settings.get(CONF_NUM_CONNECTORS, 1))
5959

6060
for desc in BUTTONS:
61-
if desc.per_connector and num_connectors > 1:
61+
if desc.per_connector:
6262
for connector_id in range(1, num_connectors + 1):
6363
entities.append(
6464
ChargePointButton(
@@ -124,5 +124,7 @@ def available(self) -> bool:
124124
async def async_press(self) -> None:
125125
"""Triggers the charger press action service."""
126126
await self.central_system.set_charger_state(
127-
self.cpid, self.entity_description.press_action
127+
self.cpid,
128+
self.entity_description.press_action,
129+
connector_id=self.connector_id,
128130
)

0 commit comments

Comments
 (0)