Skip to content

Commit d6ed8e9

Browse files
author
Jan Thunqvist
committed
Change entity id to include charger. Add session sensors to connectors. Clear charger-level entities after connector creation. Add num_connector changes to reload logic. Remove not used functions from chargepoint.py. Revert to original config flow due to automatic discovery of number of connectors. Add more tests.
1 parent 175e610 commit d6ed8e9

17 files changed

+2053
-170
lines changed

custom_components/ocpp/button.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,17 @@
1111
ButtonEntity,
1212
ButtonEntityDescription,
1313
)
14+
from homeassistant.helpers import entity_registry as er
1415
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
1516

1617
from .api import CentralSystem
17-
from .const import CONF_CPID, CONF_CPIDS, CONF_NUM_CONNECTORS, DOMAIN
18+
from .const import (
19+
CONF_CPID,
20+
CONF_CPIDS,
21+
CONF_NUM_CONNECTORS,
22+
DEFAULT_NUM_CONNECTORS,
23+
DOMAIN,
24+
)
1825
from .enums import HAChargerServices
1926

2027

@@ -50,12 +57,32 @@ async def async_setup_entry(hass, entry, async_add_devices):
5057
"""Configure the Button platform."""
5158
central_system: CentralSystem = hass.data[DOMAIN][entry.entry_id]
5259
entities: list[ChargePointButton] = []
60+
ent_reg = er.async_get(hass)
5361

5462
for charger in entry.data[CONF_CPIDS]:
5563
cp_id_settings = list(charger.values())[0]
5664
cpid = cp_id_settings[CONF_CPID]
5765

58-
num_connectors = int(cp_id_settings.get(CONF_NUM_CONNECTORS, 1) or 1)
66+
num_connectors = 1
67+
for item in entry.data.get(CONF_CPIDS, []):
68+
for _, cfg in item.items():
69+
if cfg.get(CONF_CPID) == cpid:
70+
num_connectors = int(
71+
cfg.get(CONF_NUM_CONNECTORS, DEFAULT_NUM_CONNECTORS)
72+
)
73+
break
74+
else:
75+
continue
76+
break
77+
78+
if num_connectors > 1:
79+
for desc in BUTTONS:
80+
if not desc.per_connector:
81+
continue
82+
uid_flat = ".".join([BUTTON_DOMAIN, DOMAIN, cpid, desc.key])
83+
stale_eid = ent_reg.async_get_entity_id(BUTTON_DOMAIN, DOMAIN, uid_flat)
84+
if stale_eid:
85+
ent_reg.async_remove(stale_eid)
5986

6087
for desc in BUTTONS:
6188
if desc.per_connector:
@@ -97,7 +124,7 @@ async def async_setup_entry(hass, entry, async_add_devices):
97124
class ChargePointButton(ButtonEntity):
98125
"""Individual button for charge point."""
99126

100-
_attr_has_entity_name = True
127+
_attr_has_entity_name = False
101128
entity_description: OcppButtonDescription
102129

