1
- """Representation of a OCCP Charge Point ."""
1
+ """Representation of a OCCP Entities ."""
2
2
import asyncio
3
3
from datetime import datetime
4
4
import logging
5
5
import time
6
6
from typing import Dict
7
7
8
+ from homeassistant .config_entries import ConfigEntry
8
9
from homeassistant .const import TIME_MINUTES
10
+ from homeassistant .core import HomeAssistant
11
+ from homeassistant .helpers import device_registry
9
12
import websockets
10
13
11
14
from ocpp .exceptions import NotImplementedError
30
33
)
31
34
32
35
from .const import (
36
+ CONF_CPID ,
37
+ CONF_CSID ,
38
+ CONF_HOST ,
33
39
CONF_METER_INTERVAL ,
34
40
CONF_MONITORED_VARIABLES ,
41
+ CONF_PORT ,
42
+ CONF_SUBPROTOCOL ,
43
+ DEFAULT_CPID ,
44
+ DEFAULT_CSID ,
35
45
DEFAULT_ENERGY_UNIT ,
46
+ DEFAULT_HOST ,
36
47
DEFAULT_MEASURAND ,
48
+ DEFAULT_PORT ,
37
49
DEFAULT_POWER_UNIT ,
50
+ DEFAULT_SUBPROTOCOL ,
38
51
DOMAIN ,
39
52
FEATURE_PROFILE_REMOTE ,
40
53
HA_ENERGY_UNIT ,
41
54
HA_POWER_UNIT ,
42
55
SLEEP_TIME ,
43
56
)
44
57
45
- # from .exception import ConfigurationError
46
-
47
- _LOGGER = logging .getLogger (__name__ )
58
+ _LOGGER : logging .Logger = logging .getLogger (__package__ )
48
59
logging .getLogger (DOMAIN ).setLevel (logging .DEBUG )
49
60
50
61
62
+ class CentralSystem :
63
+ """Server for handling OCPP connections."""
64
+
65
+ def __init__ (self , hass : HomeAssistant , entry : ConfigEntry ):
66
+ """Instantiate instance of a CentralSystem."""
67
+ self .hass = hass
68
+ self .entry = entry
69
+ self .host = entry .data .get (CONF_HOST ) or DEFAULT_HOST
70
+ self .port = entry .data .get (CONF_PORT ) or DEFAULT_PORT
71
+ self .csid = entry .data .get (CONF_CSID ) or DEFAULT_CSID
72
+ self .cpid = entry .data .get (CONF_CPID ) or DEFAULT_CPID
73
+
74
+ self .subprotocol = entry .data .get (CONF_SUBPROTOCOL ) or DEFAULT_SUBPROTOCOL
75
+ self ._server = None
76
+ self .config = entry .data
77
+ self .id = entry .entry_id
78
+ self .charge_points = {}
79
+
80
+ @staticmethod
81
+ async def create (hass : HomeAssistant , entry : ConfigEntry ):
82
+ """Create instance and start listening for OCPP connections on given port."""
83
+ self = CentralSystem (hass , entry )
84
+
85
+ server = await websockets .serve (
86
+ self .on_connect , self .host , self .port , subprotocols = self .subprotocol
87
+ )
88
+ self ._server = server
89
+ return self
90
+
91
+ async def on_connect (self , websocket , path : str ):
92
+ """Request handler executed for every new OCPP connection."""
93
+
94
+ _LOGGER .info (f"path={ path } " )
95
+ cp_id = path .strip ("/" )
96
+ try :
97
+ if cp_id not in self .charge_points :
98
+ _LOGGER .info (f"Charger { cp_id } connected to { self .host } :{ self .port } ." )
99
+ cp = ChargePoint (cp_id , websocket , self .hass , self .entry , self )
100
+ self .charge_points [cp_id ] = cp
101
+ await cp .start ()
102
+ else :
103
+ _LOGGER .info (f"Charger { cp_id } reconnected to { self .host } :{ self .port } ." )
104
+ cp = self .charge_points [cp_id ]
105
+ await cp .reconnect (websocket )
106
+ except Exception as e :
107
+ _LOGGER .info (f"Exception occurred:\n { e } " )
108
+ finally :
109
+ _LOGGER .info (f"Charger { cp_id } disconnected from { self .host } :{ self .port } ." )
110
+
111
+ def get_metric (self , cp_id : str , measurand : str ):
112
+ """Return last known value for given measurand."""
113
+ if cp_id in self .charge_points :
114
+ return self .charge_points [cp_id ].get_metric (measurand )
115
+ return None
116
+
117
+ def get_unit (self , cp_id : str , measurand : str ):
118
+ """Return unit of given measurand."""
119
+ if cp_id in self .charge_points :
120
+ return self .charge_points [cp_id ].get_unit (measurand )
121
+ return None
122
+
123
+ def device_info (self ):
124
+ """Return device information."""
125
+ return {
126
+ "identifiers" : {(DOMAIN , self .id )},
127
+ }
128
+
129
+
51
130
class ChargePoint (cp ):
52
131
"""Server side representation of a charger."""
53
132
54
- def __init__ (self , id , connection , config , interval_meter_metrics : int = 10 ):
133
+ def __init__ (
134
+ self ,
135
+ id : str ,
136
+ connection ,
137
+ hass : HomeAssistant ,
138
+ entry : ConfigEntry ,
139
+ central : CentralSystem ,
140
+ interval_meter_metrics : int = 10 ,
141
+ ):
55
142
"""Instantiate instance of a ChargePoint."""
56
143
super ().__init__ (id , connection )
57
144
self .interval_meter_metrics = interval_meter_metrics
58
- self .config = config
145
+ self .hass = hass
146
+ self .entry = entry
147
+ self .central = central
59
148
self .status = "init"
60
149
# Indicates if the charger requires a reboot to apply new
61
150
# configuration.
@@ -82,17 +171,17 @@ async def post_connect(self):
82
171
await self .configure ("WebSocketPingInterval" , "60" )
83
172
await self .configure (
84
173
"MeterValuesSampledData" ,
85
- self .config [CONF_MONITORED_VARIABLES ],
174
+ self .entry . data [CONF_MONITORED_VARIABLES ],
86
175
)
87
176
await self .configure (
88
- "MeterValueSampleInterval" , str (self .config [CONF_METER_INTERVAL ])
177
+ "MeterValueSampleInterval" , str (self .entry . data [CONF_METER_INTERVAL ])
89
178
)
90
179
# await self.configure(
91
- # "StopTxnSampledData", ",".join(self.config [CONF_MONITORED_VARIABLES])
180
+ # "StopTxnSampledData", ",".join(self.entry.data [CONF_MONITORED_VARIABLES])
92
181
# )
93
182
resp = await self .get_configuration ("NumberOfConnectors" )
94
183
self ._metrics ["Connectors" ] = resp .configuration_key [0 ]["value" ]
95
- # await self.start_transaction()
184
+ # await self.start_transaction()
96
185
except (NotImplementedError ) as e :
97
186
_LOGGER .error ("Configuration of the charger failed: %s" , e )
98
187
@@ -262,16 +351,11 @@ async def start(self):
262
351
await asyncio .gather (super ().start (), self .post_connect ())
263
352
except websockets .exceptions .ConnectionClosed as e :
264
353
_LOGGER .debug (e )
265
- return self ._metrics
266
354
267
- async def reconnect (self , last_metrics ):
355
+ async def reconnect (self , connection ):
268
356
"""Reconnect charge point."""
269
- try :
270
- self ._metrics = last_metrics
271
- await asyncio .gather (super ().start ())
272
- except websockets .exceptions .ConnectionClosed as e :
273
- _LOGGER .debug (e )
274
- return self ._metrics
357
+ self ._connection = connection
358
+ await self .start ()
275
359
276
360
@on (Action .MeterValues )
277
361
def on_meter_values (self , connector_id : int , meter_value : Dict , ** kwargs ):
@@ -312,14 +396,36 @@ def on_meter_values(self, connector_id: int, meter_value: Dict, **kwargs):
312
396
)
313
397
return call_result .MeterValuesPayload ()
314
398
399
+ async def async_update_device_info (self , boot_info : dict ):
400
+ """Update device info asynchronuously."""
401
+
402
+ _LOGGER .debug ("Updating device info %s: %s" , self .id , boot_info )
403
+
404
+ dr = await device_registry .async_get_registry (self .hass )
405
+
406
+ serial = boot_info .get ("charge_point_serial_number" , None )
407
+
408
+ identifiers = {(DOMAIN , self .id )}
409
+ if serial is not None :
410
+ identifiers .add ((DOMAIN , serial ))
411
+
412
+ dr .async_get_or_create (
413
+ config_entry_id = self .entry .entry_id ,
414
+ identifiers = identifiers ,
415
+ name = self .id ,
416
+ manufacturer = boot_info .get ("charge_point_vendor" , None ),
417
+ model = boot_info .get ("charge_point_model" , None ),
418
+ sw_version = boot_info .get ("firmware_version" , None ),
419
+ )
420
+
315
421
@on (Action .BootNotification )
316
- def on_boot_notification (self , charge_point_model , charge_point_vendor , ** kwargs ):
422
+ def on_boot_notification (self , ** kwargs ):
317
423
"""Handle a boot notification."""
318
- self . _metrics [ "Model" ] = charge_point_model
319
- self . _metrics [ "Vendor" ] = charge_point_vendor
320
- self . _metrics [ "FW.Version" ] = kwargs . get ( "firmware_version" )
321
- self ._metrics [ "Serial" ] = kwargs . get ( "charge_point_serial_number" )
322
- _LOGGER . debug ( "Additional boot info for %s: %s" , self . id , kwargs )
424
+
425
+ _LOGGER . debug ( "Received boot notification for %s: %s" , self . id , kwargs )
426
+
427
+ asyncio . create_task ( self .async_update_device_info ( kwargs ) )
428
+
323
429
return call_result .BootNotificationPayload (
324
430
current_time = datetime .utcnow ().isoformat (),
325
431
interval = 30 ,
0 commit comments