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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
from ._category import ElectricalComponentCategory
from ._diagnostic_code import ElectricalComponentDiagnosticCode
from ._electrical_component import ElectricalComponent
from ._ev_charger import (
AcEvCharger,
DcEvCharger,
EvCharger,
EvChargerType,
EvChargerTypes,
HybridEvCharger,
UnrecognizedEvCharger,
UnspecifiedEvCharger,
)
from ._ids import ElectricalComponentId
from ._inverter import (
BatteryInverter,
Expand All @@ -35,15 +45,21 @@
from ._state_code import ElectricalComponentStateCode

__all__ = [
"AcEvCharger",
"Battery",
"BatteryInverter",
"BatteryType",
"BatteryTypes",
"DcEvCharger",
"ElectricalComponent",
"ElectricalComponentCategory",
"ElectricalComponentDiagnosticCode",
"ElectricalComponentId",
"ElectricalComponentStateCode",
"EvCharger",
"EvChargerType",
"EvChargerTypes",
"HybridEvCharger",
"HybridInverter",
"Inverter",
"InverterType",
Expand All @@ -55,8 +71,10 @@
"SolarInverter",
"UnrecognizedBattery",
"UnrecognizedComponent",
"UnrecognizedEvCharger",
"UnrecognizedInverter",
"UnspecifiedBattery",
"UnspecifiedComponent",
"UnspecifiedEvCharger",
"UnspecifiedInverter",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# License: MIT
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH

"""Electric vehicle (EV) charger electrical component."""

import dataclasses
import enum
from typing import Any, Literal, Self, TypeAlias

from frequenz.api.common.v1alpha8.microgrid.electrical_components import (
electrical_components_pb2,
)

from ._category import ElectricalComponentCategory
from ._electrical_component import ElectricalComponent


@enum.unique
class EvChargerType(enum.Enum):
"""The known types of electric vehicle (EV) chargers."""

UNSPECIFIED = electrical_components_pb2.EV_CHARGER_TYPE_UNSPECIFIED
"""The type of the EV charger is unspecified."""

AC = electrical_components_pb2.EV_CHARGER_TYPE_AC
"""The EV charging station supports AC charging only."""

DC = electrical_components_pb2.EV_CHARGER_TYPE_DC
"""The EV charging station supports DC charging only."""

HYBRID = electrical_components_pb2.EV_CHARGER_TYPE_HYBRID
"""The EV charging station supports both AC and DC."""


@dataclasses.dataclass(frozen=True, kw_only=True)
class EvCharger(ElectricalComponent):
"""An abstract EV charger electrical component."""

category: Literal[ElectricalComponentCategory.EV_CHARGER] = (
ElectricalComponentCategory.EV_CHARGER
)
"""The category of this electrical component.

Note:
This should not be used normally, you should test if an electrical component
[`isinstance`][] of a concrete EV charger class instead.

It is only provided for using with a newer version of the API where the client
doesn't know about a new category yet (i.e. for use with
[`UnrecognizedComponent`][...UnrecognizedComponent]) and in case some low
level code needs to know the category of an electrical component.
"""

type: EvChargerType | int
"""The type of this EV charger.

Note:
This should not be used normally, you should test if a EV charger
[`isinstance`][] of a concrete component class instead.

It is only provided for using with a newer version of the API where the client
doesn't know about the new EV charger type yet (i.e. for use with
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
"""

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


@dataclasses.dataclass(frozen=True, kw_only=True)
class UnspecifiedEvCharger(EvCharger):
"""An EV charger of an unspecified type."""

type: Literal[EvChargerType.UNSPECIFIED] = EvChargerType.UNSPECIFIED
"""The type of this EV charger.

Note:
This should not be used normally, you should test if a EV charger
[`isinstance`][] of a concrete component class instead.

It is only provided for using with a newer version of the API where the client
doesn't know about the new EV charger type yet (i.e. for use with
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
"""


@dataclasses.dataclass(frozen=True, kw_only=True)
class AcEvCharger(EvCharger):
"""An EV charger that supports AC charging only."""

type: Literal[EvChargerType.AC] = EvChargerType.AC
"""The type of this EV charger.

Note:
This should not be used normally, you should test if a EV charger
[`isinstance`][] of a concrete component class instead.

It is only provided for using with a newer version of the API where the client
doesn't know about the new EV charger type yet (i.e. for use with
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
"""


@dataclasses.dataclass(frozen=True, kw_only=True)
class DcEvCharger(EvCharger):
"""An EV charger that supports DC charging only."""

type: Literal[EvChargerType.DC] = EvChargerType.DC
"""The type of this EV charger.

Note:
This should not be used normally, you should test if a EV charger
[`isinstance`][] of a concrete component class instead.

It is only provided for using with a newer version of the API where the client
doesn't know about the new EV charger type yet (i.e. for use with
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
"""


@dataclasses.dataclass(frozen=True, kw_only=True)
class HybridEvCharger(EvCharger):
"""An EV charger that supports both AC and DC charging."""

type: Literal[EvChargerType.HYBRID] = EvChargerType.HYBRID
"""The type of this EV charger.

Note:
This should not be used normally, you should test if a EV charger
[`isinstance`][] of a concrete component class instead.

It is only provided for using with a newer version of the API where the client
doesn't know about the new EV charger type yet (i.e. for use with
[`UnrecognizedEvCharger`][...UnrecognizedEvCharger]).
"""


@dataclasses.dataclass(frozen=True, kw_only=True)
class UnrecognizedEvCharger(EvCharger):
"""An EV charger of an unrecognized type."""

type: int
"""The unrecognized type of this EV charger."""


EvChargerTypes: TypeAlias = (
UnspecifiedEvCharger
| AcEvCharger
| DcEvCharger
| HybridEvCharger
| UnrecognizedEvCharger
)
"""All possible EV charger types."""
120 changes: 120 additions & 0 deletions tests/microgrid/electrical_components/test_ev_charger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# License: MIT
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH

"""Tests for EV charger components."""

import dataclasses

import pytest

from frequenz.client.common.microgrid import MicrogridId
from frequenz.client.common.microgrid.electrical_components import (
AcEvCharger,
DcEvCharger,
ElectricalComponentCategory,
ElectricalComponentId,
EvCharger,
EvChargerType,
HybridEvCharger,
UnrecognizedEvCharger,
UnspecifiedEvCharger,
)


@dataclasses.dataclass(frozen=True, kw_only=True)
class EvChargerTestCase:
"""Test case for EV charger components."""

cls: type[UnspecifiedEvCharger | AcEvCharger | DcEvCharger | HybridEvCharger]
expected_type: EvChargerType
name: str


@pytest.fixture
def component_id() -> ElectricalComponentId:
"""Provide a test component ID."""
return ElectricalComponentId(42)


@pytest.fixture
def microgrid_id() -> MicrogridId:
"""Provide a test microgrid ID."""
return MicrogridId(1)


def test_abstract_ev_charger_cannot_be_instantiated(
component_id: ElectricalComponentId, microgrid_id: MicrogridId
) -> None:
"""Test that EvCharger base class cannot be instantiated."""
with pytest.raises(TypeError, match="Cannot instantiate EvCharger directly"):
EvCharger(
id=component_id,
microgrid_id=microgrid_id,
name="test_charger",
manufacturer="test_manufacturer",
model_name="test_model",
type=EvChargerType.AC,
)


@pytest.mark.parametrize(
"case",
[
EvChargerTestCase(
cls=UnspecifiedEvCharger,
expected_type=EvChargerType.UNSPECIFIED,
name="unspecified",
),
EvChargerTestCase(cls=AcEvCharger, expected_type=EvChargerType.AC, name="ac"),
EvChargerTestCase(cls=DcEvCharger, expected_type=EvChargerType.DC, name="dc"),
EvChargerTestCase(
cls=HybridEvCharger,
expected_type=EvChargerType.HYBRID,
name="hybrid",
),
],
ids=lambda case: case.name,
)
def test_recognized_ev_charger_types( # Renamed from test_ev_charger_types
case: EvChargerTestCase,
component_id: ElectricalComponentId,
microgrid_id: MicrogridId,
) -> None:
"""Test initialization and properties of different recognized EV charger types."""
charger = case.cls(
id=component_id,
microgrid_id=microgrid_id,
name=case.name,
manufacturer="test_manufacturer",
model_name="test_model",
)

assert charger.id == component_id
assert charger.microgrid_id == microgrid_id
assert charger.name == case.name
assert charger.manufacturer == "test_manufacturer"
assert charger.model_name == "test_model"
assert charger.category == ElectricalComponentCategory.EV_CHARGER
assert charger.type == case.expected_type


def test_unrecognized_ev_charger_type(
component_id: ElectricalComponentId, microgrid_id: MicrogridId
) -> None:
"""Test initialization and properties of unrecognized EV charger type."""
charger = UnrecognizedEvCharger(
id=component_id,
microgrid_id=microgrid_id,
name="unrecognized_charger",
manufacturer="test_manufacturer",
model_name="test_model",
type=999, # type is passed here for UnrecognizedEvCharger
)

assert charger.id == component_id
assert charger.microgrid_id == microgrid_id
assert charger.name == "unrecognized_charger"
assert charger.manufacturer == "test_manufacturer"
assert charger.model_name == "test_model"
assert charger.category == ElectricalComponentCategory.EV_CHARGER
assert charger.type == 999
Loading