Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions src/pysparkplug/_edge_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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__(
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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,
)
26 changes: 26 additions & 0 deletions test/unit_tests/test_metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down