Skip to content

Index

frequenz.client.common.metrics ¤

Metrics definitions.

Classes¤

frequenz.client.common.metrics.AggregatedMetricValue dataclass ¤

Encapsulates derived statistical summaries of a single metric.

The message allows for the reporting of statistical summaries — minimum, maximum, and average values - as well as the complete list of individual samples if available.

This message represents derived metrics and contains fields for statistical summaries—minimum, maximum, and average values. Individual measurements are optional, accommodating scenarios where only subsets of this information are available.

Source code in src/frequenz/client/common/metrics/_sample.py
@dataclass(frozen=True, kw_only=True)
class AggregatedMetricValue:
    """Encapsulates derived statistical summaries of a single metric.

    The message allows for the reporting of statistical summaries — minimum,
    maximum, and average values - as well as the complete list of individual
    samples if available.

    This message represents derived metrics and contains fields for statistical
    summaries—minimum, maximum, and average values. Individual measurements are
    optional, accommodating scenarios where only subsets of this information
    are available.
    """

    avg: FloatInt
    """The derived average value of the metric."""

    min: FloatInt | None
    """The minimum measured value of the metric."""

    max: FloatInt | None
    """The maximum measured value of the metric."""

    raw: Sequence[FloatInt]
    """All the raw individual values (it might be empty if not provided by the component)."""

    def __str__(self) -> str:
        """Return the short string representation of this instance."""
        extra: list[str] = []
        if self.min is not None:
            extra.append(f"min:{self.min}")
        if self.max is not None:
            extra.append(f"max:{self.max}")
        if len(self.raw) > 0:
            extra.append(f"num_raw:{len(self.raw)}")
        extra_str = f"<{' '.join(extra)}>" if extra else ""
        return f"avg:{self.avg}{extra_str}"
Attributes¤
avg instance-attribute ¤
avg: FloatInt

The derived average value of the metric.

max instance-attribute ¤
max: FloatInt | None

The maximum measured value of the metric.

min instance-attribute ¤
min: FloatInt | None

The minimum measured value of the metric.

raw instance-attribute ¤

All the raw individual values (it might be empty if not provided by the component).

Methods:¤
__str__ ¤
__str__() -> str

Return the short string representation of this instance.

Source code in src/frequenz/client/common/metrics/_sample.py
def __str__(self) -> str:
    """Return the short string representation of this instance."""
    extra: list[str] = []
    if self.min is not None:
        extra.append(f"min:{self.min}")
    if self.max is not None:
        extra.append(f"max:{self.max}")
    if len(self.raw) > 0:
        extra.append(f"num_raw:{len(self.raw)}")
    extra_str = f"<{' '.join(extra)}>" if extra else ""
    return f"avg:{self.avg}{extra_str}"

frequenz.client.common.metrics.AggregationMethod ¤

Bases: Enum

The type of the aggregated value.

Source code in src/frequenz/client/common/metrics/_sample.py
@unique
class AggregationMethod(Enum):
    """The type of the aggregated value."""

    AVG = "avg"
    """The average value of the metric."""

    MIN = "min"
    """The minimum value of the metric."""

    MAX = "max"
    """The maximum value of the metric."""
Attributes¤
AVG class-attribute instance-attribute ¤
AVG = 'avg'

The average value of the metric.

MAX class-attribute instance-attribute ¤
MAX = 'max'

The maximum value of the metric.

MIN class-attribute instance-attribute ¤
MIN = 'min'

The minimum value of the metric.

frequenz.client.common.metrics.BaseBounds dataclass ¤

A base class for well-formed and malformed metric bounds.

This class cannot be instantiated directly. Use Bounds for a valid pair of bounds or InvalidBounds to preserve malformed wire data.

Source code in src/frequenz/client/common/metrics/_bounds.py
@dataclasses.dataclass(frozen=True, kw_only=True)
class BaseBounds:
    """A base class for well-formed and malformed metric bounds.

    This class cannot be instantiated directly. Use [`Bounds`][..Bounds] for a
    valid pair of bounds or [`InvalidBounds`][..InvalidBounds] to preserve
    malformed wire data.
    """

    lower: FloatInt | None = None
    """The lower bound.

    If `None`, there is no lower bound.
    """

    upper: FloatInt | None = None
    """The upper bound.

    If `None`, there is no upper bound.
    """

    # pylint: disable-next=unused-argument
    def __new__(cls, *args: Any, **kwargs: Any) -> Self:
        """Prevent instantiation of this class."""
        if cls is BaseBounds:
            raise TypeError(f"Cannot instantiate {cls.__name__} directly")
        return super().__new__(cls)
Attributes¤
lower class-attribute instance-attribute ¤
lower: FloatInt | None = None

The lower bound.

If None, there is no lower bound.

upper class-attribute instance-attribute ¤
upper: FloatInt | None = None

The upper bound.

If None, there is no upper bound.

Methods:¤
__new__ ¤
__new__(*args: Any, **kwargs: Any) -> Self

Prevent instantiation of this class.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
    """Prevent instantiation of this class."""
    if cls is BaseBounds:
        raise TypeError(f"Cannot instantiate {cls.__name__} directly")
    return super().__new__(cls)

frequenz.client.common.metrics.Bounds dataclass ¤

Bases: BaseBounds

A set of lower and upper bounds for any metric.

The lower bound must be less than or equal to the upper bound.

The units of the bounds are always the same as the related metric.

A -inf lower bound or a +inf upper bound denotes the unbounded direction and is canonicalized to None on construction, so Bounds(lower=-math.inf, upper=math.inf) is exactly Bounds().

Note

Raises a ValueError if lower is greater than upper, or if either bound is NaN (which is never a valid endpoint). A wrong-side infinity (+inf lower or -inf upper) is kept as a real endpoint, so a contradictory pair still raises. Use InvalidBounds to represent malformed bounds data received from the wire.

Source code in src/frequenz/client/common/metrics/_bounds.py
@dataclasses.dataclass(frozen=True, kw_only=True)
class Bounds(BaseBounds):
    """A set of lower and upper bounds for any metric.

    The lower bound must be less than or equal to the upper bound.

    The units of the bounds are always the same as the related metric.

    A `-inf` lower bound or a `+inf` upper bound denotes the unbounded
    direction and is canonicalized to `None` on construction, so
    `Bounds(lower=-math.inf, upper=math.inf)` is exactly `Bounds()`.

    Note:
        Raises a `ValueError` if [`lower`][.lower] is greater than
        [`upper`][.upper], or if either bound is `NaN` (which is never a
        valid endpoint). A wrong-side infinity (`+inf` lower or `-inf`
        upper) is kept as a real endpoint, so a contradictory pair still
        raises. Use [`InvalidBounds`][..InvalidBounds] to represent
        malformed bounds data received from the wire.
    """

    def __post_init__(self) -> None:
        """Validate and canonicalize these bounds."""
        # Only `float` can be `NaN`; guarding with `isinstance` also avoids
        # `math.isnan()` raising `OverflowError` on an `int` too large for a
        # `float` (a valid `FloatInt` endpoint).
        if isinstance(self.lower, float) and math.isnan(self.lower):
            raise ValueError("Lower bound cannot be NaN")
        if isinstance(self.upper, float) and math.isnan(self.upper):
            raise ValueError("Upper bound cannot be NaN")
        # A `-inf` lower or `+inf` upper is the unbounded direction, so
        # canonicalize it to `None` (the documented unbounded marker) for a
        # single representation across equality, hashing and membership. A
        # wrong-side infinity (`+inf` lower / `-inf` upper) is kept as a real
        # endpoint, so a contradictory pair still fails the ordering check
        # below. `==` (not `math.isinf`) keeps this overflow-safe on a large
        # `int` endpoint.
        if self.lower == -math.inf:
            object.__setattr__(self, "lower", None)
        if self.upper == math.inf:
            object.__setattr__(self, "upper", None)
        if self.lower is None:
            return
        if self.upper is None:
            return
        if self.lower > self.upper:
            raise ValueError(
                f"Lower bound ({self.lower}) must be less than or equal to upper "
                f"bound ({self.upper})"
            )

    def __str__(self) -> str:
        """Return a string representation of these bounds."""
        return f"[{self.lower},{self.upper}]"

    def __contains__(self, item: FloatInt | None) -> bool:
        """Check whether a value is within these bounds.

        The bounds are inclusive on both ends, and a `None` bound means these
        bounds are unbounded in that direction. `None` is a bound marker only
        and is never itself a value, so `None` is never contained.

        Args:
            item: The value to check.

        Returns:
            Whether `item` is within these bounds.
        """
        if item is None or (isinstance(item, float) and math.isnan(item)):
            return False
        if self.lower is not None and item < self.lower:
            return False
        if self.upper is not None and item > self.upper:
            return False
        return True

    def __bool__(self) -> bool:
        """Return whether these bounds restrict the range in any direction.

        Fully unbounded bounds (`Bounds()`, where both `lower` and `upper`
        are `None`) accept every value and are therefore falsy; any set bound
        makes them truthy.

        Returns:
            Whether at least one of `lower` or `upper` is set.
        """
        return self.lower is not None or self.upper is not None

    def is_bounded(self) -> bool:
        """Return whether these bounds restrict the range in any direction.

        This is the explicit spelling of these bounds' truthiness: fully
        unbounded bounds (`Bounds()`) are not bounded, while any set `lower`
        or `upper` makes them bounded.

        Returns:
            Whether at least one of `lower` or `upper` is set.
        """
        return bool(self)
Attributes¤
lower class-attribute instance-attribute ¤
lower: FloatInt | None = None

The lower bound.

If None, there is no lower bound.

upper class-attribute instance-attribute ¤
upper: FloatInt | None = None

The upper bound.

If None, there is no upper bound.

Methods:¤
__bool__ ¤
__bool__() -> bool

Return whether these bounds restrict the range in any direction.

Fully unbounded bounds (Bounds(), where both lower and upper are None) accept every value and are therefore falsy; any set bound makes them truthy.

RETURNS DESCRIPTION
bool