103130
def __init__(
@@ -122,14 +149,19 @@ def __init__(
122149
if self.connector_id:
123150
self._attr_device_info = DeviceInfo(
124151
identifiers={(DOMAIN, f"{cpid}-conn{self.connector_id}")},
125-
name=f"Connector {self.connector_id}",
152+
name=f"{cpid} Connector {self.connector_id}",
126153
via_device=(DOMAIN, cpid),
127154
)
128155
else:
129156
self._attr_device_info = DeviceInfo(
130157
identifiers={(DOMAIN, cpid)},
131158
name=cpid,
132159
)
160+
if self.connector_id is not None:
161+
object_id = f"{self.cpid}_connector_{self.connector_id}_{self.entity_description.key}"
162+
else:
163+
object_id = f"{self.cpid}_{self.entity_description.key}"
164+
self.entity_id = f"{BUTTON_DOMAIN}.{object_id}"
133165

134166
@property
135167
def available(self) -> bool:

custom_components/ocpp/chargepoint.py

Lines changed: 22 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from ocpp.v16 import call as callv16
2727
from ocpp.v16 import call_result as call_resultv16
2828
from ocpp.v16.enums import (
29-
UnitOfMeasure,
3029
AuthorizationStatus,
3130
Measurand,
3231
Phase,
@@ -53,10 +52,12 @@
5352
CONF_DEFAULT_AUTH_STATUS,
5453
CONF_ID_TAG,
5554
CONF_MONITORED_VARIABLES,
55+
CONF_NUM_CONNECTORS,
5656
CONF_CPIDS,
5757
CONFIG,
5858
DATA_UPDATED,
5959
DEFAULT_ENERGY_UNIT,
60+
DEFAULT_NUM_CONNECTORS,
6061
DEFAULT_POWER_UNIT,
6162
DEFAULT_MEASURAND,
6263
DOMAIN,
@@ -276,15 +277,22 @@ def __init__(
276277

277278
# Init standard metrics for connector 0
278279
self._metrics[(0, cdet.identifier.value)].value = id
279-
self._metrics[(0, csess.session_time.value)].unit = TIME_MINUTES
280-
self._metrics[(0, csess.session_energy.value)].unit = UnitOfMeasure.kwh.value
281-
self._metrics[(0, csess.meter_start.value)].unit = UnitOfMeasure.kwh.value
282280
self._metrics[(0, cstat.reconnects.value)].value = 0
283281

284282
self._attr_supported_features = prof.NONE
285283
alphabet = string.ascii_uppercase + string.digits
286284
self._remote_id_tag = "".join(secrets.choice(alphabet) for i in range(20))
287-
self.num_connectors: int = 1
285+
self.num_connectors: int = DEFAULT_NUM_CONNECTORS
286+
287+
def _init_connector_slots(self, conn_id: int) -> None:
288+
"""Ensure connector-scoped metrics exist and carry the right units."""
289+
_ = self._metrics[(conn_id, cstat.status_connector.value)]
290+
_ = self._metrics[(conn_id, cstat.error_code_connector.value)]
291+
_ = self._metrics[(conn_id, csess.transaction_id.value)]
292+
293+
self._metrics[(conn_id, csess.session_time.value)].unit = TIME_MINUTES
294+
self._metrics[(conn_id, csess.session_energy.value)].unit = HA_ENERGY_UNIT
295+
self._metrics[(conn_id, csess.meter_start.value)].unit = HA_ENERGY_UNIT
288296

289297
async def get_number_of_connectors(self) -> int:
290298
"""Return number of connectors on this charger."""
@@ -320,29 +328,28 @@ async def post_connect(self):
320328
num_connectors: int = await self.get_number_of_connectors()
321329
self.num_connectors = num_connectors
322330
for conn in range(1, self.num_connectors + 1):
323-
_ = self._metrics[(conn, cstat.status_connector.value)]
324-
_ = self._metrics[(conn, cstat.error_code_connector.value)]
325-
_ = self._metrics[(conn, csess.session_energy.value)]
326-
_ = self._metrics[(conn, csess.meter_start.value)]
327-
_ = self._metrics[(conn, csess.transaction_id.value)]
328-
self._metrics[(0, cdet.connectors.value)].value = num_connectors
331+
self._init_connector_slots(conn)
332+
self._metrics[(0, cdet.connectors.value)].value = self.num_connectors
329333
await self.get_heartbeat_interval()
330334

331335
accepted_measurands: str = await self.get_supported_measurands()
332336
updated_entry = {**self.entry.data}
333337
for i in range(len(updated_entry[CONF_CPIDS])):
334338
if self.id in updated_entry[CONF_CPIDS][i]:
335-
updated_entry[CONF_CPIDS][i][self.id][CONF_MONITORED_VARIABLES] = (
336-
accepted_measurands
337-
)
339+
s = updated_entry[CONF_CPIDS][i][self.id]
340+
if s.get(CONF_MONITORED_VARIABLES) != accepted_measurands or s.get(
341+
CONF_NUM_CONNECTORS
342+
) != int(self.num_connectors):
343+
s[CONF_MONITORED_VARIABLES] = accepted_measurands
344+
s[CONF_NUM_CONNECTORS] = int(self.num_connectors)
338345
break
339346
# if an entry differs this will unload/reload and stop/restart the central system/websocket
340347
self.hass.config_entries.async_update_entry(self.entry, data=updated_entry)
341348

342349
await self.set_standard_configuration()
343350

344351
self.post_connect_success = True
345-
_LOGGER.debug(f"'{self.id}' post connection setup completed successfully")
352+
_LOGGER.debug("'%s' post connection setup completed successfully", self.id)
346353

347354
# nice to have, but not needed for integration to function
348355
# and can cause issues with some chargers
@@ -822,22 +829,6 @@ def supported_features(self) -> int:
822829
"""Flag of Ocpp features that are supported."""
823830
return self._attr_supported_features
824831

825-
def get_metric(self, measurand: str, connector_id: int = 0):
826-
"""Return last known value for given measurand."""
827-
val = self._metrics[(connector_id, measurand)].value
828-
if val is not None:
829-
return val
830-
831-
if connector_id and connector_id > 0:
832-
if measurand == cstat.status_connector.value:
833-
agg = self._metrics[(0, cstat.status_connector.value)]
834-
return agg.extra_attr.get(connector_id, agg.value)
835-
if measurand == cstat.error_code_connector.value:
836-
agg = self._metrics[(0, cstat.error_code_connector.value)]
837-
return agg.extra_attr.get(connector_id, agg.value)
838-
839-
return None
840-
841832
def get_ha_metric(self, measurand: str, connector_id: int | None = None):
842833
"""Return last known value in HA for given measurand, or None if not available."""
843834
base = self.settings.cpid.lower()
@@ -855,30 +846,6 @@ def get_ha_metric(self, measurand: str, connector_id: int | None = None):
855846
return st.state
856847
return None
857848

858-
def get_extra_attr(self, measurand: str, connector_id: int = 0):
859-
"""Return extra attributes for given measurand (per connector)."""
860-
attrs = self._metrics[(connector_id, measurand)].extra_attr
861-
if attrs:
862-
return attrs
863-
864-
if connector_id and connector_id > 0:
865-
if measurand in (
866-
cstat.status_connector.value,
867-
cstat.error_code_connector.value,
868-
):
869-
agg = self._metrics[(0, measurand)]
870-
if connector_id in agg.extra_attr:
871-
return {connector_id: agg.extra_attr[connector_id]}
872-
return {}
873-
874-
def get_unit(self, measurand: str, connector_id: int = 0):
875-
"""Return unit of given measurand."""
876-
return self._metrics[(connector_id, measurand)].unit
877-
878-
def get_ha_unit(self, measurand: str, connector_id: int = 0):
879-
"""Return HA unit of given measurand."""
880-
return self._metrics[(connector_id, measurand)].ha_unit
881-
882849
async def notify_ha(self, msg: str, title: str = "Ocpp integration"):
883850
"""Notify user via HA web frontend."""
884851
await self.hass.services.async_call(

custom_components/ocpp/config_flow.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
CONF_METER_INTERVAL,
2121
CONF_MONITORED_VARIABLES,
2222
CONF_MONITORED_VARIABLES_AUTOCONFIG,
23-
CONF_NUM_CONNECTORS,
2423
CONF_PORT,
2524
CONF_SKIP_SCHEMA_VALIDATION,
2625
CONF_SSL,
@@ -40,7 +39,6 @@
4039
DEFAULT_METER_INTERVAL,
4140
DEFAULT_MONITORED_VARIABLES,
4241
DEFAULT_MONITORED_VARIABLES_AUTOCONFIG,
43-
DEFAULT_NUM_CONNECTORS,
4442
DEFAULT_PORT,
4543
DEFAULT_SKIP_SCHEMA_VALIDATION,
4644
DEFAULT_SSL,
@@ -93,9 +91,6 @@
9391
vol.Required(
9492
CONF_FORCE_SMART_CHARGING, default=DEFAULT_FORCE_SMART_CHARGING
9593
): bool,
96-
vol.Required(CONF_NUM_CONNECTORS, default=DEFAULT_NUM_CONNECTORS): vol.All(
97-
vol.Coerce(int), vol.Range(min=1)
98-
),
9994
}
10095
)
10196

@@ -111,7 +106,7 @@ class ConfigFlow(ConfigFlow, domain=DOMAIN):
111106
"""Handle a config flow for OCPP."""
112107

113108
VERSION = 2
114-
MINOR_VERSION = 1
109+
MINOR_VERSION = 0
115110
CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH
116111

117112
def __init__(self):

custom_components/ocpp/number.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414
from homeassistant.const import UnitOfElectricCurrent
1515
from homeassistant.core import HomeAssistant, callback
16+
from homeassistant.helpers import entity_registry as er
1617
from homeassistant.helpers.dispatcher import async_dispatcher_connect
1718
from homeassistant.helpers.entity import DeviceInfo
1819

@@ -24,6 +25,7 @@
2425
CONF_NUM_CONNECTORS,
2526
DATA_UPDATED,
2627
DEFAULT_MAX_CURRENT,
28+
DEFAULT_NUM_CONNECTORS,
2729
DOMAIN,
2830
ICON,
2931
)
@@ -57,10 +59,31 @@ async def async_setup_entry(hass, entry, async_add_devices):
5759
"""Configure the number platform."""
5860
central_system = hass.data[DOMAIN][entry.entry_id]
5961
entities: list[ChargePointNumber] = []
62+
ent_reg = er.async_get(hass)
63+
6064
for charger in entry.data[CONF_CPIDS]:
6165
cp_id_settings = list(charger.values())[0]
6266
cpid = cp_id_settings[CONF_CPID]
63-
num_connectors = int(cp_id_settings.get(CONF_NUM_CONNECTORS, 1) or 1)
67+
68+
num_connectors = 1
69+
for item in entry.data.get(CONF_CPIDS, []):
70+
for _, cfg in item.items():
71+
if cfg.get(CONF_CPID) == cpid:
72+
num_connectors = int(
73+
cfg.get(CONF_NUM_CONNECTORS, DEFAULT_NUM_CONNECTORS)
74+
)
75+
break
76+
else:
77+
continue
78+
break
79+
80+
if num_connectors > 1:
81+
for desc in NUMBERS:
82+
uid_flat = ".".join([NUMBER_DOMAIN, DOMAIN, cpid, desc.key])
83+
stale_eid = ent_reg.async_get_entity_id(NUMBER_DOMAIN, DOMAIN, uid_flat)
84+
if stale_eid:
85+
ent_reg.async_remove(stale_eid)
86+
6487
for desc in NUMBERS:
6588
if desc.key == "maximum_current":
6689
max_cur = float(
@@ -120,7 +143,7 @@ async def async_setup_entry(hass, entry, async_add_devices):
120143
class ChargePointNumber(RestoreNumber, NumberEntity):
121144
"""Individual slider for setting charge rate."""
122145

123-
_attr_has_entity_name = True
146+
_attr_has_entity_name = False
124147
entity_description: OcppNumberDescription
125148

126149
def __init__(
@@ -150,14 +173,19 @@ def __init__(
150173
if self.connector_id:
151174
self._attr_device_info = DeviceInfo(
152175
identifiers={(DOMAIN, f"{cpid}-conn{self.connector_id}")},
153-
name=f"Connector {self.connector_id}",
176+
name=f"{cpid} Connector {self.connector_id}",
154177
via_device=(DOMAIN, cpid),
155178
)
156179
else:
157180
self._attr_device_info = DeviceInfo(
158181
identifiers={(DOMAIN, cpid)},
159182
name=cpid,
160183
)
184+
if self.connector_id is not None:
185+
object_id = f"{self.cpid}_connector_{self.connector_id}_{self.entity_description.key}"
186+
else:
187+
object_id = f"{self.cpid}_{self.entity_description.key}"
188+
self.entity_id = f"{NUMBER_DOMAIN}.{object_id}"
161189
self._attr_native_value = self.entity_description.initial_value
162190
self._attr_should_poll = False
163191

0 commit comments

Comments
 (0)