Skip to content

Commit c662b1f

Browse files
author
lbbrhzn
committed
Merge branch 'add-switch-and-device-information' into main
2 parents 9d7634a + 88014d5 commit c662b1f

File tree

10 files changed

+339
-140
lines changed

10 files changed

+339
-140
lines changed

custom_components/ocpp/__init__.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66
from homeassistant.config_entries import ConfigEntry
77
from homeassistant.core import Config, HomeAssistant
8+
from homeassistant.helpers import device_registry
89

9-
from .central_system import CentralSystem
10-
from .const import CONF_HOST, CONF_PORT, DOMAIN, PLATFORMS
10+
from .api import CentralSystem
11+
from .const import CONF_CPID, CONF_CSID, DOMAIN, PLATFORMS
1112

1213
_LOGGER: logging.Logger = logging.getLogger(__package__)
1314
logging.getLogger(DOMAIN).setLevel(logging.DEBUG)
@@ -19,16 +20,30 @@ async def async_setup(hass: HomeAssistant, config: Config):
1920

2021

2122
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
22-
"""Set up this integration using UI."""
23+
"""Set up this integration from config entry."""
2324
if hass.data.get(DOMAIN) is None:
2425
hass.data.setdefault(DOMAIN, {})
2526
_LOGGER.info(entry.data)
2627

27-
cfg_host = entry.data.get(CONF_HOST)
28-
cfg_port = entry.data.get(CONF_PORT)
28+
central_sys = await CentralSystem.create(hass, entry)
2929

30-
central_sys = await CentralSystem.create(
31-
entry.entry_id, entry.data, host=cfg_host, port=cfg_port
30+
dr = await device_registry.async_get_registry(hass)
31+
32+
""" Create Central System Device """
33+
dr.async_get_or_create(
34+
config_entry_id=entry.entry_id,
35+
identifiers={(DOMAIN, entry.data[CONF_CSID])},
36+
name=entry.data[CONF_CSID],
37+
model="OCPP Central System",
38+
)
39+
40+
""" Create Charge Point Device """
41+
dr.async_get_or_create(
42+
config_entry_id=entry.entry_id,
43+
identifiers={(DOMAIN, entry.data[CONF_CPID])},
44+
name=entry.data[CONF_CPID],
45+
default_model="OCPP Charge Point",
46+
via_device=((DOMAIN), central_sys.id),
3247
)
3348

3449
hass.data[DOMAIN][entry.entry_id] = central_sys

custom_components/ocpp/charge_point.py renamed to custom_components/ocpp/api.py

Lines changed: 130 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
"""Representation of a OCCP Charge Point."""
1+
"""Representation of a OCCP Entities."""
22
import asyncio
33
from datetime import datetime
44
import logging
55
import time
66
from typing import Dict
77

8+
from homeassistant.config_entries import ConfigEntry
89
from homeassistant.const import TIME_MINUTES
10+
from homeassistant.core import HomeAssistant
11+
from homeassistant.helpers import device_registry
912
import websockets
1013

1114
from ocpp.exceptions import NotImplementedError
@@ -30,32 +33,118 @@
3033
)
3134

3235
from .const import (
36+
CONF_CPID,
37+
CONF_CSID,
38+
CONF_HOST,
3339
CONF_METER_INTERVAL,
3440
CONF_MONITORED_VARIABLES,
41+
CONF_PORT,
42+
CONF_SUBPROTOCOL,
43+
DEFAULT_CPID,
44+
DEFAULT_CSID,
3545
DEFAULT_ENERGY_UNIT,
46+
DEFAULT_HOST,
3647
DEFAULT_MEASURAND,
48+
DEFAULT_PORT,
3749
DEFAULT_POWER_UNIT,
50+
DEFAULT_SUBPROTOCOL,
3851
DOMAIN,
3952
FEATURE_PROFILE_REMOTE,
4053
HA_ENERGY_UNIT,
4154
HA_POWER_UNIT,
4255
SLEEP_TIME,
4356
)
4457

45-
# from .exception import ConfigurationError
46-
47-
_LOGGER = logging.getLogger(__name__)
58+
_LOGGER: logging.Logger = logging.getLogger(__package__)
4859
logging.getLogger(DOMAIN).setLevel(logging.DEBUG)
4960