Whether at least one of lower or upper is set.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __bool__(self) -> bool:
    """Return whether these bounds restrict the range in any direction.

    Fully unbounded bounds (`Bounds()`, where both `lower` and `upper`
    are `None`) accept every value and are therefore falsy; any set bound
    makes them truthy.

    Returns:
        Whether at least one of `lower` or `upper` is set.
    """
    return self.lower is not None or self.upper is not None
__contains__ ¤
__contains__(item: FloatInt | None) -> bool

Check whether a value is within these bounds.

The bounds are inclusive on both ends, and a None bound means these bounds are unbounded in that direction. None is a bound marker only and is never itself a value, so None is never contained.

PARAMETER DESCRIPTION
item

The value to check.

TYPE: FloatInt | None

RETURNS DESCRIPTION
bool

Whether item is within these bounds.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __contains__(self, item: FloatInt | None) -> bool:
    """Check whether a value is within these bounds.

    The bounds are inclusive on both ends, and a `None` bound means these
    bounds are unbounded in that direction. `None` is a bound marker only
    and is never itself a value, so `None` is never contained.

    Args:
        item: The value to check.

    Returns:
        Whether `item` is within these bounds.
    """
    if item is None or (isinstance(item, float) and math.isnan(item)):
        return False
    if self.lower is not None and item < self.lower:
        return False
    if self.upper is not None and item > self.upper:
        return False
    return True
__new__ ¤
__new__(*args: Any, **kwargs: Any) -> Self

Prevent instantiation of this class.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
    """Prevent instantiation of this class."""
    if cls is BaseBounds:
        raise TypeError(f"Cannot instantiate {cls.__name__} directly")
    return super().__new__(cls)
__post_init__ ¤
__post_init__() -> None

Validate and canonicalize these bounds.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __post_init__(self) -> None:
    """Validate and canonicalize these bounds."""
    # Only `float` can be `NaN`; guarding with `isinstance` also avoids
    # `math.isnan()` raising `OverflowError` on an `int` too large for a
    # `float` (a valid `FloatInt` endpoint).
    if isinstance(self.lower, float) and math.isnan(self.lower):
        raise ValueError("Lower bound cannot be NaN")
    if isinstance(self.upper, float) and math.isnan(self.upper):
        raise ValueError("Upper bound cannot be NaN")
    # A `-inf` lower or `+inf` upper is the unbounded direction, so
    # canonicalize it to `None` (the documented unbounded marker) for a
    # single representation across equality, hashing and membership. A
    # wrong-side infinity (`+inf` lower / `-inf` upper) is kept as a real
    # endpoint, so a contradictory pair still fails the ordering check
    # below. `==` (not `math.isinf`) keeps this overflow-safe on a large
    # `int` endpoint.
    if self.lower == -math.inf:
        object.__setattr__(self, "lower", None)
    if self.upper == math.inf:
        object.__setattr__(self, "upper", None)
    if self.lower is None:
        return
    if self.upper is None:
        return
    if self.lower > self.upper:
        raise ValueError(
            f"Lower bound ({self.lower}) must be less than or equal to upper "
            f"bound ({self.upper})"
        )
__str__ ¤
__str__() -> str

Return a string representation of these bounds.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __str__(self) -> str:
    """Return a string representation of these bounds."""
    return f"[{self.lower},{self.upper}]"
is_bounded ¤
is_bounded() -> bool

Return whether these bounds restrict the range in any direction.

This is the explicit spelling of these bounds' truthiness: fully unbounded bounds (Bounds()) are not bounded, while any set lower or upper makes them bounded.

RETURNS DESCRIPTION
bool

Whether at least one of lower or upper is set.

Source code in src/frequenz/client/common/metrics/_bounds.py
def is_bounded(self) -> bool:
    """Return whether these bounds restrict the range in any direction.

    This is the explicit spelling of these bounds' truthiness: fully
    unbounded bounds (`Bounds()`) are not bounded, while any set `lower`
    or `upper` makes them bounded.

    Returns:
        Whether at least one of `lower` or `upper` is set.
    """
    return bool(self)

frequenz.client.common.metrics.BoundsSet dataclass ¤

A normalized set of metric bounds for efficient membership testing.

A BoundsSet represents the union of a collection of Bounds: a value is contained when it falls within any of them. This matches the way multiple metric-sample bounds work — the value must be within at least one of the bounds. On construction the bounds are sorted by their lower bound and any overlapping or touching bounds are merged, so the stored bounds are canonical: sorted and pairwise non-overlapping.

Note

This is a domain-specialized set, not a mathematical one: the empty set is the unbounded set. It contains every value and is falsy, so not bounds_set reliably means "unbounded" (bounds that together cover the whole space also normalize to the empty set). Because of this, membership must be tested with value in bounds_set, which is authoritative — do not reconstruct it by iterating bounds, since the two disagree for the unbounded set.

Example
from frequenz.client.common.metrics import Bounds, BoundsSet

allowed = BoundsSet(
    (
        Bounds(lower=1.0, upper=5.0),
        Bounds(lower=3.0, upper=10.0),
        Bounds(lower=15.0, upper=20.0),
    )
)
# Overlapping bounds are merged on construction.
assert allowed.bounds == (
    Bounds(lower=1.0, upper=10.0),
    Bounds(lower=15.0, upper=20.0),
)
assert 7.0 in allowed
assert 12.0 not in allowed
Source code in src/frequenz/client/common/metrics/_bounds.py
@dataclasses.dataclass(frozen=True, init=False)
class BoundsSet:
    """A normalized set of metric bounds for efficient membership testing.

    A `BoundsSet` represents the union of a collection of
    [`Bounds`][..Bounds]: a value is contained when it falls within *any* of
    them. This matches the way multiple metric-sample bounds work — the value
    must be within at least one of the bounds. On construction the bounds are
    sorted by their lower bound and any overlapping or touching bounds are
    merged, so the stored `bounds` are canonical: sorted and pairwise
    non-overlapping.

    Note:
        This is a domain-specialized set, not a mathematical one: **the empty
        set is the unbounded set**. It contains every value and is falsy, so
        `not bounds_set` reliably means "unbounded" (bounds that together cover
        the whole space also normalize to the empty set). Because of this,
        membership must be tested with `value in bounds_set`, which is
        authoritative — do not reconstruct it by iterating `bounds`, since the
        two disagree for the unbounded set.

    Example:
        ```python
        from frequenz.client.common.metrics import Bounds, BoundsSet

        allowed = BoundsSet(
            (
                Bounds(lower=1.0, upper=5.0),
                Bounds(lower=3.0, upper=10.0),
                Bounds(lower=15.0, upper=20.0),
            )
        )
        # Overlapping bounds are merged on construction.
        assert allowed.bounds == (
            Bounds(lower=1.0, upper=10.0),
            Bounds(lower=15.0, upper=20.0),
        )
        assert 7.0 in allowed
        assert 12.0 not in allowed
        ```
    """

    bounds: tuple[Bounds, ...] = ()
    """The normalized bounds: sorted by lower bound and pairwise non-overlapping."""

    def __init__(self, bounds: Iterable[Bounds] = ()) -> None:
        """Create a normalized bounds set from a collection of bounds.

        Args:
            bounds: The bounds to normalize. Any collection is accepted; the
                stored bounds are sorted by lower bound, with overlapping or
                touching bounds merged.
        """
        object.__setattr__(self, "bounds", _sort_and_merge_bounds(bounds))

    def __contains__(self, item: FloatInt | None) -> bool:
        """Check whether a value is within any bounds of this set.

        Args:
            item: The value to check.

        Returns:
            Whether `item` is within any bounds of this set. `None` is never
                contained, and the empty (unbounded) set contains every value.
        """
        if item is None or (isinstance(item, float) and math.isnan(item)):
            return False
        if not self.bounds:
            return True
        position = bisect.bisect_right(
            self.bounds,
            item,
            key=lambda bound: -math.inf if bound.lower is None else bound.lower,
        )
        index = position - 1
        return index >= 0 and item in self.bounds[index]

    def __bool__(self) -> bool:
        """Return whether this set restricts the accepted values.

        The empty set is the unbounded set: it accepts every value and is
        therefore falsy. A set with any bounds is truthy.

        Returns:
            Whether this set contains any bounds.
        """
        return bool(self.bounds)

    def is_bounded(self) -> bool:
        """Return whether this set restricts the accepted values.

        This is the explicit spelling of this set's truthiness: the empty
        (unbounded) set is not bounded, while a set with any bounds is.

        Returns:
            Whether this set contains any bounds.
        """
        return bool(self)

    def __str__(self) -> str:
        """Return a string representation of this set."""
        if not self.bounds:
            return "[None,None]"
        return "∪".join(str(bound) for bound in self.bounds)
Attributes¤
bounds class-attribute instance-attribute ¤
bounds: tuple[Bounds, ...] = ()

The normalized bounds: sorted by lower bound and pairwise non-overlapping.

Methods:¤
__bool__ ¤
__bool__() -> bool

Return whether this set restricts the accepted values.

The empty set is the unbounded set: it accepts every value and is therefore falsy. A set with any bounds is truthy.

RETURNS DESCRIPTION
bool

Whether this set contains any bounds.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __bool__(self) -> bool:
    """Return whether this set restricts the accepted values.

    The empty set is the unbounded set: it accepts every value and is
    therefore falsy. A set with any bounds is truthy.

    Returns:
        Whether this set contains any bounds.
    """
    return bool(self.bounds)
__contains__ ¤
__contains__(item: FloatInt | None) -> bool

Check whether a value is within any bounds of this set.

PARAMETER DESCRIPTION
item

The value to check.

TYPE: FloatInt | None

RETURNS DESCRIPTION
bool

Whether item is within any bounds of this set. None is never contained, and the empty (unbounded) set contains every value.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __contains__(self, item: FloatInt | None) -> bool:
    """Check whether a value is within any bounds of this set.

    Args:
        item: The value to check.

    Returns:
        Whether `item` is within any bounds of this set. `None` is never
            contained, and the empty (unbounded) set contains every value.
    """
    if item is None or (isinstance(item, float) and math.isnan(item)):
        return False
    if not self.bounds:
        return True
    position = bisect.bisect_right(
        self.bounds,
        item,
        key=lambda bound: -math.inf if bound.lower is None else bound.lower,
    )
    index = position - 1
    return index >= 0 and item in self.bounds[index]
