Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/frequenz/client/common/metrics/_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class MetricConnection:
`MetricConnection.get_category()` to obtain a known member or a clear error.
"""

name: str | None = None
name: str = ""
"""The name of the specific connection from which the metric was obtained.

This is expected to be populated when the same [`Metric`][...Metric] variant
Expand All @@ -133,7 +133,7 @@ def __str__(self) -> str:
if isinstance(self.category, int)
else f"<CATEGORY={self.category.name}>"
)
if self.name is not None:
if self.name:
return f"{category_name}({self.name})"
return category_name

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def metric_connection_from_proto_with_issues(

return MetricConnection(
category=category,
name=message.name or None,
name=message.name,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ._breaker import Breaker
from ._capacitor_bank import CapacitorBank
from ._category import ElectricalComponentCategory
from ._category_specific_info import CategorySpecificInfo
from ._chp import Chp
from ._converter import Converter
from ._crypto_miner import CryptoMiner
Expand Down Expand Up @@ -81,6 +82,7 @@
"BatteryTypes",
"Breaker",
"CapacitorBank",
"CategorySpecificInfo",
"Chp",
"Converter",
"CryptoMiner",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class UnrecognizedBattery(Battery, ProblematicElectricalComponent):
type: int
"""The raw type of this battery, not recognized by this library version."""

def __str__(self) -> str:
"""Return a string representation exposing the raw type."""
return f"{self.id}:{self.name}:Battery:type={self.type}"


BatteryTypes: TypeAlias = (
LiIonBattery | NaIonBattery | UnrecognizedBattery | UnspecifiedBattery
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# License: MIT
# Copyright © 2026 Frequenz Energy-as-a-Service GmbH

"""Category specific info carried by an electrical component."""

import dataclasses
from collections.abc import Mapping
from typing import Any


@dataclasses.dataclass(frozen=True, kw_only=True)
class CategorySpecificInfo:
"""The category specific info carried by an electrical component.

A protobuf electrical component may carry a `category_specific_info` variant
with extra fields tied to its category. Fields this library version
understands are translated into typed attributes on the concrete component
(e.g. the battery type). Anything left over — either because the component's
category is not recognized, or because a newer API version added fields this
client doesn't know yet — is preserved here so callers can still inspect the
raw values.
"""

kind: str
"""The name of the info variant carried on the wire (e.g. `"battery"`)."""

fields: Mapping[str, Any] = dataclasses.field(
default_factory=dict,
# Excluded from the hash: values may be unhashable (e.g. lists), and even
# repr()-folding them breaks the eq/hash invariant since values that
# compare equal can differ under repr()/hash() (e.g. 1 == 1.0 == True).
# Instances hash on `kind` alone, mirroring `metric_config_bounds`.
hash=False,
)
"""The leftover fields not translated into typed attributes.

The keys are the protobuf field names and the values their decoded content.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ...metrics import Bounds, InvalidBounds, InvalidBoundsError, Metric
from .. import MicrogridId
from .._lifetime import InvalidLifetime, InvalidLifetimeError, Lifetime
from ._category_specific_info import CategorySpecificInfo
from ._ids import ElectricalComponentId

DefaultT = TypeVar("DefaultT")
Expand Down Expand Up @@ -81,7 +82,7 @@ class ElectricalComponent: # pylint: disable=too-many-instance-attributes
# dict is not hashable, so we don't use this field to calculate the hash.
# This shouldn't be a problem since it is very unlikely that two components
# with all other attributes being equal would have different category
# specific metadata, so hash collisions should be still very unlikely.
# specific info, so hash collisions should be still very unlikely.
hash=False,
)
)
Expand All @@ -103,21 +104,17 @@ class ElectricalComponent: # pylint: disable=too-many-instance-attributes
when a valid [`Bounds`][.....metrics.Bounds] is required.
"""

category_specific_metadata: Mapping[str, Any] = dataclasses.field(
default_factory=dict,
# dict is not hashable, so we don't use this field to calculate the hash. This
# shouldn't be a problem since it is very unlikely that two components with all
# other attributes being equal would have different category specific metadata,
# so hash collisions should be still very unlikely.
hash=False,
)
"""The category specific metadata of this electrical component.

Note:
This should not be used normally, it is only useful when accessing a newer
version of the API where the client doesn't know about the new metadata fields
yet (i.e. for use with
[`UnrecognizedElectricalComponent`][...UnrecognizedElectricalComponent]).
category_specific_info: CategorySpecificInfo | None = None
"""The category specific info carried by this component, if any.

This is `None` when the wire carried no category-specific info variant.
Otherwise it holds a
[`CategorySpecificInfo`][...CategorySpecificInfo] recording the variant
`kind` together with any fields that were not translated into typed
attributes on this component. The leftover fields are empty when everything
was translated, and non-empty when the category or its variant is not
recognized, or when a newer API version added fields this client version
doesn't know yet.
"""

def __new__(cls, *_: Any, **__: Any) -> Self:
Expand Down Expand Up @@ -327,5 +324,4 @@ def identity(self) -> tuple[ElectricalComponentId, MicrogridId]:

def __str__(self) -> str:
"""Return a human-readable string representation of this instance."""
name = f":{self.name}" if self.name else ""
return f"{self.id}<{type(self).__name__}>{name}"
return f"{self.id}:{self.name}:{type(self).__name__}"
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class UnrecognizedEvCharger(EvCharger, ProblematicElectricalComponent):
type: int
"""The raw type of this EV charger, not recognized by this library version."""

def __str__(self) -> str:
"""Return a string representation exposing the raw type."""
return f"{self.id}:{self.name}:EvCharger:type={self.type}"


EvChargerTypes: TypeAlias = (
UnspecifiedEvCharger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class UnrecognizedInverter(Inverter, ProblematicElectricalComponent):
type: int
"""The raw type of this inverter, not recognized by this library version."""

def __str__(self) -> str:
"""Return a string representation exposing the raw type."""
return f"{self.id}:{self.name}:Inverter:type={self.type}"


InverterTypes: TypeAlias = (
UnspecifiedInverter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,36 @@ class UnrecognizedElectricalComponent(ProblematicElectricalComponent):
category: int
"""The raw category of this component, not recognized by this library version."""

def __str__(self) -> str:
"""Return a string representation exposing the raw category."""
return f"{self.id}:{self.name}:category={self.category}"


@dataclasses.dataclass(frozen=True, kw_only=True)
class MismatchedCategoryElectricalComponent(ProblematicElectricalComponent):
"""An electrical component with a mismatch in the category.

This electrical component declared a category but carries category specific
metadata that doesn't match the declared category.
info that doesn't match the declared category.
"""

category: int
"""The raw category declared by this component.

It doesn't match the carried category specific metadata.
It doesn't match the carried category specific info.
"""

category_name: str | None = None
"""The short protobuf name of the declared category, or `None` if unknown.

This is the protobuf enum name without its long prefix (e.g. `"BATTERY"`).
It is normally set, since a mismatched component declares a recognized
category.
"""

def __str__(self) -> str:
"""Return a string representation exposing the category mismatch."""
info = self.category_specific_info
kind = info.kind if info is not None else ""
category = self.category_name or self.category
return f"{self.id}:{self.name}:mismatched:category={category}:kind={kind}"
Loading
Loading