Skip to content

Commit c004086

Browse files
committed
MAYBE: Bring back power manager arguments as deprecated
This is dumb, nobody uses the power manager actor directly.
1 parent ad2d2b2 commit c004086

File tree

2 files changed

+299
-2
lines changed

2 files changed

+299
-2
lines changed

src/frequenz/sdk/microgrid/_power_managing/_power_managing_actor.py

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@
88
import asyncio
99
import logging
1010
import sys
11+
import warnings
1112
from datetime import datetime, timedelta, timezone
1213
from typing import assert_never
1314

1415
from frequenz.channels import Receiver, Sender, select, selected_from
1516
from frequenz.channels.timer import SkipMissedAndDrift, Timer
1617
from frequenz.client.common.microgrid.components import ComponentId
17-
from frequenz.client.microgrid.component import Battery, EvCharger, SolarInverter
18+
from frequenz.client.microgrid.component import (
19+
Battery,
20+
ComponentCategory,
21+
EvCharger,
22+
InverterType,
23+
SolarInverter,
24+
)
1825
from typing_extensions import override
1926

2027
from ..._internal._asyncio import run_forever
@@ -36,6 +43,77 @@
3643
_logger = logging.getLogger(__name__)
3744

3845

46+
def _convert_legacy_params_to_component_class(
47+
component_category: ComponentCategory,
48+
component_type: InverterType | None,
49+
) -> type[Battery | EvCharger | SolarInverter]:
50+
"""Convert legacy parameters to component_class.
51+
52+
Args:
53+
component_category: The legacy component category.
54+
component_type: The legacy component type (used for INVERTER category).
55+
56+
Returns:
57+
The corresponding component class.
58+
59+
Raises:
60+
ValueError: If the combination is not supported.
61+
"""
62+
match component_category:
63+
case ComponentCategory.BATTERY:
64+
return Battery
65+
case ComponentCategory.EV_CHARGER:
66+
return EvCharger
67+
case ComponentCategory.INVERTER:
68+
if component_type == InverterType.SOLAR:
69+
return SolarInverter
70+
raise ValueError(
71+
f"Unsupported component_type for INVERTER: {component_type}. "
72+
"Only InverterType.SOLAR is supported."
73+
)
74+
case _:
75+
raise ValueError(f"Unsupported component_category: {component_category}")
76+
77+
78+
def _get_component_class_or_warn(
79+
component_class: type[Battery | EvCharger | SolarInverter] | None,
80+
component_category: ComponentCategory | None,
81+
component_type: InverterType | None,
82+
) -> type[Battery | EvCharger | SolarInverter]:
83+
"""Get the component class, converting deprecated parameters if needed."""
84+
if component_category is not None or component_type is not None:
85+
if component_class is not None:
86+
raise ValueError(
87+
"Cannot specify both 'component_class' and deprecated parameters "
88+
"'component_category'/'component_type'. Use only 'component_class'."
89+
)
90+
91+
if component_category is None:
92+
raise ValueError(
93+
"If 'component_type' is specified, 'component_category' must also be specified. "
94+
"Both arguments are deprecated though, use 'component_class' instead."
95+
)
96+
97+
warnings.warn(
98+
"Parameters 'component_category' and 'component_type' are deprecated "
99+
"and will be removed in a future version. "
100+
"Use 'component_class' instead (e.g., component_class=Battery).",
101+
DeprecationWarning,
102+
stacklevel=3,
103+
)
104+
105+
component_class = _convert_legacy_params_to_component_class(
106+
component_category, component_type
107+
)
108+
elif component_class is None:
109+
raise ValueError(
110+
"Must specify either 'component_class' or deprecated parameters "
111+
"'component_category'/'component_type'."
112+
)
113+
114+
return component_class
115+
116+
39117
class PowerManagingActor(Actor):
40118
"""The power manager."""
41119