__init__ ¤
__init__(bounds: Iterable[Bounds] = ()) -> None

Create a normalized bounds set from a collection of bounds.

PARAMETER DESCRIPTION
bounds

The bounds to normalize. Any collection is accepted; the stored bounds are sorted by lower bound, with overlapping or touching bounds merged.

TYPE: Iterable[Bounds] DEFAULT: ()

Source code in src/frequenz/client/common/metrics/_bounds.py
def __init__(self, bounds: Iterable[Bounds] = ()) -> None:
    """Create a normalized bounds set from a collection of bounds.

    Args:
        bounds: The bounds to normalize. Any collection is accepted; the
            stored bounds are sorted by lower bound, with overlapping or
            touching bounds merged.
    """
    object.__setattr__(self, "bounds", _sort_and_merge_bounds(bounds))
__str__ ¤
__str__() -> str

Return a string representation of this set.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __str__(self) -> str:
    """Return a string representation of this set."""
    if not self.bounds:
        return "[None,None]"
    return "∪".join(str(bound) for bound in self.bounds)
is_bounded ¤
is_bounded() -> bool

Return whether this set restricts the accepted values.

This is the explicit spelling of this set's truthiness: the empty (unbounded) set is not bounded, while a set with any bounds is.

RETURNS DESCRIPTION
bool

Whether this set contains any bounds.

Source code in src/frequenz/client/common/metrics/_bounds.py
def is_bounded(self) -> bool:
    """Return whether this set restricts the accepted values.

    This is the explicit spelling of this set's truthiness: the empty
    (unbounded) set is not bounded, while a set with any bounds is.

    Returns:
        Whether this set contains any bounds.
    """
    return bool(self)

frequenz.client.common.metrics.InvalidBounds dataclass ¤

Bases: BaseBounds

Metric bounds with malformed data received from the wire.

This class preserves bounds data that fails the invariants required for a well-formed Bounds, allowing callers to inspect the raw values without accidentally using them for range checks. Use a semantic accessor, such as ElectricalComponent.get_metric_config_bounds(), to receive a clear error on invalid data.

Source code in src/frequenz/client/common/metrics/_bounds.py
@dataclasses.dataclass(frozen=True, kw_only=True)
class InvalidBounds(BaseBounds):
    """Metric bounds with malformed data received from the wire.

    This class preserves bounds data that fails the invariants required for
    a well-formed [`Bounds`][..Bounds], allowing callers to inspect the raw
    values without accidentally using them for range checks. Use a semantic
    accessor, such as `ElectricalComponent.get_metric_config_bounds()`, to
    receive a clear error on invalid data.
    """

    def __str__(self) -> str:
        """Return a compact string representation of these invalid bounds."""
        return f"<invalid:[{self.lower},{self.upper}]>"
Attributes¤
lower class-attribute instance-attribute ¤
lower: FloatInt | None = None

The lower bound.

If None, there is no lower bound.

upper class-attribute instance-attribute ¤
upper: FloatInt | None = None

The upper bound.

If None, there is no upper bound.

Methods:¤
__new__ ¤
__new__(*args: Any, **kwargs: Any) -> Self

Prevent instantiation of this class.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
    """Prevent instantiation of this class."""
    if cls is BaseBounds:
        raise TypeError(f"Cannot instantiate {cls.__name__} directly")
    return super().__new__(cls)
__str__ ¤
__str__() -> str

Return a compact string representation of these invalid bounds.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __str__(self) -> str:
    """Return a compact string representation of these invalid bounds."""
    return f"<invalid:[{self.lower},{self.upper}]>"

frequenz.client.common.metrics.InvalidBoundsError ¤

Bases: InvalidAttributeError

Raised when a semantic accessor sees invalid metric bounds.

The offending InvalidBounds is available as the bounds attribute so callers can inspect the raw wire data.

This is also a ValueError for convenience.

Source code in src/frequenz/client/common/metrics/_bounds.py
class InvalidBoundsError(InvalidAttributeError):
    """Raised when a semantic accessor sees invalid metric bounds.

    The offending [`InvalidBounds`][..InvalidBounds] is available as the
    [`bounds`][.bounds] attribute so callers can inspect the raw wire data.

    This is also a [`ValueError`][] for convenience.
    """

    def __init__(
        self,
        instance: object,
        attr_name: str,
        bounds: InvalidBounds,
        message: str | None = None,
    ) -> None:
        """Initialize this error.

        Args:
            instance: The instance that was being accessed when this error was raised.
            attr_name: The name of the attribute that was being accessed.
            bounds: The invalid bounds instance.
            message: A custom error message. If `None`, a default message mentioning
                the invalid bounds is used.
        """
        self.bounds: InvalidBounds = bounds
        """The invalid bounds that caused this error."""

        super().__init__(
            instance,
            attr_name,
            (
                message
                if message is not None
                else f"invalid bounds {bounds} for attribute {attr_name!r} in {instance}"
            ),
        )
Attributes¤
attr_name instance-attribute ¤
attr_name: str = attr_name

The name of the attribute that had an invalid value.

bounds instance-attribute ¤
bounds: InvalidBounds = bounds

The invalid bounds that caused this error.

instance instance-attribute ¤
instance: object = instance

The object instance that had an invalid value.

Methods:¤
__init__ ¤
__init__(
    instance: object,
    attr_name: str,
    bounds: InvalidBounds,
    message: str | None = None,
) -> None

Initialize this error.

PARAMETER DESCRIPTION
instance

The instance that was being accessed when this error was raised.

TYPE: object

attr_name

The name of the attribute that was being accessed.

TYPE: str

bounds

The invalid bounds instance.

TYPE: InvalidBounds

message

A custom error message. If None, a default message mentioning the invalid bounds is used.

TYPE: str | None DEFAULT: None

Source code in src/frequenz/client/common/metrics/_bounds.py
def __init__(
    self,
    instance: object,
    attr_name: str,
    bounds: InvalidBounds,
    message: str | None = None,
) -> None:
    """Initialize this error.

    Args:
        instance: The instance that was being accessed when this error was raised.
        attr_name: The name of the attribute that was being accessed.
        bounds: The invalid bounds instance.
        message: A custom error message. If `None`, a default message mentioning
            the invalid bounds is used.
    """
    self.bounds: InvalidBounds = bounds
    """The invalid bounds that caused this error."""

    super().__init__(
        instance,
        attr_name,
        (
            message
            if message is not None
            else f"invalid bounds {bounds} for attribute {attr_name!r} in {instance}"
        ),
    )

frequenz.client.common.metrics.InvalidBoundsSet dataclass ¤

A set of metric bounds built from at least one malformed bound.

When a collection of bounds contains any InvalidBounds it cannot be normalized into a well-formed BoundsSet: malformed ranges cannot be meaningfully sorted or merged. This type preserves all of the raw bounds — valid and invalid alike — in their original order, so callers can inspect exactly what was received without accidentally range-checking against broken data.

Unlike BoundsSet, this type intentionally provides no membership test: malformed bounds must not be used for range checks. Use a semantic accessor, such as MetricSample.get_bounds_set(), to receive a clear error on invalid data.

Source code in src/frequenz/client/common/metrics/_bounds.py
@dataclasses.dataclass(frozen=True, kw_only=True)
class InvalidBoundsSet:
    """A set of metric bounds built from at least one malformed bound.

    When a collection of bounds contains any [`InvalidBounds`][..InvalidBounds]
    it cannot be normalized into a well-formed [`BoundsSet`][..BoundsSet]:
    malformed ranges cannot be meaningfully sorted or merged. This type
    preserves all of the raw bounds — valid and invalid alike — in their
    original order, so callers can inspect exactly what was received without
    accidentally range-checking against broken data.

    Unlike [`BoundsSet`][..BoundsSet], this type intentionally provides no
    membership test: malformed bounds must not be used for range checks. Use a
    semantic accessor, such as `MetricSample.get_bounds_set()`, to receive a
    clear error on invalid data.
    """

    bounds: tuple[Bounds | InvalidBounds, ...] = ()
    """The raw bounds, preserved in their original order without merging."""

    def __str__(self) -> str:
        """Return a compact string representation of this invalid set."""
        inner = "∪".join(str(bound) for bound in self.bounds)
        return f"<invalid:{inner}>"
Attributes¤
bounds class-attribute instance-attribute ¤
bounds: tuple[Bounds | InvalidBounds, ...] = ()

The raw bounds, preserved in their original order without merging.

Methods:¤
__str__ ¤
__str__() -> str

Return a compact string representation of this invalid set.

Source code in src/frequenz/client/common/metrics/_bounds.py
def __str__(self) -> str:
    """Return a compact string representation of this invalid set."""
    inner = "∪".join(str(bound) for bound in self.bounds)
    return f"<invalid:{inner}>"

frequenz.client.common.metrics.InvalidBoundsSetError ¤

Bases: InvalidAttributeError

Raised when a semantic accessor sees an invalid bounds set.

The offending InvalidBoundsSet is available as the bounds_set attribute so callers can inspect the raw wire data.

This is also a ValueError for convenience.

Source code in src/frequenz/client/common/metrics/_bounds.py
class InvalidBoundsSetError(InvalidAttributeError):
    """Raised when a semantic accessor sees an invalid bounds set.

    The offending [`InvalidBoundsSet`][..InvalidBoundsSet] is available as the
    [`bounds_set`][.bounds_set] attribute so callers can inspect the raw wire
    data.

    This is also a [`ValueError`][] for convenience.
    """

    def __init__(
        self,
        instance: object,
        attr_name: str,
        bounds_set: InvalidBoundsSet,
        message: str | None = None,
    ) -> None:
        """Initialize this error.

        Args:
            instance: The instance that was being accessed when this error was raised.
            attr_name: The name of the attribute that was being accessed.
            bounds_set: The invalid bounds set instance.
            message: A custom error message. If `None`, a default message mentioning
                the invalid bounds set is used.
        """
        self.bounds_set: InvalidBoundsSet = bounds_set
        """The invalid bounds set that caused this error."""

        super().__init__(
            instance,
            attr_name,
            (
                message
                if message is not None
                else f"invalid bounds set {bounds_set} for attribute {attr_name!r} in {instance}"
            ),
        )