5061

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+
51130
class ChargePoint(cp):
52131
"""Server side representation of a charger."""
53132

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+
):
55142
"""Instantiate instance of a ChargePoint."""
56143
super().__init__(id, connection)
57144
self.interval_meter_metrics = interval_meter_metrics
58-
self.config = config
145+
self.hass = hass
146+
self.entry = entry
147+
self.central = central
59148
self.status = "init"
60149
# Indicates if the charger requires a reboot to apply new
61150
# configuration.
@@ -82,17 +171,17 @@ async def post_connect(self):
82171
await self.configure("WebSocketPingInterval", "60")
83172
await self.configure(
84173
"MeterValuesSampledData",
85-
self.config[CONF_MONITORED_VARIABLES],
174+
self.entry.data[CONF_MONITORED_VARIABLES],
86175
)
87176
await self.configure(
88-
"MeterValueSampleInterval", str(self.config[CONF_METER_INTERVAL])
177+
"MeterValueSampleInterval", str(self.entry.data[CONF_METER_INTERVAL])
89178
)
90179
# await self.configure(
91-
# "StopTxnSampledData", ",".join(self.config[CONF_MONITORED_VARIABLES])
180+
# "StopTxnSampledData", ",".join(self.entry.data[CONF_MONITORED_VARIABLES])
92181
# )
93182
resp = await self.get_configuration("NumberOfConnectors")
94183
self._metrics["Connectors"] = resp.configuration_key[0]["value"]
95-
# await self.start_transaction()
184+
# await self.start_transaction()
96185
except (NotImplementedError) as e:
97186
_LOGGER.error("Configuration of the charger failed: %s", e)
98187

@@ -262,16 +351,11 @@ async def start(self):
262351
await asyncio.gather(super().start(), self.post_connect())
263352
except websockets.exceptions.ConnectionClosed as e:
264353
_LOGGER.debug(e)
265-
return self._metrics
266354

267-
async def reconnect(self, last_metrics):
355+
async def reconnect(self, connection):
268356
"""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()
275359

276360
@on(Action.MeterValues)
277361
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):
312396
)
313397
return call_result.MeterValuesPayload()
314398

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+
315421
@on(Action.BootNotification)
316-
def on_boot_notification(self, charge_point_model, charge_point_vendor, **kwargs):
422+
def on_boot_notification(self, **kwargs):
317423
"""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+
323429
return call_result.BootNotificationPayload(
324430
current_time=datetime.utcnow().isoformat(),
325431
interval=30,

custom_components/ocpp/central_system.py

Lines changed: 0 additions & 71 deletions
This file was deleted.

custom_components/ocpp/config_flow.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,28 @@
33
import voluptuous as vol
44

55
from .const import (
6+
CONF_CPID,
7+
CONF_CSID,
68
CONF_HOST,
79
CONF_METER_INTERVAL,
810
CONF_MONITORED_VARIABLES,
9-
CONF_NAME,
1011
CONF_PORT,
12+
DEFAULT_CPID,
13+
DEFAULT_CSID,
1114
DEFAULT_HOST,
1215
DEFAULT_MEASURAND,
1316
DEFAULT_METER_INTERVAL,
14-
DEFAULT_NAME,
1517
DEFAULT_PORT,
1618
DOMAIN,
1719
MEASURANDS,
1820
)
1921

2022
STEP_USER_DATA_SCHEMA = vol.Schema(
2123
{
22-
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
2324
vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
2425
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
26+
vol.Required(CONF_CSID, default=DEFAULT_CSID): str,
27+
vol.Required(CONF_CPID, default=DEFAULT_CPID): str,
2528
vol.Required(CONF_METER_INTERVAL, default=DEFAULT_METER_INTERVAL): int,
2629
}
2730
)
@@ -65,7 +68,7 @@ async def async_step_measurands(self, user_input=None):
6568
if set(selected_measurands).issubset(set(MEASURANDS)):
6669
self._data[CONF_MONITORED_VARIABLES] = ",".join(selected_measurands)
6770
return self.async_create_entry(
68-
title=self._data[CONF_NAME], data=self._data
71+
title=self._data[CONF_CSID], data=self._data
6972
)
7073
else:
7174
errors["base"] = "measurand"

0 commit comments

Comments
 (0)