@@ -49,7 +127,9 @@ def __init__( # pylint: disable=too-many-arguments
49127
channel_registry: ChannelRegistry,
50128
algorithm: Algorithm,
51129
default_power: DefaultPower,
52-
component_class: type[Battery | EvCharger | SolarInverter],
130+
component_class: type[Battery | EvCharger | SolarInverter] | None = None,
131+
component_category: ComponentCategory | None = None,
132+
component_type: InverterType | None = None,
53133
):
54134
"""Create a new instance of the power manager.
55135
@@ -64,7 +144,23 @@ def __init__( # pylint: disable=too-many-arguments
64144
algorithm: The power management algorithm to use.
65145
default_power: The default power to use for the components.
66146
component_class: The class of component this instance is going to support.
147+
component_category: **DEPRECATED**. Use `component_class` instead.
148+
The category of the component this power manager instance is going to
149+
support.
150+
component_type: **DEPRECATED**. Use `component_class` instead.
151+
The type of the component of the given category that this actor is
152+
responsible for. This is used only when the component category is not
153+
enough to uniquely identify the component. For example, when the
154+
category is `ComponentCategory.INVERTER`, the type is needed to identify
155+
the inverter as a solar inverter. This can be `None` when the component
156+
category is enough to uniquely identify the component.
67157
"""
158+
component_class = _get_component_class_or_warn(
159+
component_class,
160+
component_category,
161+
component_type,
162+
)
163+
68164
self._default_power = default_power
69165
self._component_class = component_class
70166
self._bounds_subscription_receiver = bounds_subscription_receiver
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for PowerManagingActor deprecated parameter handling."""
5+
6+
from __future__ import annotations
7+
8+
import warnings
9+
from typing import Any
10+
11+
import pytest
12+
from frequenz.channels import Broadcast
13+
from frequenz.client.microgrid.component import (
14+
Battery,
15+
ComponentCategory,
16+
EvCharger,
17+
InverterType,
18+
SolarInverter,
19+
)
20+
21+
from frequenz.sdk._internal._channels import ChannelRegistry
22+
from frequenz.sdk.microgrid._power_managing._base_classes import (
23+
Algorithm,
24+
DefaultPower,
25+
Proposal,
26+
ReportRequest,
27+
)
28+
from frequenz.sdk.microgrid._power_managing._power_managing_actor import (
29+
PowerManagingActor,
30+
_convert_legacy_params_to_component_class,
31+
)
32+
33+
34+
class TestLegacyParamsConversion:
35+
"""Test the legacy parameter conversion helper function."""
36+
37+
def test_convert_battery(self) -> None:
38+
"""Test conversion of BATTERY category."""
39+
result = _convert_legacy_params_to_component_class(
40+
ComponentCategory.BATTERY, None
41+
)
42+
assert result == Battery
43+
44+
def test_convert_ev_charger(self) -> None:
45+
"""Test conversion of EV_CHARGER category."""
46+
result = _convert_legacy_params_to_component_class(
47+
ComponentCategory.EV_CHARGER, None
48+
)
49+
assert result == EvCharger
50+
51+
def test_convert_solar_inverter(self) -> None:
52+
"""Test conversion of INVERTER + SOLAR category/type."""
53+
result = _convert_legacy_params_to_component_class(
54+
ComponentCategory.INVERTER, InverterType.SOLAR
55+
)
56+
assert result == SolarInverter
57+
58+
def test_unsupported_inverter_type(self) -> None:
59+
"""Test that unsupported inverter types raise ValueError."""
60+
with pytest.raises(
61+
ValueError, match="Unsupported component_type for INVERTER.*SOLAR"
62+
):
63+
_convert_legacy_params_to_component_class(
64+
ComponentCategory.INVERTER, InverterType.BATTERY
65+
)
66+
67+
def test_unsupported_inverter_no_type(self) -> None:
68+
"""Test that INVERTER without type raises ValueError."""
69+
with pytest.raises(
70+
ValueError, match="Unsupported component_type for INVERTER.*SOLAR"
71+
):
72+
_convert_legacy_params_to_component_class(ComponentCategory.INVERTER, None)
73+
74+
def test_unsupported_category(self) -> None:
75+
"""Test that unsupported categories raise ValueError."""
76+
with pytest.raises(ValueError, match="Unsupported component_category"):
77+
_convert_legacy_params_to_component_class(ComponentCategory.METER, None)
78+
79+
80+
class TestPowerManagingActorDeprecation:
81+
"""Test PowerManagingActor deprecated parameter handling."""
82+
83+
@staticmethod
84+
def _create_minimal_actor_params() -> dict[str, Any]:
85+
"""Create minimal parameters for PowerManagingActor initialization."""
86+
proposals_channel: Broadcast[Proposal] = Broadcast[Any](name="proposals")
87+
bounds_channel: Broadcast[ReportRequest] = Broadcast(name="bounds")
88+
requests_channel: Broadcast[Any] = Broadcast(name="requests")
89+
results_channel: Broadcast[Any] = Broadcast(name="results")
90+
91+
return {
92+
"proposals_receiver": proposals_channel.new_receiver(),
93+
"bounds_subscription_receiver": bounds_channel.new_receiver(),
94+
"power_distributing_requests_sender": requests_channel.new_sender(),
95+
"power_distributing_results_receiver": results_channel.new_receiver(),
96+
"channel_registry": ChannelRegistry(name="test"),
97+
"algorithm": Algorithm.MATRYOSHKA,
98+
"default_power": DefaultPower.ZERO,
99+
}
100+
101+
@staticmethod
102+
def assert_class(actor: PowerManagingActor, expected_class: type) -> None:
103+
"""Assert that the actor's component class matches the expected class."""
104+
assert (
105+
actor._component_class == expected_class # pylint: disable=protected-access
106+
)
107+
108+
def test_new_param_works_without_warning(self) -> None:
109+
"""Test that using component_class works without warnings."""
110+
params = self._create_minimal_actor_params()
111+
params["component_class"] = Battery
112+
113+
# Should not raise any warnings - use simplefilter("error") to catch them
114+
with warnings.catch_warnings():
115+
warnings.simplefilter("error")
116+
actor = PowerManagingActor(**params)
117+
118+
self.assert_class(actor, Battery)
119+
120+
def test_deprecated_battery_param_with_warning(self) -> None:
121+
"""Test that deprecated parameters work but emit warning for BATTERY."""
122+
params = self._create_minimal_actor_params()
123+
params["component_category"] = ComponentCategory.BATTERY
124+
125+
with pytest.deprecated_call():
126+
actor = PowerManagingActor(**params)
127+
128+
self.assert_class(actor, Battery)
129+
130+
def test_deprecated_ev_charger_param_with_warning(self) -> None:
131+
"""Test that deprecated parameters work but emit warning for EV_CHARGER."""
132+
params = self._create_minimal_actor_params()
133+
params["component_category"] = ComponentCategory.EV_CHARGER
134+
135+
with pytest.deprecated_call():
136+
actor = PowerManagingActor(**params)
137+
138+
self.assert_class(actor, EvCharger)
139+
140+
def test_deprecated_solar_inverter_param_with_warning(self) -> None:
141+
"""Test that deprecated parameters work but emit warning for SOLAR inverter."""
142+
params = self._create_minimal_actor_params()
143+
params["component_category"] = ComponentCategory.INVERTER
144+
params["component_type"] = InverterType.SOLAR
145+
146+
with pytest.deprecated_call():
147+
actor = PowerManagingActor(**params)
148+
149+
self.assert_class(actor, SolarInverter)
150+
151+
def test_mixing_old_and_new_params_raises_error(self) -> None:
152+
"""Test that mixing old and new parameters raises ValueError."""
153+
params = self._create_minimal_actor_params()
154+
params["component_class"] = Battery
155+
params["component_category"] = ComponentCategory.BATTERY
156+
157+
with pytest.raises(
158+
ValueError,
159+
match="Cannot specify both 'component_class' and deprecated parameters",
160+
):
161+
PowerManagingActor(**params)
162+
163+
def test_no_params_raises_error(self) -> None:
164+
"""Test that providing no component parameters raises ValueError."""
165+
params = self._create_minimal_actor_params()
166+
167+
with pytest.raises(
168+
ValueError,
169+
match="Must specify either 'component_class' or deprecated parameters",
170+
):
171+
PowerManagingActor(**params)
172+
173+
def test_component_type_without_category_raises_error(self) -> None:
174+
"""Test that component_type without component_category raises ValueError."""
175+
params = self._create_minimal_actor_params()
176+
params["component_type"] = InverterType.SOLAR
177+
178+
with pytest.raises(
179+
ValueError,
180+
match="If 'component_type' is specified, 'component_category' must also be specified",
181+
):
182+
PowerManagingActor(**params)
183+
184+
def test_unsupported_inverter_type_raises_error(self) -> None:
185+
"""Test that unsupported inverter type raises ValueError."""
186+
params = self._create_minimal_actor_params()
187+
params["component_category"] = ComponentCategory.INVERTER
188+
params["component_type"] = InverterType.BATTERY
189+
190+
with pytest.raises(
191+
ValueError, match="Unsupported component_type for INVERTER.*SOLAR"
192+
):
193+
PowerManagingActor(**params)
194+
195+
def test_unsupported_category_raises_error(self) -> None:
196+
"""Test that unsupported component category raises ValueError."""
197+
params = self._create_minimal_actor_params()
198+
params["component_category"] = ComponentCategory.METER
199+
200+
with pytest.raises(ValueError, match="Unsupported component_category"):
201+
PowerManagingActor(**params)

0 commit comments

Comments
 (0)