Attributes¤
attr_name instance-attribute ¤
attr_name: str = attr_name

The name of the attribute that had an invalid value.

bounds_set instance-attribute ¤
bounds_set: InvalidBoundsSet = bounds_set

The invalid bounds set that caused this error.

instance instance-attribute ¤
instance: object = instance

The object instance that had an invalid value.

Methods:¤
__init__ ¤
__init__(
    instance: object,
    attr_name: str,
    bounds_set: InvalidBoundsSet,
    message: str | None = None,
) -> None

Initialize this error.

PARAMETER DESCRIPTION
instance

The instance that was being accessed when this error was raised.

TYPE: object

attr_name

The name of the attribute that was being accessed.

TYPE: str

bounds_set

The invalid bounds set instance.

TYPE: InvalidBoundsSet

message

A custom error message. If None, a default message mentioning the invalid bounds set is used.

TYPE: str | None DEFAULT: None

Source code in src/frequenz/client/common/metrics/_bounds.py
def __init__(
    self,
    instance: object,
    attr_name: str,
    bounds_set: InvalidBoundsSet,
    message: str | None = None,
) -> None:
    """Initialize this error.

    Args:
        instance: The instance that was being accessed when this error was raised.
        attr_name: The name of the attribute that was being accessed.
        bounds_set: The invalid bounds set instance.
        message: A custom error message. If `None`, a default message mentioning
            the invalid bounds set is used.
    """
    self.bounds_set: InvalidBoundsSet = bounds_set
    """The invalid bounds set that caused this error."""

    super().__init__(
        instance,
        attr_name,
        (
            message
            if message is not None
            else f"invalid bounds set {bounds_set} for attribute {attr_name!r} in {instance}"
        ),
    )

frequenz.client.common.metrics.Metric ¤

Bases: Enum

List of supported metrics.

Metric units are as follows:

  • VOLTAGE: V (Volts)
  • CURRENT: A (Amperes)
  • POWER_ACTIVE: W (Watts)
  • POWER_APPARENT: VA (Volt-Amperes)
  • POWER_REACTIVE: VAr (Volt-Amperes reactive)
  • ENERGY_ACTIVE: Wh (Watt-hours)
  • ENERGY_APPARENT: VAh (Volt-Ampere hours)
  • ENERGY_REACTIVE: VArh (Volt-Ampere reactive hours)
  • FREQUENCY: Hz (Hertz)
  • TEMPERATURE: °C (Degree Celsius)
  • BATTERY_SOC_PCT: % (percentage)
  • BATTERY_CAPACITY: Wh (Watt-hours)
  • FACTOR: no unit
AC energy metrics information
  • This energy metric is reported directly from the component, and not a result of aggregations in our systems. If a component does not have this metric, this field cannot be populated.

  • Components that provide energy metrics reset this metric from time to time. This behaviour is specific to each component model. E.g., some components reset it on UTC 00:00:00.

  • This energy metric does not specify the start time of the accumulation period, and therefore can be inconsistent.

Source code in src/frequenz/client/common/metrics/_metric.py
@unique
class Metric(Enum):
    """List of supported metrics.

    Metric units are as follows:

    * `VOLTAGE`: V (Volts)
    * `CURRENT`: A (Amperes)
    * `POWER_ACTIVE`: W (Watts)
    * `POWER_APPARENT`: VA (Volt-Amperes)
    * `POWER_REACTIVE`: VAr (Volt-Amperes reactive)
    * `ENERGY_ACTIVE`: Wh (Watt-hours)
    * `ENERGY_APPARENT`: VAh (Volt-Ampere hours)
    * `ENERGY_REACTIVE`: VArh (Volt-Ampere reactive hours)
    * `FREQUENCY`: Hz (Hertz)
    * `TEMPERATURE`: °C (Degree Celsius)
    * `BATTERY_SOC_PCT`: % (percentage)
    * `BATTERY_CAPACITY`: Wh (Watt-hours)
    * `FACTOR`: no unit

    Note: AC energy metrics information
        - This energy metric is reported directly from the component, and not a
          result of aggregations in our systems. If a component does not have this
          metric, this field cannot be populated.

        - Components that provide energy metrics reset this metric from time to
          time. This behaviour is specific to each component model. E.g., some
          components reset it on UTC 00:00:00.

        - This energy metric does not specify the start time of the accumulation
          period, and therefore can be inconsistent.
    """

    UNSPECIFIED = deprecated_member(
        0,
        "Metric.UNSPECIFIED is deprecated; use the `int` value `0` "
        "instead if you really need to check for this low-level value.",
    )
    """The metric is unspecified (this should not be used)."""

    DC_VOLTAGE = 1
    """The DC voltage."""

    DC_CURRENT = 2
    """The DC current."""

    DC_POWER = 3
    """The DC power."""

    AC_FREQUENCY = 10
    """The AC frequency."""

    AC_VOLTAGE = 11
    """The AC electric potential difference."""

    AC_VOLTAGE_PHASE_1_N = 12
    """The AC electric potential difference between phase 1 and neutral."""

    AC_VOLTAGE_PHASE_2_N = 13
    """The AC electric potential difference between phase 2 and neutral."""

    AC_VOLTAGE_PHASE_3_N = 14
    """The AC electric potential difference between phase 3 and neutral."""

    AC_VOLTAGE_PHASE_1_PHASE_2 = 15
    """The AC electric potential difference between phase 1 and phase 2."""

    AC_VOLTAGE_PHASE_2_PHASE_3 = 16
    """The AC electric potential difference between phase 2 and phase 3."""

    AC_VOLTAGE_PHASE_3_PHASE_1 = 17
    """The AC electric potential difference between phase 3 and phase 1."""

    AC_CURRENT = 18
    """The AC current."""

    AC_CURRENT_PHASE_1 = 19
    """The AC current in phase 1."""

    AC_CURRENT_PHASE_2 = 20
    """The AC current in phase 2."""

    AC_CURRENT_PHASE_3 = 21
    """The AC current in phase 3."""

    AC_POWER_APPARENT = 22
    """The AC apparent power."""

    AC_POWER_APPARENT_PHASE_1 = 23
    """The AC apparent power in phase 1."""

    AC_POWER_APPARENT_PHASE_2 = 24
    """The AC apparent power in phase 2."""

    AC_POWER_APPARENT_PHASE_3 = 25
    """The AC apparent power in phase 3."""

    AC_POWER_ACTIVE = 26
    """The AC active power."""

    AC_POWER_ACTIVE_PHASE_1 = 27
    """The AC active power in phase 1."""

    AC_POWER_ACTIVE_PHASE_2 = 28
    """The AC active power in phase 2."""

    AC_POWER_ACTIVE_PHASE_3 = 29
    """The AC active power in phase 3."""

    AC_POWER_REACTIVE = 30
    """The AC reactive power."""

    AC_POWER_REACTIVE_PHASE_1 = 31
    """The AC reactive power in phase 1."""

    AC_POWER_REACTIVE_PHASE_2 = 32
    """The AC reactive power in phase 2."""

    AC_POWER_REACTIVE_PHASE_3 = 33
    """The AC reactive power in phase 3."""

    AC_POWER_FACTOR = 40
    """The AC power factor."""

    AC_POWER_FACTOR_PHASE_1 = 41
    """The AC power factor in phase 1."""

    AC_POWER_FACTOR_PHASE_2 = 42
    """The AC power factor in phase 2."""

    AC_POWER_FACTOR_PHASE_3 = 43
    """The AC power factor in phase 3."""

    AC_ENERGY_APPARENT = 50
    """The AC apparent energy."""

    AC_ENERGY_APPARENT_PHASE_1 = 51
    """The AC apparent energy in phase 1."""

    AC_ENERGY_APPARENT_PHASE_2 = 52
    """The AC apparent energy in phase 2."""

    AC_ENERGY_APPARENT_PHASE_3 = 53
    """The AC apparent energy in phase 3."""

    AC_ENERGY_ACTIVE = 54
    """The AC active energy."""

    AC_ENERGY_ACTIVE_PHASE_1 = 55
    """The AC active energy in phase 1."""

    AC_ENERGY_ACTIVE_PHASE_2 = 56
    """The AC active energy in phase 2."""

    AC_ENERGY_ACTIVE_PHASE_3 = 57
    """The AC active energy in phase 3."""

    AC_ENERGY_ACTIVE_CONSUMED = 58
    """The AC active energy consumed."""

    AC_ENERGY_ACTIVE_CONSUMED_PHASE_1 = 59
    """The AC active energy consumed in phase 1."""

    AC_ENERGY_ACTIVE_CONSUMED_PHASE_2 = 60
    """The AC active energy consumed in phase 2."""

    AC_ENERGY_ACTIVE_CONSUMED_PHASE_3 = 61
    """The AC active energy consumed in phase 3."""

    AC_ENERGY_ACTIVE_DELIVERED = 62
    """The AC active energy delivered."""

    AC_ENERGY_ACTIVE_DELIVERED_PHASE_1 = 63
    """The AC active energy delivered in phase 1."""

    AC_ENERGY_ACTIVE_DELIVERED_PHASE_2 = 64
    """The AC active energy delivered in phase 2."""

    AC_ENERGY_ACTIVE_DELIVERED_PHASE_3 = 65
    """The AC active energy delivered in phase 3."""

    AC_ENERGY_REACTIVE = 66
    """The AC reactive energy."""

    AC_ENERGY_REACTIVE_PHASE_1 = 67
    """The AC reactive energy in phase 1."""

    AC_ENERGY_REACTIVE_PHASE_2 = 68
    """The AC reactive energy in phase 2."""

    AC_ENERGY_REACTIVE_PHASE_3 = 69
    """The AC reactive energy in phase 3."""

    AC_TOTAL_HARMONIC_DISTORTION_CURRENT = 80
    """The AC total harmonic distortion current."""

    AC_TOTAL_HARMONIC_DISTORTION_CURRENT_PHASE_1 = 81
    """The AC total harmonic distortion current in phase 1."""

    AC_TOTAL_HARMONIC_DISTORTION_CURRENT_PHASE_2 = 82
    """The AC total harmonic distortion current in phase 2."""

    AC_TOTAL_HARMONIC_DISTORTION_CURRENT_PHASE_3 = 83
    """The AC total harmonic distortion current in phase 3."""

    BATTERY_CAPACITY = 100
    """The capacity of the battery."""

    BATTERY_SOC_PCT = 101
    """The state of charge of the battery as a percentage."""

    BATTERY_TEMPERATURE = 102
    """The temperature of the battery."""

    INVERTER_TEMPERATURE = 120
    """The temperature of the inverter."""

    INVERTER_TEMPERATURE_CABINET = 121
    """The temperature of the inverter cabinet."""

    INVERTER_TEMPERATURE_HEATSINK = 122
    """The temperature of the inverter heatsink."""

    INVERTER_TEMPERATURE_TRANSFORMER = 123
    """The temperature of the inverter transformer."""

    EV_CHARGER_TEMPERATURE = 140
    """The temperature of the EV charger."""

    SENSOR_WIND_SPEED = 160
    """The speed of the wind measured."""

    SENSOR_WIND_DIRECTION = 161
    """The direction of the wind measured."""

    SENSOR_TEMPERATURE = 162
    """The temperature measured."""

    SENSOR_RELATIVE_HUMIDITY = 163
    """The relative humidity measured."""

    SENSOR_DEW_POINT = 164
    """The dew point measured."""

    SENSOR_AIR_PRESSURE = 165
    """The air pressure measured."""

    SENSOR_IRRADIANCE = 166
    """The irradiance measured."""
