Skip to content

Enum-or-int fields¤

Some fields are an enum member or an integer, such as MetricSample.metric. The integer keeps newer server values available to an older client instead of failing when the client does not recognize them.

Prefer a safe accessor when one is available. For example, MetricSample.get_metric() returns a known Metric member. It raises UnspecifiedEnumValueError for the raw value 0 and UnrecognizedEnumValueError for another unrecognized integer. See safe accessors to handle those exceptions.

from datetime import datetime, timezone

from frequenz.client.common import (
    UnrecognizedEnumValueError,
    UnspecifiedEnumValueError,
)
from frequenz.client.common.metrics import BoundsSet, Metric, MetricSample


def sample_with(metric: Metric | int) -> MetricSample:
    return MetricSample(
        sample_time=datetime(2026, 1, 1, tzinfo=timezone.utc),
        metric=metric,
        value=42.0,
        bounds_set=BoundsSet(),
    )


for sample in (
    sample_with(Metric.AC_POWER_ACTIVE),
    sample_with(0),
    sample_with(999),
):
    try:
        print(sample.get_metric().name)
    except UnspecifiedEnumValueError:
        print("unspecified")
    except UnrecognizedEnumValueError as error:
        print(f"unrecognized: {error.value}")

When you need the lower-level field, dispatch with match. A known member matches the enum, raw 0 means unspecified, and any other integer is an unrecognized value. Do not use isinstance() or look for an UNSPECIFIED member.

You only read these values. Treat the raw integer 0 as unspecified rather than setting an enum's UNSPECIFIED member.

from datetime import datetime, timezone
from typing import assert_never

from frequenz.client.common.metrics import BoundsSet, Metric, MetricSample


def describe(sample: MetricSample) -> str:
    match sample.metric:
        case Metric() as metric:
            return f"known: {metric.name}"
        case 0:
            return "unspecified"
        case int() as value:
            return f"unrecognized: {value}"
        case unexpected:
            assert_never(unexpected)


samples = (
    MetricSample(
        sample_time=datetime(2026, 1, 1, tzinfo=timezone.utc),
        metric=Metric.AC_POWER_ACTIVE,
        value=42.0,
        bounds_set=BoundsSet(),
    ),
    MetricSample(
        sample_time=datetime(2026, 1, 1, tzinfo=timezone.utc),
        metric=0,
        value=42.0,
        bounds_set=BoundsSet(),
    ),
    MetricSample(
        sample_time=datetime(2026, 1, 1, tzinfo=timezone.utc),
        metric=999,
        value=42.0,
        bounds_set=BoundsSet(),
    ),
)

for sample in samples:
    print(describe(sample))

An unrecognized integer is not invalid data. It is a value the server knows that this client version does not yet recognize. Invalid values are a separate topic; see Validity in the type.

Treat the numbers as opaque¤

Work with members, not numbers. Compare against the member itself, as in sample.metric is Metric.AC_POWER_ACTIVE, and let the accessors and match arms above do the rest. A number is how the protocol identifies a value. It is not how your code should identify it.

Reach for .value only as a last resort, when you must hand a raw protocol number to something that speaks the protocol itself. The mapping is direct: a member name is the protocol name without its fixed prefix, and a member value is the protocol number.

from frequenz.client.common.metrics import Metric

# A last resort: Metric.AC_POWER_ACTIVE is METRIC_AC_POWER_ACTIVE in the protocol.
assert Metric.AC_POWER_ACTIVE.value == 26

Those numbers belong to a protocol version, and only that version gives them a meaning. The same number can name different things in two versions. When a new protocol version gives an existing name a different number, this library follows the newest version it supports. A member's number therefore changes only in a release that adds support for a new protocol version. That is a breaking change and the release notes call it out, but the member name stays the same.

A raw integer needs the same care, and more. Read it as "the server sent a value this client does not know", not as a stable identifier. Do not persist it or pass it to another system as if it were version-independent, and do not compare it against a member number to guess which value it is. To learn what one means, look it up in the frequenz-api-common definition for the version your client speaks, or upgrade this library and your client so the value resolves to a member.