22# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
33"""Class to return battery status."""
44
5+ from __future__ import annotations
6+
57import asyncio
68import logging
79from dataclasses import dataclass
1517from frequenz .api .microgrid .inverter_pb2 import ComponentState as InverterComponentState
1618from frequenz .channels import Peekable
1719
18- from .. import ComponentGraph
20+ from ... _internal . asyncio import AsyncConstructible
1921from .. import get as get_microgrid
2022from ..component import BatteryData , ComponentCategory , ComponentData , InverterData
2123
@@ -46,9 +48,15 @@ class _ComponentReceiver(Generic[T]):
4648 receiver : Peekable [T ]
4749
4850
49- class StatusTracker :
50- """Class for tracking if battery is working."""
51+ class StatusTracker (AsyncConstructible ):
52+ """Class for tracking if battery is working.
53+
54+ To create an instance of this class you should use `async_new` class method.
55+ Standard constructor (__init__) is not supported and using it will raise
56+ `NotSyncConstructible` error.
57+ """
5158
59+ # Class attributes
5260 _battery_valid_relay : Set [BatteryRelayState .ValueType ] = {
5361 BatteryRelayState .RELAY_STATE_CLOSED
5462 }
@@ -64,9 +72,21 @@ class StatusTracker:
6472 InverterComponentState .COMPONENT_STATE_STANDBY ,
6573 }
6674
67- def __init__ (
68- self , battery_id : int , max_data_age_sec : float , max_blocking_duration_sec : float
69- ) -> None :
75+ # Instance attributes
76+ _battery_id : int
77+ _max_data_age : float
78+ _last_status : BatteryStatus
79+ _min_blocking_duration_sec : float
80+ _max_blocking_duration_sec : float
81+ _blocked_until : Optional [datetime ]
82+ _last_blocking_duration_sec : float
83+ _battery_receiver : _ComponentReceiver [BatteryData ]
84+ _inverter_receiver : _ComponentReceiver [InverterData ]
85+
86+ @classmethod
87+ async def async_new (
88+ cls , battery_id : int , max_data_age_sec : float , max_blocking_duration_sec : float
89+ ) -> StatusTracker :
7090 """Create class instance.
7191
7292 Args:
@@ -77,37 +97,25 @@ def __init__(
7797 data.
7898 max_blocking_duration_sec: This value tell what should be the maximum
7999 timeout used for blocking failing component.
80- """
81- self ._battery_id : int = battery_id
82- self ._max_data_age : float = max_data_age_sec
83-
84- self ._init_method_called : bool = False
85- self ._last_status : BatteryStatus = BatteryStatus .WORKING
86-
87- self ._min_blocking_duration_sec : float = 1.0
88- self ._max_blocking_duration_sec : float = max_blocking_duration_sec
89- self ._blocked_until : Optional [datetime ] = None
90- self ._last_blocking_duration_sec : float = self ._min_blocking_duration_sec
91-
92- @property
93- def battery_id (self ) -> int :
94- """Get battery id.
95100
96101 Returns:
97- Battery id
102+ New instance of this class.
103+
104+ Raises:
105+ RuntimeError: If battery has no adjacent inverter.
98106 """
99- return self ._battery_id
107+ self : StatusTracker = StatusTracker .__new__ (cls )
108+ self ._battery_id = battery_id
109+ self ._max_data_age = max_data_age_sec
100110
101- async def async_init (self ) -> None :
102- """Async part of constructor.
111+ self ._last_status = BatteryStatus .WORKING
103112
104- This should be called in order to subscribe for necessary data.
113+ self ._min_blocking_duration_sec = 1.0
114+ self ._max_blocking_duration_sec = max_blocking_duration_sec
115+ self ._blocked_until = None
116+ self ._last_blocking_duration_sec = self ._min_blocking_duration_sec
105117
106- Raises:
107- RuntimeError: If given battery as no adjacent inverter.
108- """
109- component_graph = get_microgrid ().component_graph
110- inverter_id = self ._find_adjacent_inverter_id (component_graph )
118+ inverter_id = self ._find_adjacent_inverter_id ()
111119 if inverter_id is None :
112120 raise RuntimeError (
113121 f"Can't find inverter adjacent to battery: { self ._battery_id } "
@@ -116,22 +124,26 @@ async def async_init(self) -> None:
116124 api_client = get_microgrid ().api_client
117125
118126 bat_recv = await api_client .battery_data (self ._battery_id )
119- # Defining battery_receiver and inverter receiver needs await.
120- # Because of that it is impossible to define it in constructor.
121- # Setting it to None first would require to check None condition in
122- # every call.
123- # pylint: disable=attribute-defined-outside-init
124127 self ._battery_receiver = _ComponentReceiver (
125128 self ._battery_id , bat_recv .into_peekable ()
126129 )
130+
127131 inv_recv = await api_client .inverter_data (inverter_id )
128- # pylint: disable=attribute-defined-outside-init
129132 self ._inverter_receiver = _ComponentReceiver (
130133 inverter_id , inv_recv .into_peekable ()
131134 )
132135
133136 await asyncio .sleep (_START_DELAY_SEC )
134- self ._init_method_called = True
137+ return self
138+
139+ @property
140+ def battery_id (self ) -> int :
141+ """Get battery id.
142+
143+ Returns:
144+ Battery id
145+ """
146+ return self ._battery_id
135147
136148 def get_status (self ) -> BatteryStatus :
137149 """Return status of the battery.
@@ -145,11 +157,6 @@ def get_status(self) -> BatteryStatus:
145157 Returns:
146158 Battery status
147159 """
148- if not self ._init_method_called :
149- raise RuntimeError (
150- "`async_init` method not called or not awaited. Run it before first use"
151- )
152-
153160 bat_msg = self ._battery_receiver .receiver .peek ()
154161 inv_msg = self ._inverter_receiver .receiver .peek ()
155162 if bat_msg is None or inv_msg is None :
@@ -353,15 +360,13 @@ def _is_message_reliable(self, message: ComponentData) -> bool:
353360
354361 return not is_outdated
355362
356- def _find_adjacent_inverter_id (self , graph : ComponentGraph ) -> Optional [int ]:
363+ def _find_adjacent_inverter_id (self ) -> Optional [int ]:
357364 """Find inverter adjacent to this battery.
358365
359- Args:
360- graph: Component graph
361-
362366 Returns:
363367 Id of the inverter. If battery hasn't adjacent inverter, then return None.
364368 """
369+ graph = get_microgrid ().component_graph
365370 return next (
366371 (
367372 comp .component_id
0 commit comments