Attributes¤
AC_CURRENT class-attribute instance-attribute ¤
AC_CURRENT = 18

The AC current.

AC_CURRENT_PHASE_1 class-attribute instance-attribute ¤
AC_CURRENT_PHASE_1 = 19

The AC current in phase 1.

AC_CURRENT_PHASE_2 class-attribute instance-attribute ¤
AC_CURRENT_PHASE_2 = 20

The AC current in phase 2.

AC_CURRENT_PHASE_3 class-attribute instance-attribute ¤
AC_CURRENT_PHASE_3 = 21

The AC current in phase 3.

AC_ENERGY_ACTIVE class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE = 54

The AC active energy.

AC_ENERGY_ACTIVE_CONSUMED class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_CONSUMED = 58

The AC active energy consumed.

AC_ENERGY_ACTIVE_CONSUMED_PHASE_1 class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_CONSUMED_PHASE_1 = 59

The AC active energy consumed in phase 1.

AC_ENERGY_ACTIVE_CONSUMED_PHASE_2 class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_CONSUMED_PHASE_2 = 60

The AC active energy consumed in phase 2.

AC_ENERGY_ACTIVE_CONSUMED_PHASE_3 class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_CONSUMED_PHASE_3 = 61

The AC active energy consumed in phase 3.

AC_ENERGY_ACTIVE_DELIVERED class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_DELIVERED = 62

The AC active energy delivered.

AC_ENERGY_ACTIVE_DELIVERED_PHASE_1 class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_DELIVERED_PHASE_1 = 63

The AC active energy delivered in phase 1.

AC_ENERGY_ACTIVE_DELIVERED_PHASE_2 class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_DELIVERED_PHASE_2 = 64

The AC active energy delivered in phase 2.

AC_ENERGY_ACTIVE_DELIVERED_PHASE_3 class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_DELIVERED_PHASE_3 = 65

The AC active energy delivered in phase 3.

AC_ENERGY_ACTIVE_PHASE_1 class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_PHASE_1 = 55

The AC active energy in phase 1.

AC_ENERGY_ACTIVE_PHASE_2 class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_PHASE_2 = 56

The AC active energy in phase 2.

AC_ENERGY_ACTIVE_PHASE_3 class-attribute instance-attribute ¤
AC_ENERGY_ACTIVE_PHASE_3 = 57

The AC active energy in phase 3.

AC_ENERGY_APPARENT class-attribute instance-attribute ¤
AC_ENERGY_APPARENT = 50

The AC apparent energy.

AC_ENERGY_APPARENT_PHASE_1 class-attribute instance-attribute ¤
AC_ENERGY_APPARENT_PHASE_1 = 51

The AC apparent energy in phase 1.

AC_ENERGY_APPARENT_PHASE_2 class-attribute instance-attribute ¤
AC_ENERGY_APPARENT_PHASE_2 = 52

The AC apparent energy in phase 2.

AC_ENERGY_APPARENT_PHASE_3 class-attribute instance-attribute ¤
AC_ENERGY_APPARENT_PHASE_3 = 53

The AC apparent energy in phase 3.

AC_ENERGY_REACTIVE class-attribute instance-attribute ¤
AC_ENERGY_REACTIVE = 66

The AC reactive energy.

AC_ENERGY_REACTIVE_PHASE_1 class-attribute instance-attribute ¤
AC_ENERGY_REACTIVE_PHASE_1 = 67

The AC reactive energy in phase 1.

AC_ENERGY_REACTIVE_PHASE_2 class-attribute instance-attribute ¤
AC_ENERGY_REACTIVE_PHASE_2 = 68

The AC reactive energy in phase 2.

AC_ENERGY_REACTIVE_PHASE_3 class-attribute instance-attribute ¤
AC_ENERGY_REACTIVE_PHASE_3 = 69

The AC reactive energy in phase 3.

AC_FREQUENCY class-attribute instance-attribute ¤
AC_FREQUENCY = 10

The AC frequency.

AC_POWER_ACTIVE class-attribute instance-attribute ¤
AC_POWER_ACTIVE = 26

The AC active power.

AC_POWER_ACTIVE_PHASE_1 class-attribute instance-attribute ¤
AC_POWER_ACTIVE_PHASE_1 = 27

The AC active power in phase 1.

AC_POWER_ACTIVE_PHASE_2 class-attribute instance-attribute ¤
AC_POWER_ACTIVE_PHASE_2 = 28

The AC active power in phase 2.

AC_POWER_ACTIVE_PHASE_3 class-attribute instance-attribute ¤
AC_POWER_ACTIVE_PHASE_3 = 29

The AC active power in phase 3.

AC_POWER_APPARENT class-attribute instance-attribute ¤
AC_POWER_APPARENT = 22

The AC apparent power.

AC_POWER_APPARENT_PHASE_1 class-attribute instance-attribute ¤
AC_POWER_APPARENT_PHASE_1 = 23

The AC apparent power in phase 1.

AC_POWER_APPARENT_PHASE_2 class-attribute instance-attribute ¤
AC_POWER_APPARENT_PHASE_2 = 24

The AC apparent power in phase 2.

AC_POWER_APPARENT_PHASE_3 class-attribute instance-attribute ¤
AC_POWER_APPARENT_PHASE_3 = 25

The AC apparent power in phase 3.

AC_POWER_FACTOR class-attribute instance-attribute ¤
AC_POWER_FACTOR = 40

The AC power factor.

AC_POWER_FACTOR_PHASE_1 class-attribute instance-attribute ¤
AC_POWER_FACTOR_PHASE_1 = 41

The AC power factor in phase 1.

AC_POWER_FACTOR_PHASE_2 class-attribute instance-attribute ¤
AC_POWER_FACTOR_PHASE_2 = 42

The AC power factor in phase 2.

AC_POWER_FACTOR_PHASE_3 class-attribute instance-attribute ¤
AC_POWER_FACTOR_PHASE_3 = 43

The AC power factor in phase 3.

AC_POWER_REACTIVE class-attribute instance-attribute ¤
AC_POWER_REACTIVE = 30

The AC reactive power.

AC_POWER_REACTIVE_PHASE_1 class-attribute instance-attribute ¤
AC_POWER_REACTIVE_PHASE_1 = 31

The AC reactive power in phase 1.

AC_POWER_REACTIVE_PHASE_2 class-attribute instance-attribute ¤
AC_POWER_REACTIVE_PHASE_2 = 32

The AC reactive power in phase 2.

AC_POWER_REACTIVE_PHASE_3 class-attribute instance-attribute ¤
AC_POWER_REACTIVE_PHASE_3 = 33

The AC reactive power in phase 3.

AC_TOTAL_HARMONIC_DISTORTION_CURRENT class-attribute instance-attribute ¤
AC_TOTAL_HARMONIC_DISTORTION_CURRENT = 80

The AC total harmonic distortion current.

AC_TOTAL_HARMONIC_DISTORTION_CURRENT_PHASE_1 class-attribute instance-attribute ¤
AC_TOTAL_HARMONIC_DISTORTION_CURRENT_PHASE_1 = 81

The AC total harmonic distortion current in phase 1.

AC_TOTAL_HARMONIC_DISTORTION_CURRENT_PHASE_2 class-attribute instance-attribute ¤
AC_TOTAL_HARMONIC_DISTORTION_CURRENT_PHASE_2 = 82

The AC total harmonic distortion current in phase 2.

AC_TOTAL_HARMONIC_DISTORTION_CURRENT_PHASE_3 class-attribute instance-attribute ¤
AC_TOTAL_HARMONIC_DISTORTION_CURRENT_PHASE_3 = 83

The AC total harmonic distortion current in phase 3.

AC_VOLTAGE class-attribute instance-attribute ¤
AC_VOLTAGE = 11

The AC electric potential difference.

AC_VOLTAGE_PHASE_1_N class-attribute instance-attribute ¤
AC_VOLTAGE_PHASE_1_N = 12

The AC electric potential difference between phase 1 and neutral.

AC_VOLTAGE_PHASE_1_PHASE_2 class-attribute instance-attribute ¤
AC_VOLTAGE_PHASE_1_PHASE_2 = 15

