PyPlumIO 0.5.21__py3-none-any.whl → 0.5.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/METADATA +12 -10
  2. PyPlumIO-0.5.23.dist-info/RECORD +60 -0
  3. {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/WHEEL +1 -1
  4. pyplumio/__init__.py +2 -2
  5. pyplumio/_version.py +2 -2
  6. pyplumio/connection.py +3 -12
  7. pyplumio/devices/__init__.py +16 -16
  8. pyplumio/devices/ecomax.py +126 -126
  9. pyplumio/devices/mixer.py +50 -44
  10. pyplumio/devices/thermostat.py +36 -35
  11. pyplumio/exceptions.py +9 -9
  12. pyplumio/filters.py +56 -37
  13. pyplumio/frames/__init__.py +6 -6
  14. pyplumio/frames/messages.py +4 -6
  15. pyplumio/helpers/data_types.py +8 -7
  16. pyplumio/helpers/event_manager.py +53 -33
  17. pyplumio/helpers/parameter.py +138 -52
  18. pyplumio/helpers/task_manager.py +7 -2
  19. pyplumio/helpers/timeout.py +0 -3
  20. pyplumio/helpers/uid.py +2 -2
  21. pyplumio/protocol.py +35 -28
  22. pyplumio/stream.py +2 -2
  23. pyplumio/structures/alerts.py +40 -31
  24. pyplumio/structures/ecomax_parameters.py +493 -282
  25. pyplumio/structures/frame_versions.py +5 -6
  26. pyplumio/structures/lambda_sensor.py +6 -6
  27. pyplumio/structures/mixer_parameters.py +136 -71
  28. pyplumio/structures/network_info.py +2 -3
  29. pyplumio/structures/product_info.py +0 -4
  30. pyplumio/structures/program_version.py +24 -17
  31. pyplumio/structures/schedules.py +35 -15
  32. pyplumio/structures/thermostat_parameters.py +82 -50
  33. pyplumio/utils.py +12 -7
  34. PyPlumIO-0.5.21.dist-info/RECORD +0 -61
  35. pyplumio/helpers/typing.py +0 -29
  36. {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/LICENSE +0 -0
  37. {PyPlumIO-0.5.21.dist-info → PyPlumIO-0.5.23.dist-info}/top_level.txt +0 -0
@@ -3,43 +3,47 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Sequence
7
- from typing import Any
6
+ from collections.abc import Coroutine, Generator, Sequence
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from pyplumio.devices import AddressableDevice, SubDevice
10
10
  from pyplumio.helpers.parameter import ParameterValues
11
11
  from pyplumio.structures.thermostat_parameters import (
12
12
  ATTR_THERMOSTAT_PARAMETERS,
13
13
  THERMOSTAT_PARAMETERS,
14
- ThermostatBinaryParameter,
15
- ThermostatBinaryParameterDescription,
16
- ThermostatParameter,
14
+ ThermostatNumber,
15
+ ThermostatSwitch,
16
+ ThermostatSwitchDescription,
17
17
  )
18
18
  from pyplumio.structures.thermostat_sensors import ATTR_THERMOSTAT_SENSORS
19
19
 
20
+ if TYPE_CHECKING:
21
+ from pyplumio.frames import Frame
22
+
20
23
 
21
24
  class Thermostat(SubDevice):
22
25
  """Represents a thermostat."""
23
26
 
24
- def __init__(self, queue: asyncio.Queue, parent: AddressableDevice, index: int = 0):
27
+ def __init__(
28
+ self, queue: asyncio.Queue[Frame], parent: AddressableDevice, index: int = 0
29
+ ):
25
30
  """Initialize a new thermostat."""
26
31
  super().__init__(queue, parent, index)
27
- self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_sensors)
28
- self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_parameters)
32
+ self.subscribe(ATTR_THERMOSTAT_SENSORS, self._handle_thermostat_sensors)
33
+ self.subscribe(ATTR_THERMOSTAT_PARAMETERS, self._handle_thermostat_parameters)
29
34
 
30
- async def _handle_sensors(self, sensors: dict[str, Any]) -> bool:
35
+ async def _handle_thermostat_sensors(self, sensors: dict[str, Any]) -> bool:
31
36
  """Handle thermostat sensors.
32
37
 
33
38
  For each sensor dispatch an event with the
34
39
  sensor's name and value.
35
40
  """
