Skip to content

Commit 0e4daca

Browse files
committed
Remove streaming interface of StateTracker
... because it is no longer getting exposed to users directly, but instead through a hybrid method that will be introduced in the next commit. Signed-off-by: Sahas Subramanian <[email protected]>
1 parent acf83b0 commit 0e4daca

File tree

2 files changed

+28
-100
lines changed

2 files changed

+28
-100
lines changed

src/frequenz/sdk/timeseries/ev_charger_pool/_state_tracker.py

Lines changed: 19 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
from __future__ import annotations
77

88
import asyncio
9-
from collections.abc import Iterator
10-
from dataclasses import dataclass
119
from enum import Enum
1210
from typing import Optional
1311

14-
from frequenz.channels import Broadcast, Receiver
12+
from frequenz.channels import Receiver
1513
from frequenz.channels.util import Merge
1614

1715
from frequenz.sdk import microgrid
@@ -62,44 +60,6 @@ def from_ev_charger_data(cls, data: EVChargerData) -> EVChargerState:
6260
return EVChargerState.IDLE
6361

6462

65-
@dataclass(frozen=True)
66-
class EVChargerPoolStates:
67-
"""States of all EV Chargers in the pool."""
68-
69-
_states: dict[int, EVChargerState]
70-
_changed_component: Optional[int] = None
71-
72-
def __iter__(self) -> Iterator[tuple[int, EVChargerState]]:
73-
"""Iterate over states of all EV Chargers.
74-
75-
Returns:
76-
An iterator over all EV Charger states.
77-
"""
78-
return iter(self._states.items())
79-
80-
def latest_change(self) -> Optional[tuple[int, EVChargerState]]:
81-
"""Return the most recent EV Charger state change.
82-
83-
The first `EVChargerPoolStates` instance created by a `StateTracker` will just
84-
be a representation of the states of all EV Chargers. At that point, the most
85-
recent change in state of an ev charger will be unknown, so this function will
86-
return `None`.
87-
88-
Returns:
89-
None, when the most recent change is unknown. Otherwise, a tuple with
90-
the component ID of an EV Charger that just had a state change, and its
91-
new state.
92-
"""
93-
if self._changed_component is None:
94-
return None
95-
return (
96-
self._changed_component,
97-
self._states.setdefault(
98-
self._changed_component, EVChargerState.UNSPECIFIED
99-
),
100-
)
101-
102-
10363
class StateTracker:
10464
"""A class for keeping track of the states of all EV Chargers in a pool."""
10565

@@ -110,74 +70,50 @@ def __init__(self, component_ids: set[int]) -> None:
11070
component_ids: EV Charger component ids to track the states of.
11171
"""
11272
self._component_ids = component_ids
113-
self._channel = Broadcast[EVChargerPoolStates](
114-
"EVCharger States", resend_latest=True
115-
)
116-
self._task: Optional[asyncio.Task[None]] = None
73+
self._task: asyncio.Task[None] = asyncio.create_task(self._run())
11774
self._merged_stream: Optional[Merge[EVChargerData]] = None
118-
self._states: dict[int, EVChargerState] = {}
11975

120-
def _get(self) -> EVChargerPoolStates:
121-
"""Get a representation of the current states of all EV Chargers.
76+
# Initialize all components to the `MISSING` state. This will change as data
77+
# starts arriving from the individual components.
78+
self._states: dict[int, EVChargerState] = {
79+
component_id: EVChargerState.MISSING for component_id in component_ids
80+
}
81+
82+
def get(self, component_id: int) -> EVChargerState:
83+
"""Return the current state of the EV Charger with the given component ID.
84+
85+
Args:
86+
component_id: id of the EV Charger whose state is being fetched.
12287
12388
Returns:
124-
An `EVChargerPoolStates` instance.
89+
An `EVChargerState` value corresponding to the given component id.
12590
"""
126-
return EVChargerPoolStates(self._states)
91+
return self._states[component_id]
12792

12893
def _update(
12994
self,
13095
data: EVChargerData,
131-
) -> Optional[EVChargerPoolStates]:
96+
) -> None:
13297
"""Update the state of an EV Charger, from a new data point.
13398
13499
Args:
135100
data: component data from the microgrid, for an EV Charger in the pool.
136-
137-
Returns:
138-
A new `EVChargerPoolStates` instance representing all the EV Chargers in
139-
the pool, in case there has been a state change for any of the EV
140-
Chargers, or `None` otherwise.
141101
"""
142102
evc_id = data.component_id
143103
new_state = EVChargerState.from_ev_charger_data(data)
144-
if evc_id not in self._states or self._states[evc_id] != new_state:
145-
self._states[evc_id] = new_state
146-
return EVChargerPoolStates(self._states, evc_id)
147-
return None
104+
self._states[evc_id] = new_state
148105

