diff --git a/src/pysparkplug/_edge_node.py b/src/pysparkplug/_edge_node.py index 4767cd8..86579fd 100644 --- a/src/pysparkplug/_edge_node.py +++ b/src/pysparkplug/_edge_node.py @@ -55,6 +55,7 @@ class EdgeNode: group_id: str edge_node_id: str _metrics: dict[str, Metric] + _names_mapping: dict[int, str] _devices: dict[str, Device] _client: Client @@ -91,6 +92,7 @@ def __init__( def _setup_metrics(self, metrics: Iterable[Metric]) -> None: self._metrics = {} + self._names_mapping = {} for metric in metrics: if metric.name is None: raise ValueError( @@ -100,6 +102,8 @@ def _setup_metrics(self, metrics: Iterable[Metric]) -> None: raise ValueError( f"Metric {metric} must have a defined datatype when provided to an Edge Node" ) + if metric.alias is not None: + self._names_mapping[metric.alias] = metric.name self._metrics[metric.name] = metric def _setup_will(self) -> None: @@ -372,22 +376,37 @@ def update(self, metrics: Iterable[Metric]) -> None: an iterable of metrics to be updated """ for metric in metrics: - if metric.name is None: + metric_name = metric.name or self._names_mapping.get(metric.alias) + if metric_name is None: raise ValueError( - f"Metric {metric} must have a defined name when provided to an Edge Node" + f"Metric {metric} must have either a defined name or a valid alias when provided to an Edge Node" ) try: - curr_metric = self._metrics[metric.name] + curr_metric = self._metrics[metric_name] except KeyError as exc: raise ValueError( - f"Unrecognized metric {metric.name} cannot be updated" + f"Unrecognized metric {metric_name} cannot be updated" ) from exc if curr_metric.datatype != metric.datatype: raise ValueError( f"Metric datatype provided {metric.datatype} " f"doesn't match {curr_metric.datatype}" ) - self._metrics[metric.name] = metric + + if metric.name is not None: + self._metrics[metric_name] = metric + else: + self._metrics[metric_name] = Metric( + timestamp=metric.timestamp, + name=metric_name, + datatype=metric.datatype, + metadata=metric.metadata, + value=metric.value, + alias=metric.alias, + is_historical=metric.is_historical, + is_transient=metric.is_transient, + is_null=metric.is_null, + ) topic = Topic( message_type=MessageType.NDATA, @@ -458,6 +477,7 @@ class Device: device_id: str _metrics: dict[str, Metric] + _names_mapping: dict[int, str] cmd_callback: Callable[[EdgeNode, Message], None] def __init__( @@ -472,6 +492,7 @@ def __init__( def _setup_metrics(self, metrics: Iterable[Metric]) -> None: self._metrics = {} + self._names_mapping = {} for metric in metrics: if metric.name is None: raise ValueError( @@ -481,6 +502,8 @@ def _setup_metrics(self, metrics: Iterable[Metric]) -> None: raise ValueError( f"Metric {metric} must have a defined datatype when provided to an Edge Node" ) + if metric.alias is not None: + self._names_mapping[metric.alias] = metric.name self._metrics[metric.name] = metric @property @@ -496,19 +519,34 @@ def update(self, metrics: Iterable[Metric]) -> None: an iterable of metrics to be updated """ for metric in metrics: - if metric.name is None: + metric_name = metric.name or self._names_mapping.get(metric.alias) + if metric_name is None: raise ValueError( - f"Metric {metric} must have a defined name when provided to a Device" + f"Metric {metric} must have either a defined name or a valid alias when provided to a Device" ) try: - curr_metric = self._metrics[metric.name] + curr_metric = self._metrics[metric_name] except KeyError as exc: raise ValueError( - f"Unrecognized metric {metric.name} cannot be updated" + f"Unrecognized metric {metric_name} cannot be updated" ) from exc if curr_metric.datatype != metric.datatype: raise ValueError( f"Metric datatype provided {metric.datatype} " f"doesn't match {curr_metric.datatype}" ) - self._metrics[metric.name] = metric + + if metric.name is not None: + self._metrics[metric_name] = metric + else: + self._metrics[metric_name] = Metric( + timestamp=metric.timestamp, + name=metric_name, + datatype=metric.datatype, + metadata=metric.metadata, + value=metric.value, + alias=metric.alias, + is_historical=metric.is_historical, + is_transient=metric.is_transient, + is_null=metric.is_null, + ) diff --git a/test/unit_tests/test_metric.py b/test/unit_tests/test_metric.py index ffa9e7d..3e0e423 100644 --- a/test/unit_tests/test_metric.py +++ b/test/unit_tests/test_metric.py @@ -32,6 +32,32 @@ def test_basic_metric(self): self.assertEqual(metric2.value, 42) self.assertFalse(metric2.is_null) + def test_metric_with_name_none_and_alias(self): + """Test metric handling when name is None and alias is provided""" + metric = Metric( + timestamp=1234567890, + name=None, + alias=123, + datatype=DataType.INT32, + value=42, + ) + + # Convert to protobuf + pb = metric.to_pb(include_dtype=True) + self.assertEqual(pb.timestamp, 1234567890) + self.assertEqual(pb.alias, 123) + self.assertFalse(pb.HasField("name")) + self.assertEqual(pb.int_value, 42) + self.assertFalse(pb.is_null) + + # Convert back + metric2 = Metric.from_pb(pb) + self.assertEqual(metric2.timestamp, 1234567890) + self.assertIsNone(metric2.name) + self.assertEqual(metric2.alias, 123) + self.assertEqual(metric2.value, 42) + self.assertFalse(metric2.is_null) + def test_array_null_handling(self): """Test array handling with is_null flag""" test_cases = [