36
41
  await asyncio.gather(
37
- *[self.dispatch(name, value) for name, value in sensors.items()]
42
+ *(self.dispatch(name, value) for name, value in sensors.items())
38
43
  )
39
-
40
44
  return True
41
45
 
42
- async def _handle_parameters(
46
+ async def _handle_thermostat_parameters(
43
47
  self, parameters: Sequence[tuple[int, ParameterValues]]
44
48
  ) -> bool:
45
49
  """Handle thermostat parameters.
@@ -47,29 +51,26 @@ class Thermostat(SubDevice):
47
51
  For each parameter dispatch an event with the
48
52
  parameter's name and value.
49
53
  """
50
- for index, values in parameters:
51
- description = THERMOSTAT_PARAMETERS[index]
52
- name = description.name
53
- if name in self.data:
54
- parameter: ThermostatParameter = self.data[name]
55
- parameter.values = values
56
- await self.dispatch(name, parameter)
57
- continue
58
54
 
59
- cls = (
60
- ThermostatBinaryParameter
61
- if isinstance(description, ThermostatBinaryParameterDescription)
62
- else ThermostatParameter
63
- )
64
- await self.dispatch(
65
- name,
66
- cls(
67
- device=self,
68
- values=values,
69
- description=description,
70
- index=index,
71
- offset=(self.index * len(parameters)),
72
- ),
73
- )
55
+ def _thermostat_parameter_events() -> Generator[Coroutine, Any, None]:
56
+ """Get dispatch calls for thermostat parameter events."""
57
+ for index, values in parameters:
58
+ description = THERMOSTAT_PARAMETERS[index]
59
+ handler = (
60
+ ThermostatSwitch
61
+ if isinstance(description, ThermostatSwitchDescription)
62
+ else ThermostatNumber
63
+ )
64
+ yield self.dispatch(
65
+ description.name,
66
+ handler.create_or_update(
67
+ device=self,
68
+ description=description,
69
+ values=values,
70
+ index=index,
71
+ offset=(self.index * len(parameters)),
72
+ ),
73
+ )
74
74
 
75
+ await asyncio.gather(*_thermostat_parameter_events())
75
76
  return True
pyplumio/exceptions.py CHANGED
@@ -11,25 +11,25 @@ class ConnectionFailedError(PyPlumIOError):
11
11
  """Raised on connection failure."""
12
12
 
13
13
 
14
- class UnknownDeviceError(PyPlumIOError):
15
- """Raised on unsupported device."""
14
+ class ProtocolError(PyPlumIOError):
15
+ """Base class for protocol-related errors."""
16
16
 
17
17
 
18
- class ReadError(PyPlumIOError):
18
+ class ReadError(ProtocolError):
19
19
  """Raised on read error."""
20
20
 
21
21
 
22
- class FrameError(PyPlumIOError):
23
- """Base class for frame errors."""
22
+ class ChecksumError(ProtocolError):
23
+ """Raised on incorrect frame checksum."""
24
24
 
25
25
 
26
- class ChecksumError(FrameError):
27
- """Raised on checksum error while parsing frame content."""
26
+ class UnknownDeviceError(ProtocolError):
27
+ """Raised on unknown device."""
28
28
 
29
29
 
30
- class UnknownFrameError(FrameError):
30
+ class UnknownFrameError(ProtocolError):
31
31
  """Raised on unknown frame type."""
32
32
 
33
33
 
34
- class FrameDataError(FrameError):
34
+ class FrameDataError(ProtocolError):
35
35
  """Raised on incorrect frame data."""
pyplumio/filters.py CHANGED
@@ -3,18 +3,49 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
- from collections.abc import Awaitable, Callable
6
+ from collections.abc import Callable
7
7
  from copy import copy
8
8
  import math
9
9
  import time
10
- from typing import Any, Final, SupportsFloat, TypeVar, overload
11
-
10
+ from typing import (
11
+ Any,
12
+ Final,
13
+ Protocol,
14
+ SupportsFloat,
15
+ TypeVar,
16
+ overload,
17
+ runtime_checkable,
18
+ )
19
+
20
+ from pyplumio.helpers.event_manager import Callback
12
21
  from pyplumio.helpers.parameter import Parameter
13
- from pyplumio.helpers.typing import SupportsComparison, SupportsSubtraction
14
22
 
15
23
  UNDEFINED: Final = "undefined"
16
24
  TOLERANCE: Final = 0.1
17
25
 
26
+
27
+ @runtime_checkable
28
+ class SupportsSubtraction(Protocol):
29
+ """Supports subtraction operation."""
30
+
31
+ __slots__ = ()
32
+
33
+ def __sub__(
34
+ self: SupportsSubtraction, other: SupportsSubtraction
35
+ ) -> SupportsSubtraction:
36
+ """Subtract a value."""
37
+
38
+
39
+ @runtime_checkable
40
+ class SupportsComparison(Protocol):
41
+ """Supports comparison."""
42
+
43
+ __slots__ = ()
44
+
45
+ def __eq__(self: SupportsComparison, other: SupportsComparison) -> bool:
46
+ """Compare a value."""
47
+
48
+
18
49
  Comparable = TypeVar("Comparable", Parameter, SupportsFloat, SupportsComparison)
19
50
 
20
51
 
@@ -71,23 +102,23 @@ class Filter(ABC):
71
102
 
72
103
  __slots__ = ("_callback", "_value")
73
104
 
74
- _callback: Callable[[Any], Awaitable[Any]]
105
+ _callback: Callback
75
106
  _value: Any
76
107
 
77
- def __init__(self, callback: Callable[[Any], Awaitable[Any]]) -> None:
108
+ def __init__(self, callback: Callback) -> None:
78
109
  """Initialize a new filter."""
79
110
  self._callback = callback
80
111
  self._value = UNDEFINED
81
112
 
82
- def __eq__(self, other: Any) -> Any:
113
+ def __eq__(self, other: Any) -> bool:
83
114
  """Compare callbacks."""
84
115
  if isinstance(other, Filter):
85
116
  return self._callback == other._callback
86
117
 
87
118
  if callable(other):
88
- return self._callback == other
119
+ return bool(self._callback == other)
89
120
 
90
- raise TypeError
121
+ return NotImplemented
91
122
 
92
123
  @abstractmethod
93
124
  async def __call__(self, new_value: Any) -> Any:
@@ -112,14 +143,14 @@ class _OnChange(Filter):
112
143
  return await self._callback(new_value)
113
144
 
114
145
 
115
- def on_change(callback: Callable[[Any], Awaitable[Any]]) -> _OnChange:
146
+ def on_change(callback: Callback) -> _OnChange:
116
147
  """Return a value changed filter.
117
148
 
118
149
  A callback function will only be called if value is changed from the
119
150
  previous call.
120
151
 
121
152
  :param callback: A callback function to be awaited on value change
122
- :type callback: Callable[[Any], Awaitable[Any]]
153
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
123
154
  :return: A instance of callable filter
124
155
  :rtype: _OnChange
125
156
  """
@@ -138,9 +169,7 @@ class _Debounce(Filter):
138
169
  _calls: int
139
170
  _min_calls: int
140
171
 
141
- def __init__(
142
- self, callback: Callable[[Any], Awaitable[Any]], min_calls: int
143
- ) -> None:
172
+ def __init__(self, callback: Callback, min_calls: int) -> None:
144
173
  """Initialize a new debounce filter."""
145
174
  super().__init__(callback)
146
175
  self._calls = 0
@@ -161,14 +190,14 @@ class _Debounce(Filter):
161
190
  return await self._callback(new_value)
162
191
 
163
192
 
164
- def debounce(callback: Callable[[Any], Awaitable[Any]], min_calls: int) -> _Debounce:
193
+ def debounce(callback: Callback, min_calls: int) -> _Debounce:
165
194
  """Return a debounce filter.
166
195
 
167
196
  A callback function will only called once value is stabilized
168
197
  across multiple filter calls.
169
198
 
170
199
  :param callback: A callback function to be awaited on value change
171
- :type callback: Callable[[Any], Awaitable[Any]]
200
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
172
201
  :param min_calls: Value shouldn't change for this amount of
173
202
  filter calls
174
203
  :type min_calls: int
@@ -190,9 +219,7 @@ class _Throttle(Filter):
190
219
  _last_called: float | None
191
220
  _timeout: float
192
221
 
193
- def __init__(
194
- self, callback: Callable[[Any], Awaitable[Any]], seconds: float
195
- ) -> None:
222
+ def __init__(self, callback: Callback, seconds: float) -> None:
196
223
  """Initialize a new throttle filter."""
197
224
  super().__init__(callback)
198
225
  self._last_called = None
@@ -209,7 +236,7 @@ class _Throttle(Filter):
209
236
  return await self._callback(new_value)
210
237
 
211
238
 
212
- def throttle(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Throttle:
239
+ def throttle(callback: Callback, seconds: float) -> _Throttle:
213
240
  """Return a throttle filter.
214
241
 
215
242
  A callback function will only be called once a certain amount of
@@ -217,7 +244,7 @@ def throttle(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Thro
217
244
 
218
245
  :param callback: A callback function that will be awaited once
219
246
  filter conditions are fulfilled
220
- :type callback: Callable[[Any], Awaitable[Any]]
247
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
221
248
  :param seconds: A callback will be awaited at most once per
222
249
  this amount of seconds
223
250
  :type seconds: float
@@ -249,7 +276,7 @@ class _Delta(Filter):
249
276
  return await self._callback(difference)
250
277
 
251
278
 
252
- def delta(callback: Callable[[Any], Awaitable[Any]]) -> _Delta:
279
+ def delta(callback: Callback) -> _Delta:
253
280
  """Return a difference filter.
254
281
 
255
282
  A callback function will be called with a difference between two
@@ -257,7 +284,7 @@ def delta(callback: Callable[[Any], Awaitable[Any]]) -> _Delta:
257
284
 
258
285
  :param callback: A callback function that will be awaited with
259
286
  difference between values in two subsequent calls
260
- :type callback: Callable[[Any], Awaitable[Any]]
287
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
261
288
  :return: A instance of callable filter
262
289
  :rtype: _Delta
263
290
  """
@@ -277,9 +304,7 @@ class _Aggregate(Filter):
277
304
  _last_update: float
278
305
  _timeout: float
279
306
 
280
- def __init__(
281
- self, callback: Callable[[Any], Awaitable[Any]], seconds: float
282
- ) -> None:
307
+ def __init__(self, callback: Callback, seconds: float) -> None:
283
308
  """Initialize a new aggregate filter."""
284
309
  super().__init__(callback)
285
310
  self._last_update = time.monotonic()
@@ -303,7 +328,7 @@ class _Aggregate(Filter):
303
328
  return result
304
329
 
305
330
 
306
- def aggregate(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Aggregate:
331
+ def aggregate(callback: Callback, seconds: float) -> _Aggregate:
307
332
  """Return an aggregate filter.
308
333
 
309
334
  A callback function will be called with a sum of values collected
@@ -311,7 +336,7 @@ def aggregate(callback: Callable[[Any], Awaitable[Any]], seconds: float) -> _Agg
311
336
 
312
337
  :param callback: A callback function to be awaited once filter
313
338
  conditions are fulfilled
314
- :type callback: Callable[[Any], Awaitable[Any]]
339
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
315
340
  :param seconds: A callback will be awaited with a sum of values
316
341
  aggregated over this amount of seconds.
317
342
  :type seconds: float
@@ -333,11 +358,7 @@ class _Custom(Filter):
333
358
 
334
359
  filter_fn: Callable[[Any], bool]
335
360
 
336
- def __init__(
337
- self,
338
- callback: Callable[[Any], Awaitable[Any]],
339
- filter_fn: Callable[[Any], bool],
340
- ) -> None:
361
+ def __init__(self, callback: Callback, filter_fn: Callable[[Any], bool]) -> None:
341
362
  """Initialize a new custom filter."""
342
363
  super().__init__(callback)
343
364
  self._filter_fn = filter_fn
@@ -348,9 +369,7 @@ class _Custom(Filter):
348
369
  await self._callback(new_value)
349
370
 
350
371
 
351
- def custom(
352
- callback: Callable[[Any], Awaitable[Any]], filter_fn: Callable[[Any], bool]
353
- ) -> _Custom:
372
+ def custom(callback: Callback, filter_fn: Callable[[Any], bool]) -> _Custom:
354
373
  """Return a custom filter.
355
374
 
356
375
  A callback function will be called when user-defined filter
@@ -359,7 +378,7 @@ def custom(
359
378
 
360
379
  :param callback: A callback function to be awaited when
361
380
  filter function return true
362
- :type callback: Callable[[Any], Awaitable[Any]]
381
+ :type callback: Callable[[Any], Coroutine[Any, Any, Any]]
363
382
  :param filter_fn: Filter function, that will be called with a
364
383
  value and should return `True` to await filter's callback
365
384
  :type filter_fn: Callable[[Any], bool]
@@ -29,9 +29,6 @@ if TYPE_CHECKING:
29
29
  from pyplumio.devices import AddressableDevice
30
30
 
31
31
 
32
- T = TypeVar("T")
33
-
34
-
35
32
  def bcc(data: bytes) -> int:
36
33
  """Return a block check character."""
37
34
  return reduce(lambda x, y: x ^ y, data)
@@ -112,7 +109,7 @@ class Frame(ABC):
112
109
  self._data = data if not kwargs else ensure_dict(data, kwargs)
113
110
  self._message = message
114
111
 
115
- def __eq__(self, other: Any) -> bool:
112
+ def __eq__(self, other: object) -> bool:
116
113
  """Compare if this frame is equal to other."""
117
114
  if isinstance(other, Frame):
118
115
  return (
@@ -131,7 +128,7 @@ class Frame(ABC):
131
128
  self._data,
132
129
  )
133
130
 
134
- raise TypeError
131
+ return NotImplemented
135
132
 
136
133
  def __repr__(self) -> str:
137
134
  """Return a serializable string representation."""
@@ -224,7 +221,7 @@ class Frame(ABC):
224
221
  return bytes(data)
225
222
 
226
223
  @classmethod
227
- async def create(cls: type[T], frame_type: int, **kwargs: Any) -> T:
224
+ async def create(cls: type[FrameT], frame_type: int, **kwargs: Any) -> FrameT:
228
225
  """Create a frame handler object from frame type."""
229
226
  return await create_instance(get_frame_handler(frame_type), cls=cls, **kwargs)
230
227
 
@@ -237,6 +234,9 @@ class Frame(ABC):
237
234
  """Decode frame message."""
238
235
 
239
236
 
237
+ FrameT = TypeVar("FrameT", bound=Frame)
238
+
239
+
240
240
  class Request(Frame):
241
241
  """Represents a request."""
242
242
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from contextlib import suppress
5
6
  from typing import Any, ClassVar
6
7
 
7
8
  from pyplumio.const import (
@@ -53,12 +54,7 @@ class SensorDataMessage(Message):
53
54
  def decode_message(self, message: bytearray) -> dict[str, Any]:
54
55
  """Decode a frame message."""
55
56
  sensors, offset = FrameVersionsStructure(self).decode(message, offset=0)
56
- try:
57
- sensors[ATTR_STATE] = message[offset]
58
- sensors[ATTR_STATE] = DeviceState(sensors[ATTR_STATE])
59
- except ValueError:
60
- pass
61
-
57
+ sensors[ATTR_STATE] = message[offset]
62
58
  sensors, offset = OutputsStructure(self).decode(message, offset + 1, sensors)
63
59
  sensors, offset = OutputFlagsStructure(self).decode(message, offset, sensors)
64
60
  sensors, offset = TemperaturesStructure(self).decode(message, offset, sensors)
@@ -79,5 +75,7 @@ class SensorDataMessage(Message):
79
75
  message, offset, sensors
80
76
  )
81
77
  sensors, offset = MixerSensorsStructure(self).decode(message, offset, sensors)
78
+ with suppress(ValueError):
79
+ sensors[ATTR_STATE] = DeviceState(sensors[ATTR_STATE])
82
80
 
83
81
  return {ATTR_SENSORS: sensors}
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
  import socket
7
7
  import struct
8
- from typing import Any, ClassVar, Final
8
+ from typing import Any, ClassVar, Final, TypeVar
9
9
 
10
10
 
11
11
  class DataType(ABC):
@@ -25,21 +25,19 @@ class DataType(ABC):
25
25
  """Return serializable string representation of the class."""
26
26
  return f"{self.__class__.__name__}(value={self._value})"
27
27
 
28
- def __eq__(self, other: Any) -> bool:
28
+ def __eq__(self, other: object) -> bool:
29
29
  """Compare if this data type is equal to other."""
30
30
  if isinstance(other, DataType):
31
- result: bool = self._value == other._value
32
- else:
33
- result = self._value == other
31
+ return bool(self._value == other._value)
34
32
 
35
- return result
33
+ return bool(self._value == other)
36
34
 
37
35
  def _slice_data(self, data: bytes) -> bytes:
38
36
  """Slice the data to data type size."""
39
37
  return data[: self.size] if self.size is not None else data
40
38
 
41
39
  @classmethod
42
- def from_bytes(cls, data: bytes, offset: int = 0) -> DataType:
40
+ def from_bytes(cls: type[DataTypeT], data: bytes, offset: int = 0) -> DataTypeT:
43
41
  """Initialize a new data type from bytes."""
44
42
  data_type = cls()
45
43
  data_type.unpack(data[offset:])
@@ -68,6 +66,9 @@ class DataType(ABC):
68
66
  """Unpack the data."""
69
67
 
70
68
 
69
+ DataTypeT = TypeVar("DataTypeT", bound=DataType)
70
+
71
+
71
72
  class Undefined(DataType):
72
73
  """Represents an undefined."""
73
74
 
@@ -3,18 +3,26 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from collections.abc import Awaitable, Callable
7
- from typing import Any
6
+ from collections.abc import Callable, Coroutine
7
+ from typing import Any, Generic, TypeVar, overload
8
+
9
+ from typing_extensions import TypeAlias
8
10
 
9
11
  from pyplumio.helpers.task_manager import TaskManager
10
12
 
13
+ Callback: TypeAlias = Callable[[Any], Coroutine[Any, Any, Any]]
14
+ CallbackT = TypeVar("CallbackT", bound=Callback)
15
+ T = TypeVar("T")
16
+
11
17
 
12
- class EventManager(TaskManager):
18
+ class EventManager(TaskManager, Generic[T]):
13
19
  """Represents an event manager."""
14
20
 
15
- data: dict[str, Any]
21
+ __slots__ = ("data", "_events", "_callbacks")
22
+
23
+ data: dict[str, T]
16
24
  _events: dict[str, asyncio.Event]
17
- _callbacks: dict[str, list[Callable[[Any], Awaitable[Any]]]]
25
+ _callbacks: dict[str, list[Callback]]
18
26
 
19
27
  def __init__(self) -> None:
20
28
  """Initialize a new event manager."""
@@ -23,7 +31,7 @@ class EventManager(TaskManager):
23
31
  self._events = {}
24
32
  self._callbacks = {}
25
33
 
26
- def __getattr__(self, name: str) -> Any:
34
+ def __getattr__(self, name: str) -> T:
27
35
  """Return attributes from the underlying data dictionary."""
28
36
  try:
29
37
  return self.data[name]
@@ -43,7 +51,7 @@ class EventManager(TaskManager):
43
51
  if name not in self.data:
44
52
  await asyncio.wait_for(self.create_event(name).wait(), timeout=timeout)
45
53
 
46
- async def get(self, name: str, timeout: float | None = None) -> Any:
54
+ async def get(self, name: str, timeout: float | None = None) -> T:
47
55
  """Get the value by name.
48
56
 
49
57
  :param name: Event name or ID
@@ -57,6 +65,12 @@ class EventManager(TaskManager):
57
65
  await self.wait_for(name, timeout=timeout)
58
66
  return self.data[name]
59
67
 
68
+ @overload
69
+ def get_nowait(self, name: str, default: None = ...) -> T | None: ...
70
+
71
+ @overload
72
+ def get_nowait(self, name: str, default: T) -> T: ...
73
+
60
74
  def get_nowait(self, name: str, default: Any = None) -> Any:
61
75
  """Get the value by name without waiting.
62
76
 
@@ -75,23 +89,23 @@ class EventManager(TaskManager):
75
89
  except KeyError:
76
90
  return default
77
91
 
78
- def subscribe(self, name: str, callback: Callable[[Any], Awaitable[Any]]) -> None:
92
+ def subscribe(self, name: str, callback: CallbackT) -> CallbackT:
79
93
  """Subscribe a callback to the event.
80
94
 
81
95
  :param name: Event name or ID
82
96
  :type name: str
83
97
  :param callback: A coroutine callback function, that will be
84
98
  awaited on the with the event data as an argument.
85
- :type callback: Callable[[Any], Awaitable[Any]]
99
+ :type callback: Callback
100
+ :return: A reference to the callback, that can be used
101
+ with `EventManager.unsubscribe()`.
102
+ :rtype: Callback
86
103
  """
87
- if name not in self._callbacks:
88
- self._callbacks[name] = []
89
-
90
- self._callbacks[name].append(callback)
104
+ callbacks = self._callbacks.setdefault(name, [])
105
+ callbacks.append(callback)
106
+ return callback
91
107
 
92
- def subscribe_once(
93
- self, name: str, callback: Callable[[Any], Awaitable[Any]]
94
- ) -> None:
108
+ def subscribe_once(self, name: str, callback: Callback) -> Callback:
95
109
  """Subscribe a callback to the event once.
96
110
 
97
111
  Callback will be unsubscribed after single event.
@@ -100,17 +114,20 @@ class EventManager(TaskManager):
100
114
  :type name: str
101
115
  :param callback: A coroutine callback function, that will be
102
116
  awaited on the with the event data as an argument.
103
- :type callback: Callable[[Any], Awaitable[Any]]
117
+ :type callback: Callback
118
+ :return: A reference to the callback, that can be used
119
+ with `EventManager.unsubscribe()`.
120
+ :rtype: Callback
104
121
  """
105
122
 
106
- async def _callback(value: Any) -> Any:
123
+ async def _call_once(value: Any) -> Any:
107
124
  """Unsubscribe callback from the event and calls it."""
108
- self.unsubscribe(name, _callback)
125
+ self.unsubscribe(name, _call_once)
109
126
  return await callback(value)
110
127
 
111
- self.subscribe(name, _callback)
128
+ return self.subscribe(name, _call_once)
112
129
 
113
- def unsubscribe(self, name: str, callback: Callable[[Any], Awaitable[Any]]) -> None:
130
+ def unsubscribe(self, name: str, callback: Callback) -> bool:
114
131
  """Usubscribe a callback from the event.
115
132
 
116
133
  :param name: Event name or ID
@@ -118,34 +135,37 @@ class EventManager(TaskManager):
118
135
  :param callback: A coroutine callback function, previously
119
136
  subscribed to an event using ``subscribe()`` or
120
137
  ``subscribe_once()`` methods.
121
- :type callback: Callable[[Any], Awaitable[Any]]
138
+ :type callback: Callback
139
+ :return: `True` if callback is found, `False` otherwise.
140
+ :rtype: bool
122
141
  """
123
142
  if name in self._callbacks and callback in self._callbacks[name]:
124
143
  self._callbacks[name].remove(callback)
144
+ return True
145
+
146
+ return False
125
147
 
126
- async def dispatch(self, name: str, value: Any) -> None:
148
+ async def dispatch(self, name: str, value: T) -> None:
127
149
  """Call registered callbacks and dispatch the event."""
128
- if name in self._callbacks:
129
- callbacks = self._callbacks[name].copy()
130
- for callback in callbacks:
131
- return_value = await callback(value)
132
- value = return_value if return_value is not None else value
150
+ if callbacks := self._callbacks.get(name, None):
151
+ for callback in list(callbacks):
152
+ result = await callback(value)
153
+ value = result if result is not None else value
133
154
 
134
155
  self.data[name] = value
135
156
  self.set_event(name)
136
157
 
137
- def dispatch_nowait(self, name: str, value: Any) -> None:
158
+ def dispatch_nowait(self, name: str, value: T) -> None:
138
159
  """Call a registered callbacks and dispatch the event without waiting."""
139
160
  self.create_task(self.dispatch(name, value))
140
161
 
141
- async def load(self, data: dict[str, Any]) -> None:
162
+ async def load(self, data: dict[str, T]) -> None:
142
163
  """Load event data."""
143
- self.data = data
144
164
  await asyncio.gather(
145
- *[self.dispatch(name, value) for name, value in data.items()]
165
+ *(self.dispatch(name, value) for name, value in data.items())
146
166
  )
147
167
 
148
- def load_nowait(self, data: dict[str, Any]) -> None:
168
+ def load_nowait(self, data: dict[str, T]) -> None:
149
169
  """Load event data without waiting."""
150
170
  self.create_task(self.load(data))
151
171