Skip to content

Commit b6d98f6

Browse files
author
lbbrhzn
authored
introduce the Metric class (#113)
* introduce the Metric class * fix errors * imake extra attr a metric property * fix unreachable code * improve meter_values processing Co-authored-by: lbbrhzn <@lbbrhzn>
1 parent f8adc54 commit b6d98f6

File tree

1 file changed

+129
-71
lines changed

1 file changed

+129
-71
lines changed

custom_components/ocpp/api.py

Lines changed: 129 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Representation of a OCCP Entities."""
22
import asyncio
3+
from collections import defaultdict
34
from datetime import datetime, timedelta, timezone
45
import logging
56
from math import sqrt
@@ -167,27 +168,28 @@ async def on_connect(self, websocket, path: str):
167168
cp = self.charge_points[self.cpid]
168169
await self.charge_points[self.cpid].reconnect(websocket)
169170
except Exception as e:
170-
_LOGGER.info(f"Exception occurred:\n{e}")
171+
_LOGGER.error(f"Exception occurred:\n{e}", exc_info=True)
172+
171173
finally:
172174
self.charge_points[self.cpid].status = STATE_UNAVAILABLE
173175
_LOGGER.info(f"Charger {cp_id} disconnected from {self.host}:{self.port}.")
174176

175177
def get_metric(self, cp_id: str, measurand: str):
176178
"""Return last known value for given measurand."""
177179
if cp_id in self.charge_points:
178-
return self.charge_points[cp_id].get_metric(measurand)
180+
return self.charge_points[cp_id]._metrics[measurand].value
179181
return None
180182

181183
def get_unit(self, cp_id: str, measurand: str):
182184
"""Return unit of given measurand."""
183185
if cp_id in self.charge_points:
184-
return self.charge_points[cp_id].get_unit(measurand)
186+
return self.charge_points[cp_id]._metrics[measurand].unit
185187
return None
186188

187189
def get_extra_attr(self, cp_id: str, measurand: str):
188190
"""Return last known extra attributes for given measurand."""
189191
if cp_id in self.charge_points:
190-
return self.charge_points[cp_id].get_extra_attr(measurand)
192+
return self.charge_points[cp_id]._metrics[measurand].extra_attr
191193
return None
192194

193195
def get_available(self, cp_id: str):
@@ -257,16 +259,14 @@ def __init__(
257259
# Indicates if the charger requires a reboot to apply new
258260
# configuration.
259261
self._requires_reboot = False
260-
self._metrics = {}
261-
self._units = {}
262-
self._extra_attr = {}
263262
self._features_supported = {}
264263
self.preparing = asyncio.Event()
265264
self._transactionId = 0
266-
self._metrics[cdet.identifier.value] = id
267-
self._units[csess.session_time.value] = TIME_MINUTES
268-
self._units[csess.session_energy.value] = UnitOfMeasure.kwh.value
269-
self._units[csess.meter_start.value] = UnitOfMeasure.kwh.value
265+
self._metrics = defaultdict(lambda: Metric(None, None))
266+
self._metrics[cdet.identifier.value].value = id
267+
self._metrics[csess.session_time.value].unit = TIME_MINUTES
268+
self._metrics[csess.session_energy.value].unit = UnitOfMeasure.kwh.value
269+
self._metrics[csess.meter_start.value].unit = UnitOfMeasure.kwh.value
270270

271271
async def post_connect(self):
272272
"""Logic to be executed right after a charger connects."""
@@ -349,7 +349,7 @@ async def handle_get_diagnostics(call):
349349
# "StopTxnSampledData", ",".join(self.entry.data[CONF_MONITORED_VARIABLES])
350350
# )
351351
resp = await self.get_configuration(ckey.number_of_connectors.value)
352-
self._metrics[cdet.connectors.value] = resp
352+
self._metrics[cdet.connectors.value].value = resp
353353
# await self.start_transaction()
354354

355355
# Register custom services with home assistant
@@ -397,7 +397,7 @@ async def get_supported_features(self):
397397
resp = await self.call(req)
398398
for key_value in resp.configuration_key:
399399
self._features_supported = key_value[om.value.value]
400-
self._metrics[cdet.features.value] = self._features_supported
400+
self._metrics[cdet.features.value].value = self._features_supported
401401
_LOGGER.debug("Supported feature profiles: %s", self._features_supported)
402402

403403
async def trigger_boot_notification(self):
@@ -532,7 +532,7 @@ async def start_transaction(self, limit_amps: int = 32, limit_watts: int = 22000
532532
stack_level = int(resp)
533533
req = call.RemoteStartTransactionPayload(
534534
connector_id=1,
535-
id_tag=self._metrics[cdet.identifier.value],
535+
id_tag=self._metrics[cdet.identifier.value].value,
536536
charging_profile={
537537
om.charging_profile_id.value: 1,
538538
om.stack_level.value: stack_level,
@@ -748,8 +748,9 @@ def process_phases(self, data):
748748
measurand_data[measurand] = {}
749749
measurand_data[measurand][om.unit.value] = unit
750750
measurand_data[measurand][phase] = float(value)
751-
# store the measurand data as extra attributes
752-
self._extra_attr.update(measurand_data)
751+
self._metrics[measurand].extra_attr[om.unit.value] = unit
752+
self._metrics[measurand].extra_attr[phase] = float(value)
753+
753754
for metric, phase_info in measurand_data.items():
754755
# _LOGGER.debug("Metric: %s, extra attributes: %s", metric, phase_info)
755756
metric_value = None
@@ -782,56 +783,68 @@ def process_phases(self, data):
782783
+ phase_info.get(Phase.l3.value, 0)
783784
)
784785
if metric_value is not None:
785-
self._metrics[metric] = round(metric_value, 1)
786+
self._metrics[metric].value = round(metric_value, 1)
786787

787788
@on(Action.MeterValues)
788789
def on_meter_values(self, connector_id: int, meter_value: Dict, **kwargs):
789790
"""Request handler for MeterValues Calls."""
790-
m = om.measurand.value
791791
for bucket in meter_value:
792792
unprocessed = bucket[om.sampled_value.name]
793793
processed_keys = []
794794
for idx, sv in enumerate(bucket[om.sampled_value.name]):
795-
if m in sv and om.phase.value not in sv:
796-
self._metrics[sv[m]] = round(float(sv[om.value.value]), 1)
797-
if om.unit.value in sv:
798-
if sv[om.unit.value] == DEFAULT_POWER_UNIT:
799-
self._metrics[sv[m]] = float(sv[om.value.value]) / 1000
800-
self._units[sv[m]] = HA_POWER_UNIT
801-
if sv[om.unit.value] == DEFAULT_ENERGY_UNIT:
802-
self._metrics[sv[m]] = float(sv[om.value.value]) / 1000
803-
self._units[sv[m]] = HA_ENERGY_UNIT
795+
measurand = sv.get(om.measurand.value, None)
796+
value = sv.get(om.value.value, None)
797+
unit = sv.get(om.unit.value, None)
798+
phase = sv.get(om.phase.value, None)
799+
location = sv.get(om.location.value, None)
800+
801+
if len(sv.keys()) == 1: # Backwars compatibility
802+
measurand = DEFAULT_MEASURAND
803+
unit = DEFAULT_ENERGY_UNIT
804+
805+
if phase is None:
806+
if unit == DEFAULT_POWER_UNIT:
807+
self._metrics[measurand].value = float(value) / 1000
808+
self._metrics[measurand].unit = HA_POWER_UNIT
809+
elif unit == DEFAULT_ENERGY_UNIT:
810+
self._metrics[measurand].value = float(value) / 1000
811+
self._metrics[measurand].unit = HA_ENERGY_UNIT
812+
else:
813+
self._metrics[measurand].value = round(float(value), 1)
814+
self._metrics[measurand].unit = unit
815+
if location is not None:
816+
self._metrics[measurand].extra_attr[
817+
om.location.value
818+
] = location
804819
processed_keys.append(idx)
805-
if len(sv.keys()) == 1: # for backwards compatibility
806-
self._metrics[DEFAULT_MEASURAND] = float(sv[om.value.value]) / 1000
807-
self._units[DEFAULT_MEASURAND] = HA_ENERGY_UNIT
808-
processed_keys.append(idx)
809-
if m in sv and om.location.value in sv:
810-
if self._extra_attr.get(sv[m]) is None:
811-
self._extra_attr[sv[m]] = {}
812-
self._extra_attr[sv[m]][om.location.value] = sv.get(
813-
om.location.value
814-
)
815820
for idx in sorted(processed_keys, reverse=True):
816821
unprocessed.pop(idx)
817822
# _LOGGER.debug("Meter data not yet processed: %s", unprocessed)
818823
if unprocessed is not None:
819824
self.process_phases(unprocessed)
820825
if csess.meter_start.value not in self._metrics:
821-
self._metrics[csess.meter_start.value] = self._metrics[DEFAULT_MEASURAND]
826+
self._metrics[csess.meter_start.value].value = self._metrics[
827+
DEFAULT_MEASURAND
828+
]
822829
if csess.transaction_id.value not in self._metrics:
823-
self._metrics[csess.transaction_id.value] = kwargs.get(
830+
self._metrics[csess.transaction_id.value].value = kwargs.get(
824831
om.transaction_id.name
825832
)
826833
self._transactionId = kwargs.get(om.transaction_id.name)
827-
self._metrics[csess.session_time.value] = round(
828-
(int(time.time()) - float(self._metrics[csess.transaction_id.value])) / 60
829-
)
830-
self._metrics[csess.session_energy.value] = round(
831-
float(self._metrics[DEFAULT_MEASURAND])
832-
- float(self._metrics[csess.meter_start.value]),
833-
1,
834-
)
834+
if self._metrics[csess.transaction_id.value].value is not None:
835+
self._metrics[csess.session_time.value].value = round(
836+
(
837+
int(time.time())
838+
- float(self._metrics[csess.transaction_id.value].value)
839+
)
840+
/ 60
841+
)
842+
if self._metrics[csess.meter_start.value].value is not None:
843+
self._metrics[csess.session_energy.value].value = round(
844+
float(self._metrics[DEFAULT_MEASURAND].value or 0)
845+
- float(self._metrics[csess.meter_start.value].value),
846+
1,
847+
)
835848
self.hass.async_create_task(self.central.update(self.central.cpid))
836849
return call_result.MeterValuesPayload()
837850

@@ -842,12 +855,16 @@ def on_boot_notification(self, **kwargs):
842855
_LOGGER.debug("Received boot notification for %s: %s", self.id, kwargs)
843856

844857
# update metrics
845-
self._metrics[cdet.model.value] = kwargs.get(om.charge_point_model.name, None)
846-
self._metrics[cdet.vendor.value] = kwargs.get(om.charge_point_vendor.name, None)
847-
self._metrics[cdet.firmware_version.value] = kwargs.get(
858+
self._metrics[cdet.model.value].value = kwargs.get(
859+
om.charge_point_model.name, None
860+
)
861+
self._metrics[cdet.vendor.value].value = kwargs.get(
862+
om.charge_point_vendor.name, None
863+
)
864+
self._metrics[cdet.firmware_version.value].value = kwargs.get(
848865
om.firmware_version.name, None
849866
)
850-
self._metrics[cdet.serial.value] = kwargs.get(
867+
self._metrics[cdet.serial.value].value = kwargs.get(
851868
om.charge_point_serial_number.name, None
852869
)
853870

@@ -862,25 +879,25 @@ def on_boot_notification(self, **kwargs):
862879
@on(Action.StatusNotification)
863880
def on_status_notification(self, connector_id, error_code, status, **kwargs):
864881
"""Handle a status notification."""
865-
self._metrics[cstat.status.value] = status
882+
self._metrics[cstat.status.value].value = status
866883
if (
867884
status == ChargePointStatus.suspended_ev.value
868885
or status == ChargePointStatus.suspended_evse.value
869886
):
870887
if Measurand.current_import.value in self._metrics:
871-
self._metrics[Measurand.current_import.value] = 0
888+
self._metrics[Measurand.current_import.value].value = 0
872889
if Measurand.power_active_import.value in self._metrics:
873-
self._metrics[Measurand.power_active_import.value] = 0
890+
self._metrics[Measurand.power_active_import.value].value = 0
874891
if Measurand.power_reactive_import.value in self._metrics:
875-
self._metrics[Measurand.power_reactive_import.value] = 0
876-
self._metrics[cstat.error_code.value] = error_code
892+
self._metrics[Measurand.power_reactive_import.value].value = 0
893+
self._metrics[cstat.error_code.value].value = error_code
877894
self.hass.async_create_task(self.central.update(self.central.cpid))
878895
return call_result.StatusNotificationPayload()
879896

880897
@on(Action.FirmwareStatusNotification)
881898
def on_firmware_status(self, status, **kwargs):
882899
"""Handle firmware status notification."""
883-
self._metrics[cstat.firmware_status.value] = status
900+
self._metrics[cstat.firmware_status.value].value = status
884901
self.hass.async_create_task(self.central.update(self.central.cpid))
885902
return call_result.FirmwareStatusNotificationPayload()
886903

@@ -901,9 +918,9 @@ def on_authorize(self, id_tag, **kwargs):
901918
def on_start_transaction(self, connector_id, id_tag, meter_start, **kwargs):
902919
"""Handle a Start Transaction request."""
903920
self._transactionId = int(time.time())
904-
self._metrics[cstat.stop_reason.value] = ""
905-
self._metrics[csess.transaction_id.value] = self._transactionId
906-
self._metrics[csess.meter_start.value] = int(meter_start) / 1000
921+
self._metrics[cstat.stop_reason.value].value = ""
922+
self._metrics[csess.transaction_id.value].value = self._transactionId
923+
self._metrics[csess.meter_start.value].value = int(meter_start) / 1000
907924
self.hass.async_create_task(self.central.update(self.central.cpid))
908925
return call_result.StartTransactionPayload(
909926
id_tag_info={om.status.value: AuthorizationStatus.accepted.value},
@@ -913,19 +930,20 @@ def on_start_transaction(self, connector_id, id_tag, meter_start, **kwargs):
913930
@on(Action.StopTransaction)
914931
def on_stop_transaction(self, meter_stop, timestamp, transaction_id, **kwargs):
915932
"""Stop the current transaction."""
916-
self._metrics[cstat.stop_reason.value] = kwargs.get(om.reason.name, None)
933+
self._metrics[cstat.stop_reason.value].value = kwargs.get(om.reason.name, None)
917934

918-
if csess.meter_start.value in self._metrics:
919-
self._metrics[csess.session_energy.value] = round(
920-
int(meter_stop) / 1000 - float(self._metrics[csess.meter_start.value]),
935+
if self._metrics[csess.meter_start.value].value is not None:
936+
self._metrics[csess.session_energy.value].value = round(
937+
int(meter_stop) / 1000
938+
- float(self._metrics[csess.meter_start.value].value),
921939
1,
922940
)
923941
if Measurand.current_import.value in self._metrics:
924-
self._metrics[Measurand.current_import.value] = 0
942+
self._metrics[Measurand.current_import.value].value = 0
925943
if Measurand.power_active_import.value in self._metrics:
926-
self._metrics[Measurand.power_active_import.value] = 0
944+
self._metrics[Measurand.power_active_import.value].value = 0
927945
if Measurand.power_reactive_import.value in self._metrics:
928-
self._metrics[Measurand.power_reactive_import.value] = 0
946+
self._metrics[Measurand.power_reactive_import.value].value = 0
929947
self.hass.async_create_task(self.central.update(self.central.cpid))
930948
return call_result.StopTransactionPayload(
931949
id_tag_info={om.status.value: AuthorizationStatus.accepted.value}
@@ -941,18 +959,58 @@ def on_data_transfer(self, vendor_id, **kwargs):
941959
def on_heartbeat(self, **kwargs):
942960
"""Handle a Heartbeat."""
943961
now = datetime.now(tz=timezone.utc).isoformat()
944-
self._metrics[cstat.heartbeat.value] = now
962+
self._metrics[cstat.heartbeat.value].value = now
945963
self.hass.async_create_task(self.central.update(self.central.cpid))
946964
return call_result.HeartbeatPayload(current_time=now)
947965

948966
def get_metric(self, measurand: str):
949967
"""Return last known value for given measurand."""
950-
return self._metrics.get(measurand, None)
968+
return self._metrics[measurand].value
951969

952970
def get_extra_attr(self, measurand: str):
953971
"""Return last known extra attributes for given measurand."""
954-
return self._extra_attr.get(measurand, None)
972+
return self._metrics[measurand].extra_attr
955973

956974
def get_unit(self, measurand: str):
957975
"""Return unit of given measurand."""
958-
return self._units.get(measurand, None)
976+
return self._metrics[measurand].unit
977+
978+
979+
class Metric:
980+
"""Metric class."""
981+
982+
def __init__(self, value, unit):
983+
"""Initialize a Metric."""
984+
self._value = value
985+
self._unit = unit
986+
self._extra_attr = {}
987+
988+
@property
989+
def value(self):
990+
"""Get the value of the metric."""
991+
return self._value
992+
993+
@value.setter
994+
def value(self, value):
995+
"""Set the value of the metric."""
996+
self._value = value
997+
998+
@property
999+
def unit(self):
1000+
"""Get the unit of the metric."""
1001+
return self._unit
1002+
1003+
@unit.setter
1004+
def unit(self, unit: str):
1005+
"""Set the unit of the metric."""
1006+
self._unit = unit
1007+
1008+
@property
1009+
def extra_attr(self):
1010+
"""Get the extra attributes of the metric."""
1011+
return self._extra_attr
1012+
1013+
@extra_attr.setter
1014+
def extra_attr(self, extra_attr: dict):
1015+
"""Set the unit of the metric."""
1016+
self._extra_attr = extra_attr

0 commit comments

Comments
 (0)