The AC electric potential difference between phase 1 and phase 2.

AC_VOLTAGE_PHASE_2_N class-attribute instance-attribute ¤
AC_VOLTAGE_PHASE_2_N = 13

The AC electric potential difference between phase 2 and neutral.

AC_VOLTAGE_PHASE_2_PHASE_3 class-attribute instance-attribute ¤
AC_VOLTAGE_PHASE_2_PHASE_3 = 16

The AC electric potential difference between phase 2 and phase 3.

AC_VOLTAGE_PHASE_3_N class-attribute instance-attribute ¤
AC_VOLTAGE_PHASE_3_N = 14

The AC electric potential difference between phase 3 and neutral.

AC_VOLTAGE_PHASE_3_PHASE_1 class-attribute instance-attribute ¤
AC_VOLTAGE_PHASE_3_PHASE_1 = 17

The AC electric potential difference between phase 3 and phase 1.

BATTERY_CAPACITY class-attribute instance-attribute ¤
BATTERY_CAPACITY = 100

The capacity of the battery.

BATTERY_SOC_PCT class-attribute instance-attribute ¤
BATTERY_SOC_PCT = 101

The state of charge of the battery as a percentage.

BATTERY_TEMPERATURE class-attribute instance-attribute ¤
BATTERY_TEMPERATURE = 102

The temperature of the battery.

DC_CURRENT class-attribute instance-attribute ¤
DC_CURRENT = 2

The DC current.

DC_POWER class-attribute instance-attribute ¤
DC_POWER = 3

The DC power.

DC_VOLTAGE class-attribute instance-attribute ¤
DC_VOLTAGE = 1

The DC voltage.

EV_CHARGER_TEMPERATURE class-attribute instance-attribute ¤
EV_CHARGER_TEMPERATURE = 140

The temperature of the EV charger.

INVERTER_TEMPERATURE class-attribute instance-attribute ¤
INVERTER_TEMPERATURE = 120

The temperature of the inverter.

INVERTER_TEMPERATURE_CABINET class-attribute instance-attribute ¤
INVERTER_TEMPERATURE_CABINET = 121

The temperature of the inverter cabinet.

INVERTER_TEMPERATURE_HEATSINK class-attribute instance-attribute ¤
INVERTER_TEMPERATURE_HEATSINK = 122

The temperature of the inverter heatsink.

INVERTER_TEMPERATURE_TRANSFORMER class-attribute instance-attribute ¤
INVERTER_TEMPERATURE_TRANSFORMER = 123

The temperature of the inverter transformer.

SENSOR_AIR_PRESSURE class-attribute instance-attribute ¤
SENSOR_AIR_PRESSURE = 165

The air pressure measured.

SENSOR_DEW_POINT class-attribute instance-attribute ¤
SENSOR_DEW_POINT = 164

The dew point measured.

SENSOR_IRRADIANCE class-attribute instance-attribute ¤
SENSOR_IRRADIANCE = 166

The irradiance measured.

SENSOR_RELATIVE_HUMIDITY class-attribute instance-attribute ¤
SENSOR_RELATIVE_HUMIDITY = 163

The relative humidity measured.

SENSOR_TEMPERATURE class-attribute instance-attribute ¤
SENSOR_TEMPERATURE = 162

The temperature measured.

SENSOR_WIND_DIRECTION class-attribute instance-attribute ¤
SENSOR_WIND_DIRECTION = 161

The direction of the wind measured.

SENSOR_WIND_SPEED class-attribute instance-attribute ¤
SENSOR_WIND_SPEED = 160

The speed of the wind measured.

UNSPECIFIED class-attribute instance-attribute ¤
UNSPECIFIED = deprecated_member(
    0,
    "Metric.UNSPECIFIED is deprecated; use the `int` value `0` instead if you really need to check for this low-level value.",
)

The metric is unspecified (this should not be used).

frequenz.client.common.metrics.MetricConnection dataclass ¤

A connection from which a metric was obtained.

