5
5
import contextlib
6
6
import json
7
7
import logging
8
+ import re
8
9
import ssl
9
10
10
11
from functools import partial
90
91
)
91
92
92
93
94
+ def _norm (s : str ) -> str :
95
+ return re .sub (r"[^a-z0-9]" , "" , str (s ).lower ())
96
+
97
+
93
98
class CentralSystem :
94
99
"""Server for handling OCPP connections."""
95
100
@@ -192,7 +197,7 @@ async def create(hass: HomeAssistant, entry: ConfigEntry):
192
197
193
198
@staticmethod
194
199
def _norm_conn (connector_id : int | None ) -> int :
195
- if connector_id in ( None , 0 ) :
200
+ if connector_id is None :
196
201
return 0
197
202
try :
198
203
return int (connector_id )
@@ -293,20 +298,50 @@ def _get_metrics(self, id: str):
293
298
def get_metric (self , id : str , measurand : str , connector_id : int | None = None ):
294
299
"""Return last known value for given measurand."""
295
300
# 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 :
299
303
return None
300
304
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
308
313
return None
309
314
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
+
310
345
def del_metric (self , id : str , measurand : str , connector_id : int | None = None ):
311
346
"""Set given measurand to None."""
312
347
# 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):
326
361
def get_unit (self , id : str , measurand : str , connector_id : int | None = None ):
327
362
"""Return unit of given measurand."""
328
363
# 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 :
331
366
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
339
375
return None
340
376
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
+
341
402
def get_ha_unit (self , id : str , measurand : str , connector_id : int | None = None ):
342
403
"""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 :
345
406
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
353
415
return None
354
416
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
+
355
442
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."""
357
444
# 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 :
360
447
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
368
456
return None
369
457
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
+
370
483
def get_available (self , id : str , connector_id : int | None = None ):
371
484
"""Return whether the charger (or a specific connector) is available."""
372
485
# allow id to be either cpid or cp_id
@@ -397,7 +510,7 @@ def get_available(self, id: str, connector_id: int | None = None):
397
510
if not status_val :
398
511
return cp .status == STATE_OK
399
512
400
- ok_statuses = {
513
+ ok_statuses_norm = {
401
514
"available" ,
402
515
"preparing" ,
403
516
"charging" ,
@@ -408,7 +521,7 @@ def get_available(self, id: str, connector_id: int | None = None):
408
521
"reserved" ,
409
522
}
410
523
411
- ret = str (status_val ). lower () in ok_statuses
524
+ ret = _norm (status_val ) in ok_statuses_norm
412
525
return ret
413
526
414
527
def get_supported_features (self , id : str ):
@@ -420,32 +533,46 @@ def get_supported_features(self, id: str):
420
533
return self .charge_points [cp_id ].supported_features
421
534
return 0
422
535
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
+ ):
424
539
"""Set the maximum charge rate in amps."""
425
540
# allow id to be either cpid or cp_id
426
541
cp_id = self .cpids .get (id , id )
427
542
428
543
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
+ )
430
547
return False
431
548
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
+ ):
433
556
"""Carry out requested service/state change on connected charger."""
434
557
# allow id to be either cpid or cp_id
435
558
cp_id = self .cpids .get (id , id )
436
559
437
560
resp = False
438
561
if cp_id in self .charge_points :
439
562
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
+ )
441
566
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
+ )
443
570
if service_name == csvcs .service_charge_stop .name :
444
571
resp = await self .charge_points [cp_id ].stop_transaction ()
445
572
if service_name == csvcs .service_reset .name :
446
573
resp = await self .charge_points [cp_id ].reset ()
447
574
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 )
449
576
return resp
450
577
451
578
def device_info (self ):
0 commit comments