149106
async def _run(self) -> None:
150107
api_client = microgrid.connection_manager.get().api_client
151108
streams: list[Receiver[EVChargerData]] = await asyncio.gather(
152109
*[api_client.ev_charger_data(cid) for cid in self._component_ids]
153110
)
154-
155-
# Start with the `MISSING` state for all components. This will change as data
156-
# starts arriving from the individual components.
157-
self._states = {
158-
component_id: EVChargerState.MISSING for component_id in self._component_ids
159-
}
160111
self._merged_stream = Merge(*streams)
161-
sender = self._channel.new_sender()
162-
await sender.send(self._get())
163112
async for data in self._merged_stream:
164-
if updated_states := self._update(data):
165-
await sender.send(updated_states)
166-
167-
def new_receiver(self) -> Receiver[EVChargerPoolStates]:
168-
"""Return a receiver that streams ev charger states.
169-
170-
Returns:
171-
A receiver that streams the states of all EV Chargers in the pool, every
172-
time the states of any of them change.
173-
"""
174-
if self._task is None or self._task.done():
175-
self._task = asyncio.create_task(self._run())
176-
return self._channel.new_receiver()
113+
self._update(data)
177114

178115
async def stop(self) -> None:
179116
"""Stop the status tracker."""
180-
if self._task:
181-
await cancel_and_await(self._task)
117+
await cancel_and_await(self._task)
182118
if self._merged_stream:
183119
await self._merged_stream.stop()

tests/timeseries/test_ev_charger_pool.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import asyncio
99
from math import isclose
10-
from typing import Optional
1110

1211
from pytest_mock import MockerFixture
1312

@@ -18,7 +17,6 @@
1817
EVChargerComponentState,
1918
)
2019
from frequenz.sdk.timeseries.ev_charger_pool._state_tracker import (
21-
EVChargerPoolStates,
2220
EVChargerState,
2321
StateTracker,
2422
)
@@ -40,45 +38,39 @@ async def test_state_updates(self, mocker: MockerFixture) -> None:
4038
await mockgrid.start(mocker)
4139

4240
state_tracker = StateTracker(set(mockgrid.evc_ids))
43-
states = state_tracker.new_receiver()
4441

45-
async def check_next_state(
42+
async def check_states(
4643
expected: dict[int, EVChargerState],
47-
latest: Optional[tuple[int, EVChargerState]],
48-
) -> EVChargerPoolStates:
49-
pool_states = await states.receive()
50-
assert pool_states.latest_change() == latest
51-
assert pool_states._states == expected # pylint: disable=protected-access
52-
return pool_states
44+
) -> None:
45+
await asyncio.sleep(0.02)
46+
for comp_id, exp_state in expected.items():
47+
assert state_tracker.get(comp_id) == exp_state
5348

5449
## check that all chargers are in idle state.
5550
expected_states = {evc_id: EVChargerState.IDLE for evc_id in mockgrid.evc_ids}
5651
assert len(expected_states) == 5
57-
await check_next_state(expected_states, None)
52+
await check_states(expected_states)
5853

5954
## check that EV_PLUGGED state gets set
60-
await asyncio.sleep(0.02)
6155
evc_2_id = mockgrid.evc_ids[2]
6256
mockgrid.evc_cable_states[evc_2_id] = EVChargerCableState.EV_PLUGGED
6357
mockgrid.evc_component_states[evc_2_id] = EVChargerComponentState.READY
6458
expected_states[evc_2_id] = EVChargerState.EV_PLUGGED
65-
await check_next_state(expected_states, (evc_2_id, EVChargerState.EV_PLUGGED))
59+
await check_states(expected_states)
6660

6761
## check that EV_LOCKED state gets set
68-
await asyncio.sleep(0.03)
6962
evc_3_id = mockgrid.evc_ids[3]
7063
mockgrid.evc_cable_states[evc_3_id] = EVChargerCableState.EV_LOCKED
7164
mockgrid.evc_component_states[evc_3_id] = EVChargerComponentState.READY
7265
expected_states[evc_3_id] = EVChargerState.EV_LOCKED
73-
await check_next_state(expected_states, (evc_3_id, EVChargerState.EV_LOCKED))
66+
await check_states(expected_states)
7467

7568
## check that ERROR state gets set
76-
await asyncio.sleep(0.1)
7769
evc_1_id = mockgrid.evc_ids[1]
7870
mockgrid.evc_cable_states[evc_1_id] = EVChargerCableState.EV_LOCKED
7971
mockgrid.evc_component_states[evc_1_id] = EVChargerComponentState.ERROR
8072
expected_states[evc_1_id] = EVChargerState.ERROR
81-
await check_next_state(expected_states, (evc_1_id, EVChargerState.ERROR))
73+
await check_states(expected_states)
8274

8375
await state_tracker.stop()
8476
await mockgrid.cleanup()

0 commit comments

Comments
 (0)