Source code in src/frequenz/client/common/metrics/_sample.py
@dataclass(frozen=True, kw_only=True)
class MetricConnection:
    """A connection from which a metric was obtained."""

    category: MetricConnectionCategory | int
    """The category of the connection from which the metric was obtained.

    This is the lower-level, forward-compatible accessor: it may hold a known
    `MetricConnectionCategory` member, the raw `int` `0` when the category is
    unspecified, or any other raw `int` not yet known to this client. Prefer
    `MetricConnection.get_category()` to obtain a known member or a clear error.
    """

    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
    can be obtained from multiple distinct inputs or connection points on the
    component. Knowing the connection for the metric can help in certain control
    and monitoring applications.
    """

    def __str__(self) -> str:
        """Return a string representation of this connection."""
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=DeprecationWarning)
            match self.category:
                case 0 | MetricConnectionCategory.UNSPECIFIED:
                    category_name = "cat=<invalid:0>"
                case MetricConnectionCategory() as category:
                    category_name = category.name
                case int() as category:
                    category_name = f"cat={category}"
                case unexpected:
                    assert_never(unexpected)
        return f"{self.name}:{category_name}"

    def get_category(self) -> MetricConnectionCategory:
        """Return the connection category as a known enum member.

        This is the higher-level accessor for the lower-level
        [`category`][frequenz.client.common.metrics.MetricConnection.category]
        field: it returns a known member or raises instead of exposing the raw
        sentinel `0` or an unknown `int`.

        Returns:
            The category when it is a known `MetricConnectionCategory` member.

        Raises:
            UnspecifiedEnumValueError: If the category is unspecified (the raw
                value `0` or a member whose value is `0`).
            UnrecognizedEnumValueError: If the category is an `int` this
                client does not recognize. The raw value is available on the
                error's `value` attribute.
        """
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=DeprecationWarning)
            match self.category:
                case 0 | MetricConnectionCategory.UNSPECIFIED:
                    raise UnspecifiedEnumValueError(self, "category")
                case MetricConnectionCategory():
                    return self.category
                case int():
                    raise UnrecognizedEnumValueError(self, "category", self.category)
                case unexpected:
                    assert_never(unexpected)
Attributes¤
category instance-attribute ¤

The category of the connection from which the metric was obtained.

This is the lower-level, forward-compatible accessor: it may hold a known MetricConnectionCategory member, the raw int 0 when the category is unspecified, or any other raw int not yet known to this client. Prefer MetricConnection.get_category() to obtain a known member or a clear error.

name class-attribute instance-attribute ¤
name: str = ''

The name of the specific connection from which the metric was obtained.

This is expected to be populated when the same Metric variant can be obtained from multiple distinct inputs or connection points on the component. Knowing the connection for the metric can help in certain control and monitoring applications.

Methods:¤
__str__ ¤
__str__() -> str

Return a string representation of this connection.

Source code in src/frequenz/client/common/metrics/_sample.py
def __str__(self) -> str:
    """Return a string representation of this connection."""
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", category=DeprecationWarning)
        match self.category:
            case 0 | MetricConnectionCategory.UNSPECIFIED:
                category_name = "cat=<invalid:0>"
            case MetricConnectionCategory() as category:
                category_name = category.name
            case int() as category:
                category_name = f"cat={category}"
            case unexpected:
                assert_never(unexpected)
    return f"{self.name}:{category_name}"
get_category ¤
get_category() -> MetricConnectionCategory

Return the connection category as a known enum member.

This is the higher-level accessor for the lower-level category field: it returns a known member or raises instead of exposing the raw sentinel 0 or an unknown int.

RETURNS DESCRIPTION
MetricConnectionCategory

The category when it is a known MetricConnectionCategory member.

RAISES DESCRIPTION
UnspecifiedEnumValueError

If the category is unspecified (the raw value 0 or a member whose value is 0).

UnrecognizedEnumValueError

If the category is an int this client does not recognize. The raw value is available on the error's value attribute.

Source code in src/frequenz/client/common/metrics/_sample.py
def get_category(self) -> MetricConnectionCategory:
    """Return the connection category as a known enum member.

    This is the higher-level accessor for the lower-level
    [`category`][frequenz.client.common.metrics.MetricConnection.category]
    field: it returns a known member or raises instead of exposing the raw
    sentinel `0` or an unknown `int`.

    Returns:
        The category when it is a known `MetricConnectionCategory` member.

    Raises:
        UnspecifiedEnumValueError: If the category is unspecified (the raw
            value `0` or a member whose value is `0`).
        UnrecognizedEnumValueError: If the category is an `int` this
            client does not recognize. The raw value is available on the
            error's `value` attribute.
    """
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", category=DeprecationWarning)
        match self.category:
            case 0 | MetricConnectionCategory.UNSPECIFIED:
                raise UnspecifiedEnumValueError(self, "category")
            case MetricConnectionCategory():
                return self.category
            case int():
                raise UnrecognizedEnumValueError(self, "category", self.category)
            case unexpected:
                assert_never(unexpected)

frequenz.client.common.metrics.MetricConnectionCategory ¤

Bases: Enum

The categories of connections from which metrics can be obtained.

Source code in src/frequenz/client/common/metrics/_sample.py
@unique
class MetricConnectionCategory(Enum):
    """The categories of connections from which metrics can be obtained."""

    UNSPECIFIED = deprecated_member(
        0,
        "MetricConnectionCategory.UNSPECIFIED is deprecated; use the `int` value `0` "
        "instead if you really need to check for this low-level value.",
    )
    """The connection category was not specified (do not use)."""

    OTHER = 1
    """A generic connection for metrics that do not fit into any other category."""

    BATTERY = 2
    """A connection to a metric representing a battery."""

    PV = 3
    """A connection to a metric representing a PV (photovoltaic) array or string."""

    AMBIENT = 10
    """A connection to a metric representing ambient conditions."""

    CABINET = 11
    """A connection to a metric representing a cabinet or an enclosure."""

    HEATSINK = 12
    """A connection to a metric representing a heatsink."""

    TRANSFORMER = 13
    """A connection to a metric representing a transformer."""
Attributes¤
AMBIENT class-attribute instance-attribute ¤
AMBIENT = 10

A connection to a metric representing ambient conditions.

BATTERY class-attribute instance-attribute ¤
BATTERY = 2

A connection to a metric representing a battery.

CABINET class-attribute instance-attribute ¤
CABINET = 11

A connection to a metric representing a cabinet or an enclosure.

HEATSINK class-attribute instance-attribute ¤
HEATSINK = 12

A connection to a metric representing a heatsink.

OTHER class-attribute instance-attribute ¤
OTHER = 1

A generic connection for metrics that do not fit into any other category.

PV class-attribute instance-attribute ¤
PV = 3

A connection to a metric representing a PV (photovoltaic) array or string.

TRANSFORMER class-attribute instance-attribute ¤
TRANSFORMER = 13

A connection to a metric representing a transformer.

UNSPECIFIED class-attribute instance-attribute ¤
UNSPECIFIED = deprecated_member(
    0,
    "MetricConnectionCategory.UNSPECIFIED is deprecated; use the `int` value `0` instead if you really need to check for this low-level value.",
)

The connection category was not specified (do not use).

frequenz.client.common.metrics.MetricSample dataclass ¤

A sampled metric.

This represents a single sample of a specific metric, the value of which is either measured or derived at a particular time. The real-time system-defined bounds are optional and may not always be present or set.

Relationship Between Bounds and Metric Samples

Suppose a metric sample for active power has a lower-bound of -10,000 W, and an upper-bound of 10,000 W. For the system to accept a charge command, clients need to request current values within the bounds.

Source code in src/frequenz/client/common/metrics/_sample.py
@dataclass(frozen=True, init=False)
class MetricSample:
    """A sampled metric.

    This represents a single sample of a specific metric, the value of which is either
    measured or derived at a particular time. The real-time system-defined bounds are
    optional and may not always be present or set.

    Note: Relationship Between Bounds and Metric Samples
        Suppose a metric sample for active power has a lower-bound of -10,000 W, and an
        upper-bound of 10,000 W. For the system to accept a charge command, clients need
        to request current values within the bounds.
    """

    sample_time: datetime
    """The moment when the metric was sampled."""

    metric: Metric | int
    """The metric that was sampled.

    This is the lower-level, forward-compatible accessor: it may hold a known
    `Metric` member, the raw `int` `0` when the metric is unspecified, or any
    other raw `int` not yet known to this client. Prefer
    `MetricSample.get_metric()` to obtain a known member or a clear error.
    """

    value: FloatInt | AggregatedMetricValue | None
    """The value of the sampled metric."""

    bounds_set: BoundsSet | InvalidBoundsSet
    """The bounds that apply to the metric sample.

    These bounds adapt in real-time to reflect the operating conditions at the time of
    aggregation or derivation. They form a union: the value of the metric must be within
    at least one of them, and an empty [`BoundsSet`][...BoundsSet] means the metric is
    unbounded.

    This is a [`BoundsSet`][...BoundsSet] for well-formed data, or an
    [`InvalidBoundsSet`][...InvalidBoundsSet] preserving the raw bounds when the wire
    carried any malformed entry, so callers must handle both.

    Tip:
        Prefer `MetricSample.get_bounds_set()` to obtain a valid `BoundsSet` or a
        clear error.

    In accordance with the passive sign convention, bounds that limit discharge would
    have negative numbers, while those limiting charge, such as for the State of Power
    (SoP) metric, would be positive. Hence bounds can have positive and negative values
    depending on the metric they represent.
    """

    connection: MetricConnection | None = None
    """The specific source or connection from which the metric was sampled.

    This will be present when the same [`Metric`][...Metric] can be obtained from
    multiple sources or connections. Knowing the connection can help in certain
    control and monitoring applications.

    In cases where the component has just one connection for a metric, then the
    connection is `None`.

    Example:
        A hybrid inverter can have a DC string for a battery and another DC string for a
        PV array. The connection names could resemble, say, `dc_battery_0` (category
        `BATTERY`) and `dc_pv_0` (category `PV`). A metric like DC voltage can be
        obtained from both connections. For an application to determine the SoC of the
        battery using the battery voltage, which connection the voltage metric was
        sampled from is important.
    """

    # This custom `__init__` should be removed once the deprecated `bounds` field is removed.
    # pylint: disable-next=too-many-arguments
    def __init__(
        self,
        *,
        sample_time: datetime,
        metric: Metric | int,
        value: FloatInt | AggregatedMetricValue | None,
        bounds_set: BoundsSet | InvalidBoundsSet | None = None,
        bounds: list[Bounds] | None = None,
        connection: MetricConnection | None = None,
    ) -> None:
        """Initialize this metric sample.

        Args:
            sample_time: The moment when the metric was sampled.
            metric: The metric that was sampled.
            value: The value of the sampled metric.
            bounds_set: The bounds that apply to the metric sample.
            bounds: Deprecated alias that accepts a list of valid
                [`Bounds`][...Bounds] and stores them as a
                [`BoundsSet`][...BoundsSet]. Use `bounds_set` instead.
            connection: The source or connection the metric was sampled from.

        Raises:
            TypeError: If both `bounds_set` and the deprecated `bounds` are
                given, or if neither is given.
        """
        if bounds is not None and bounds_set is not None:
            raise TypeError(
                "`MetricSample` accepts either `bounds_set` or the deprecated "
                "`bounds`, not both."
            )
        if bounds is not None:
            warnings.warn(
                "The `bounds` argument is deprecated; use `bounds_set` instead.",
                DeprecationWarning,
                stacklevel=2,
            )
            bounds_set = BoundsSet(bounds=tuple(bounds))
        if bounds_set is None:
            raise TypeError("`MetricSample` requires the `bounds_set` argument.")
        object.__setattr__(self, "sample_time", sample_time)
        object.__setattr__(self, "metric", metric)
        object.__setattr__(self, "value", value)
        object.__setattr__(self, "bounds_set", bounds_set)
        object.__setattr__(self, "connection", connection)

    def __str__(self) -> str:
        """Return a compact string representation of this sample."""
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=DeprecationWarning)
            match self.metric:
                case 0 | Metric.UNSPECIFIED:
                    metric = "<invalid:0>"
                case Metric() as known:
                    metric = known.name
                case int() as unknown:
                    metric = str(unknown)
                case unexpected:
                    assert_never(unexpected)
        sample = f"{metric}={self.value}"
        if self.connection is not None:
            sample = f"{sample}@{self.connection}"
        return sample

    @property
    @deprecated("`MetricSample.bounds` is deprecated; use `bounds_set` instead.")
    def bounds(self) -> list[Bounds]:
        """The valid bounds that apply to the metric sample.

        Deprecated:
            Use `bounds_set` instead. For backward compatibility this returns
            only the valid [`Bounds`][...Bounds] from `bounds_set` (dropping any
            malformed entries, as the old field did), but it returns the
            normalized, merged bounds rather than the raw list received on the
            wire.

        Returns:
            The valid bounds in `bounds_set`.
        """
        valid = tuple(
            bound for bound in self.bounds_set.bounds if isinstance(bound, Bounds)
        )
        return list(BoundsSet(bounds=valid).bounds)

    def as_single_value(
        self, *, aggregation_method: AggregationMethod = AggregationMethod.AVG
    ) -> FloatInt | None:
        """Return the value of this sample as a single value.

        If [`value`][..value] is a number, it is returned as is. If `value`
        is an [`AggregatedMetricValue`][...AggregatedMetricValue], the value is
        aggregated using the provided `aggregation_method`.

        Args:
            aggregation_method: The method to use to aggregate the value when `value`
                is an [`AggregatedMetricValue`][...AggregatedMetricValue].

        Returns:
            The value of the sample as a single value, or `None` if the value is `None`.
        """
        match self.value:
            case float() | int():
                return self.value
            case AggregatedMetricValue():
                match aggregation_method:
                    case AggregationMethod.AVG:
                        return self.value.avg
                    case AggregationMethod.MIN:
                        return self.value.min
                    case AggregationMethod.MAX:
                        return self.value.max
                    case unexpected:
                        assert_never(unexpected)
            case None:
                return None
            case unexpected:
                assert_never(unexpected)

    def get_metric(self) -> Metric:
        """Return the sampled metric as a known enum member.

        This is the higher-level accessor for the lower-level
        [`metric`][frequenz.client.common.metrics.MetricSample.metric] field: it
        returns a known member or raises instead of exposing the raw sentinel
        `0` or an unknown `int`.

        Returns:
            The metric when it is a known `Metric` member.

        Raises:
            UnspecifiedEnumValueError: If the metric is unspecified (the raw
                value `0` or a member whose value is `0`).
            UnrecognizedEnumValueError: If the metric is an `int` this client
                does not recognize. The raw value is available on the error's
                `value` attribute.
        """
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=DeprecationWarning)
            match self.metric:
                case 0 | Metric.UNSPECIFIED:
                    raise UnspecifiedEnumValueError(self, "metric")
                case Metric():
                    return self.metric
                case int():
                    raise UnrecognizedEnumValueError(self, "metric", self.metric)
                case unexpected:
                    assert_never(unexpected)

    def get_bounds_set(self) -> BoundsSet:
        """Return the bounds as a valid `BoundsSet`.

        This is the higher-level accessor for the lower-level
        [`bounds_set`][frequenz.client.common.metrics.MetricSample.bounds_set]
        field: it returns a valid `BoundsSet` or raises instead of exposing an
        `InvalidBoundsSet`.

        Returns:
            The bounds set when it is a valid `BoundsSet`.

        Raises:
            InvalidBoundsSetError: If the bounds set is an `InvalidBoundsSet`.
                The offending set is available on the error's `bounds_set`
                attribute.
        """
        match self.bounds_set:
            case BoundsSet() as bounds_set:
                return bounds_set
            case InvalidBoundsSet() as invalid:
                raise InvalidBoundsSetError(self, "bounds_set", invalid)
            case unexpected:
                assert_never(unexpected)
Attributes¤
bounds property ¤
bounds: list[Bounds]

The valid bounds that apply to the metric sample.

Deprecated

Use bounds_set instead. For backward compatibility this returns only the valid Bounds from bounds_set (dropping any malformed entries, as the old field did), but it returns the normalized, merged bounds rather than the raw list received on the wire.

RETURNS DESCRIPTION
list[Bounds]

The valid bounds in bounds_set.

bounds_set instance-attribute ¤

The bounds that apply to the metric sample.

These bounds adapt in real-time to reflect the operating conditions at the time of aggregation or derivation. They form a union: the value of the metric must be within at least one of them, and an empty BoundsSet means the metric is unbounded.

This is a BoundsSet for well-formed data, or an InvalidBoundsSet preserving the raw bounds when the wire carried any malformed entry, so callers must handle both.

Tip

Prefer MetricSample.get_bounds_set() to obtain a valid BoundsSet or a clear error.

In accordance with the passive sign convention, bounds that limit discharge would have negative numbers, while those limiting charge, such as for the State of Power (SoP) metric, would be positive. Hence bounds can have positive and negative values depending on the metric they represent.

connection class-attribute instance-attribute ¤
connection: MetricConnection | None = None

The specific source or connection from which the metric was sampled.

This will be present when the same Metric can be obtained from multiple sources or connections. Knowing the connection can help in certain control and monitoring applications.

In cases where the component has just one connection for a metric, then the connection is None.

Example

A hybrid inverter can have a DC string for a battery and another DC string for a PV array. The connection names could resemble, say, dc_battery_0 (category BATTERY) and dc_pv_0 (category PV). A metric like DC voltage can be obtained from both connections. For an application to determine the SoC of the battery using the battery voltage, which connection the voltage metric was sampled from is important.

metric instance-attribute ¤
metric: Metric | int

The metric that was sampled.

This is the lower-level, forward-compatible accessor: it may hold a known Metric member, the raw int 0 when the metric is unspecified, or any other raw int not yet known to this client. Prefer MetricSample.get_metric() to obtain a known member or a clear error.

sample_time instance-attribute ¤
sample_time: datetime

The moment when the metric was sampled.

value instance-attribute ¤

The value of the sampled metric.

Methods:¤
__init__ ¤
__init__(
    *,
    sample_time: datetime,
    metric: Metric | int,
    value: FloatInt | AggregatedMetricValue | None,
    bounds_set: BoundsSet | InvalidBoundsSet | None = None,
    bounds: list[Bounds] | None = None,
    connection: MetricConnection | None = None
) -> None

Initialize this metric sample.

PARAMETER DESCRIPTION
sample_time

The moment when the metric was sampled.

TYPE: datetime

metric

The metric that was sampled.

TYPE: Metric | int

value

The value of the sampled metric.

TYPE: FloatInt | AggregatedMetricValue | None

bounds_set

The bounds that apply to the metric sample.

TYPE: BoundsSet | InvalidBoundsSet | None DEFAULT: None

bounds

Deprecated alias that accepts a list of valid Bounds and stores them as a BoundsSet. Use bounds_set instead.

TYPE: list[Bounds] | None DEFAULT: None

connection

The source or connection the metric was sampled from.

TYPE: MetricConnection | None DEFAULT: None

RAISES DESCRIPTION
TypeError

If both bounds_set and the deprecated bounds are given, or if neither is given.

Source code in src/frequenz/client/common/metrics/_sample.py
def __init__(
    self,
    *,
    sample_time: datetime,
    metric: Metric | int,
    value: FloatInt | AggregatedMetricValue | None,
    bounds_set: BoundsSet | InvalidBoundsSet | None = None,
    bounds: list[Bounds] | None = None,
    connection: MetricConnection | None = None,
) -> None:
    """Initialize this metric sample.

    Args:
        sample_time: The moment when the metric was sampled.
        metric: The metric that was sampled.
        value: The value of the sampled metric.
        bounds_set: The bounds that apply to the metric sample.
        bounds: Deprecated alias that accepts a list of valid
            [`Bounds`][...Bounds] and stores them as a
            [`BoundsSet`][...BoundsSet]. Use `bounds_set` instead.
        connection: The source or connection the metric was sampled from.

    Raises:
        TypeError: If both `bounds_set` and the deprecated `bounds` are
            given, or if neither is given.
    """
    if bounds is not None and bounds_set is not None:
        raise TypeError(
            "`MetricSample` accepts either `bounds_set` or the deprecated "
            "`bounds`, not both."
        )
    if bounds is not None:
        warnings.warn(
            "The `bounds` argument is deprecated; use `bounds_set` instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        bounds_set = BoundsSet(bounds=tuple(bounds))
    if bounds_set is None:
        raise TypeError("`MetricSample` requires the `bounds_set` argument.")
    object.__setattr__(self, "sample_time", sample_time)
    object.__setattr__(self, "metric", metric)
    object.__setattr__(self, "value", value)
    object.__setattr__(self, "bounds_set", bounds_set)
    object.__setattr__(self, "connection", connection)
__str__ ¤
__str__() -> str

Return a compact string representation of this sample.

Source code in src/frequenz/client/common/metrics/_sample.py
def __str__(self) -> str:
    """Return a compact string representation of this sample."""
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", category=DeprecationWarning)
        match self.metric:
            case 0 | Metric.UNSPECIFIED:
                metric = "<invalid:0>"
            case Metric() as known:
                metric = known.name
            case int() as unknown:
                metric = str(unknown)
            case unexpected:
                assert_never(unexpected)
    sample = f"{metric}={self.value}"
    if self.connection is not None:
        sample = f"{sample}@{self.connection}"
    return sample
as_single_value ¤
as_single_value(
    *, aggregation_method: AggregationMethod = AVG
) -> FloatInt | None

Return the value of this sample as a single value.

If value is a number, it is returned as is. If value is an AggregatedMetricValue, the value is aggregated using the provided aggregation_method.

PARAMETER DESCRIPTION
aggregation_method

The method to use to aggregate the value when value is an AggregatedMetricValue.

TYPE: AggregationMethod DEFAULT: AVG

RETURNS DESCRIPTION
FloatInt | None

The value of the sample as a single value, or None if the value is None.

Source code in src/frequenz/client/common/metrics/_sample.py
def as_single_value(
    self, *, aggregation_method: AggregationMethod = AggregationMethod.AVG
) -> FloatInt | None:
    """Return the value of this sample as a single value.

    If [`value`][..value] is a number, it is returned as is. If `value`
    is an [`AggregatedMetricValue`][...AggregatedMetricValue], the value is
    aggregated using the provided `aggregation_method`.

    Args:
        aggregation_method: The method to use to aggregate the value when `value`
            is an [`AggregatedMetricValue`][...AggregatedMetricValue].

    Returns:
        The value of the sample as a single value, or `None` if the value is `None`.
    """
    match self.value:
        case float() | int():
            return self.value
        case AggregatedMetricValue():
            match aggregation_method:
                case AggregationMethod.AVG:
                    return self.value.avg
                case AggregationMethod.MIN:
                    return self.value.min
                case AggregationMethod.MAX:
                    return self.value.max
                case unexpected:
                    assert_never(unexpected)
        case None:
            return None
        case unexpected:
            assert_never(unexpected)
get_bounds_set ¤
get_bounds_set() -> BoundsSet

Return the bounds as a valid BoundsSet.

This is the higher-level accessor for the lower-level bounds_set field: it returns a valid BoundsSet or raises instead of exposing an InvalidBoundsSet.

RETURNS DESCRIPTION
BoundsSet

The bounds set when it is a valid BoundsSet.

RAISES DESCRIPTION
InvalidBoundsSetError

If the bounds set is an InvalidBoundsSet. The offending set is available on the error's bounds_set attribute.

Source code in src/frequenz/client/common/metrics/_sample.py
def get_bounds_set(self) -> BoundsSet:
    """Return the bounds as a valid `BoundsSet`.

    This is the higher-level accessor for the lower-level
    [`bounds_set`][frequenz.client.common.metrics.MetricSample.bounds_set]
    field: it returns a valid `BoundsSet` or raises instead of exposing an
    `InvalidBoundsSet`.

    Returns:
        The bounds set when it is a valid `BoundsSet`.

    Raises:
        InvalidBoundsSetError: If the bounds set is an `InvalidBoundsSet`.
            The offending set is available on the error's `bounds_set`
            attribute.
    """
    match self.bounds_set:
        case BoundsSet() as bounds_set:
            return bounds_set
        case InvalidBoundsSet() as invalid:
            raise InvalidBoundsSetError(self, "bounds_set", invalid)
        case unexpected:
            assert_never(unexpected)
get_metric ¤
get_metric() -> Metric

Return the sampled metric as a known enum member.

This is the higher-level accessor for the lower-level metric field: it returns a known member or raises instead of exposing the raw sentinel 0 or an unknown int.

RETURNS DESCRIPTION
Metric

The metric when it is a known Metric member.

RAISES DESCRIPTION
UnspecifiedEnumValueError

If the metric is unspecified (the raw value 0 or a member whose value is 0).

UnrecognizedEnumValueError

If the metric is an int this client does not recognize. The raw value is available on the error's value attribute.

Source code in src/frequenz/client/common/metrics/_sample.py
def get_metric(self) -> Metric:
    """Return the sampled metric as a known enum member.

    This is the higher-level accessor for the lower-level
    [`metric`][frequenz.client.common.metrics.MetricSample.metric] field: it
    returns a known member or raises instead of exposing the raw sentinel
    `0` or an unknown `int`.

    Returns:
        The metric when it is a known `Metric` member.

    Raises:
        UnspecifiedEnumValueError: If the metric is unspecified (the raw
            value `0` or a member whose value is `0`).
        UnrecognizedEnumValueError: If the metric is an `int` this client
            does not recognize. The raw value is available on the error's
            `value` attribute.
    """
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", category=DeprecationWarning)
        match self.metric:
            case 0 | Metric.UNSPECIFIED:
                raise UnspecifiedEnumValueError(self, "metric")
            case Metric():
                return self.metric
            case int():
                raise UnrecognizedEnumValueError(self, "metric", self.metric)
            case unexpected:
                assert_never(unexpected)