PyPlumIO 0.5.55__py3-none-any.whl → 0.6.0__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.
pyplumio/__init__.py CHANGED
@@ -23,17 +23,17 @@ from pyplumio.structures.network_info import EthernetParameters, WirelessParamet
23
23
 
24
24
 
25
25
  def open_serial_connection(
26
- device: str,
26
+ url: str,
27
27
  baudrate: int = 115200,
28
28
  *,
29
29
  protocol: Protocol | None = None,
30
30
  reconnect_on_failure: bool = True,
31
- **kwargs: Any,
31
+ **options: Any,
32
32
  ) -> SerialConnection:
33
33
  r"""Create a serial connection.
34
34
 
35
- :param device: Serial port device name. e. g. /dev/ttyUSB0
36
- :type device: str
35
+ :param url: Serial port device url. e. g. /dev/ttyUSB0
36
+ :type url: str
37
37
  :param baudrate: Serial port baud rate, defaults to 115200
38
38
  :type baudrate: int, optional
39
39
  :param protocol: Protocol that will be used for communication with
@@ -42,17 +42,17 @@ def open_serial_connection(
42
42
  :param reconnect_on_failure: `True` if PyPlumIO should try
43
43
  reconnecting on failure, otherwise `False`, default to `True`
44
44
  :type reconnect_on_failure: bool, optional
45
- :param \**kwargs: Additional keyword arguments to be passed to
45
+ :param \**options: Additional arguments to be passed to
46
46
  serial_asyncio.open_serial_connection()
47
47
  :return: An instance of serial connection
48
48
  :rtype: SerialConnection
49
49
  """
50
50
  return SerialConnection(
51
- device,
51
+ url,
52
52
  baudrate,
53
53
  protocol=protocol,
54
54
  reconnect_on_failure=reconnect_on_failure,
55
- **kwargs,
55
+ **options,
56
56
  )
57
57
 
58
58
 
@@ -62,7 +62,7 @@ def open_tcp_connection(
62
62
  *,
63
63
  protocol: Protocol | None = None,
64
64
  reconnect_on_failure: bool = True,
65
- **kwargs: Any,
65
+ **options: Any,
66
66
  ) -> TcpConnection:
67
67
  r"""Create a TCP connection.
68
68
 
@@ -76,7 +76,7 @@ def open_tcp_connection(
76
76
  :param reconnect_on_failure: `True` if PyPlumIO should try
77
77
  reconnecting on failure, otherwise `False`, default to `True`
78
78
  :type reconnect_on_failure: bool, optional
79
- :param \**kwargs: Additional keyword arguments to be passed to
79
+ :param \**options: Additional arguments to be passed to
80
80
  asyncio.open_connection()
81
81
  :return: An instance of TCP connection
82
82
  :rtype: TcpConnection
@@ -86,7 +86,7 @@ def open_tcp_connection(
86
86
  port,
87
87
  protocol=protocol,
88
88
  reconnect_on_failure=reconnect_on_failure,
89
- **kwargs,
89
+ **options,
90
90
  )
91
91
 
92
92
 
pyplumio/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.5.55'
32
- __version_tuple__ = version_tuple = (0, 5, 55)
31
+ __version__ = version = '0.6.0'
32
+ __version_tuple__ = version_tuple = (0, 6, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
pyplumio/connection.py CHANGED
@@ -4,11 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  from abc import ABC, abstractmethod
6
6
  import asyncio
7
+ from collections.abc import AsyncGenerator
8
+ from contextlib import asynccontextmanager
7
9
  import logging
8
10
  from typing import Any, Final
9
11
 
10
12
  from serial import EIGHTBITS, PARITY_NONE, STOPBITS_ONE
11
13
 
14
+ from pyplumio.devices import PhysicalDevice
12
15
  from pyplumio.exceptions import ConnectionFailedError
13
16
  from pyplumio.helpers.task_manager import TaskManager
14
17
  from pyplumio.protocol import AsyncProtocol, Protocol
@@ -33,15 +36,17 @@ class Connection(ABC, TaskManager):
33
36
  All specific connection classes MUST be inherited from this class.
34
37
  """
35
38
 
39
+ __slots__ = ("_protocol", "_reconnect_on_failure", "_options")
40
+
36
41
  _protocol: Protocol
37
42
  _reconnect_on_failure: bool
38
- _kwargs: dict[str, Any]
43
+ _options: dict[str, Any]
39
44
 
40
45
  def __init__(
41
46
  self,
42
47
  protocol: Protocol | None = None,
43
48
  reconnect_on_failure: bool = True,
44
- **kwargs: Any,
49
+ **options: Any,
45
50
  ) -> None:
46
51
  """Initialize a new connection."""
47
52
  super().__init__()
@@ -53,7 +58,7 @@ class Connection(ABC, TaskManager):
53
58
 
54
59
  self._reconnect_on_failure = reconnect_on_failure
55
60
  self._protocol = protocol
56
- self._kwargs = kwargs
61
+ self._options = options
57
62
 
58
63
  async def __aenter__(self) -> Connection:
59
64
  """Provide an entry point for the context manager."""
@@ -101,11 +106,62 @@ class Connection(ABC, TaskManager):
101
106
  self.cancel_tasks()
102
107
  await self.protocol.shutdown()
103
108
 
109
+ @asynccontextmanager
110
+ async def device(
111
+ self, name: str, timeout: float | None = None
112
+ ) -> AsyncGenerator[PhysicalDevice]:
113
+ """Get the device in context manager."""
114
+ if not isinstance(self.protocol, AsyncProtocol):
115
+ raise NotImplementedError
116
+
117
+ yield await self.protocol.get(name, timeout=timeout)
118
+
119
+ @property
120
+ def get(self): # type: ignore[no-untyped-def]
121
+ """Access the remote device.
122
+
123
+ Raise NotImplementedError when using protocol
124
+ different from AsyncProtocol.
125
+ """
126
+ if isinstance(self.protocol, AsyncProtocol):
127
+ return self.protocol.get
128
+
129
+ raise NotImplementedError
130
+
131
+ @property
132
+ def get_nowait(self): # type: ignore[no-untyped-def]
133
+ """Access the remote device without waiting.
134
+
135
+ Raise NotImplementedError when using protocol
136
+ different from AsyncProtocol.
137
+ """
138
+ if isinstance(self.protocol, AsyncProtocol):
139
+ return self.protocol.get_nowait
140
+
141
+ raise NotImplementedError
142
+
143
+ @property
144
+ def wait_for(self): # type: ignore[no-untyped-def]
145
+ """Wait for the remote device to become available.
146
+
147
+ Raise NotImplementedError when using protocol
148
+ different from AsyncProtocol.
149
+ """
150
+ if isinstance(self.protocol, AsyncProtocol):
151
+ return self.protocol.wait_for
152
+
153
+ raise NotImplementedError
154
+
104
155
  @property
105
156
  def protocol(self) -> Protocol:
106
157
  """Return the protocol object."""
107
158
  return self._protocol
108
159
 
160
+ @property
161
+ def options(self) -> dict[str, Any]:
162
+ """Return connection options."""
163
+ return self._options
164
+
109
165
  @timeout(CONNECT_TIMEOUT)
110
166
  @abstractmethod
111
167
  async def _open_connection(
@@ -117,6 +173,8 @@ class Connection(ABC, TaskManager):
117
173
  class TcpConnection(Connection):
118
174
  """Represents a TCP connection."""
119
175
 
176
+ __slots__ = ("host", "port")
177
+
120
178
  host: str
121
179
  port: int
122
180
 
@@ -127,17 +185,17 @@ class TcpConnection(Connection):
127
185
  *,
128
186
  protocol: Protocol | None = None,
129
187
  reconnect_on_failure: bool = True,
130
- **kwargs: Any,
188
+ **options: Any,
131
189
  ) -> None:
132
190
  """Initialize a new TCP connection."""
133
- super().__init__(protocol, reconnect_on_failure, **kwargs)
191
+ super().__init__(protocol, reconnect_on_failure, **options)
134
192
  self.host = host
135
193
  self.port = port
136
194
 
137
195
  def __repr__(self) -> str:
138
196
  """Return a serializable string representation."""
139
197
  return (
140
- f"TcpConnection(host={self.host}, port={self.port}, kwargs={self._kwargs})"
198
+ f"TcpConnection(host={self.host}, port={self.port}, options={self.options})"
141
199
  )
142
200
 
143
201
  @timeout(CONNECT_TIMEOUT)
@@ -146,37 +204,39 @@ class TcpConnection(Connection):
146
204
  ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
147
205
  """Open the connection and return reader and writer objects."""
148
206
  return await asyncio.open_connection(
149
- host=self.host, port=self.port, **self._kwargs
207
+ host=self.host, port=self.port, **self.options
150
208
  )
151
209
 
152
210
 
153
211
  class SerialConnection(Connection):
154
212
  """Represents a serial connection."""
155
213
 
156
- device: str
214
+ __slots__ = ("url", "baudrate")
215
+
216
+ url: str
157
217
  baudrate: int
158
218
 
159
219
  def __init__(
160
220
  self,
161
- device: str,
221
+ url: str,
162
222
  baudrate: int = 115200,
163
223
  *,
164
224
  protocol: Protocol | None = None,
165
225
  reconnect_on_failure: bool = True,
166
- **kwargs: Any,
226
+ **options: Any,
167
227
  ) -> None:
168
228
  """Initialize a new serial connection."""
169
- super().__init__(protocol, reconnect_on_failure, **kwargs)
170
- self.device = device
229
+ super().__init__(protocol, reconnect_on_failure, **options)
230
+ self.url = url
171
231
  self.baudrate = baudrate
172
232
 
173
233
  def __repr__(self) -> str:
174
234
  """Return a serializable string representation."""
175
235
  return (
176
236
  "SerialConnection("
177
- f"device={self.device}, "
237
+ f"url={self.url}, "
178
238
  f"baudrate={self.baudrate}, "
179
- f"kwargs={self._kwargs})"
239
+ f"options={self.options})"
180
240
  )
181
241
 
182
242
  @timeout(CONNECT_TIMEOUT)
@@ -185,12 +245,12 @@ class SerialConnection(Connection):
185
245
  ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
186
246
  """Open the connection and return reader and writer objects."""
187
247
  return await pyserial_asyncio.open_serial_connection(
188
- url=self.device,
248
+ url=self.url,
189
249
  baudrate=self.baudrate,
190
250
  bytesize=EIGHTBITS,
191
251
  parity=PARITY_NONE,
192
252
  stopbits=STOPBITS_ONE,
193
- **self._kwargs,
253
+ **self.options,
194
254
  )
195
255
 
196
256
 
pyplumio/const.py CHANGED
@@ -3,9 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from enum import Enum, IntEnum, unique
6
- from typing import Any, Final, Literal
7
-
8
- from typing_extensions import TypeAlias
6
+ from typing import Any, Final, Literal, TypeAlias
9
7
 
10
8
  # General attributes.
11
9
  ATTR_CONNECTED: Final = "connected"
pyplumio/filters.py CHANGED
@@ -15,13 +15,12 @@ from typing import (
15
15
  Final,
16
16
  Protocol,
17
17
  SupportsFloat,
18
+ TypeAlias,
18
19
  TypeVar,
19
20
  overload,
20
21
  runtime_checkable,
21
22
  )
22
23
 
23
- from typing_extensions import TypeAlias
24
-
25
24
  from pyplumio.helpers.event_manager import Callback
26
25
  from pyplumio.parameters import Parameter
27
26
 
@@ -176,7 +175,7 @@ class _Aggregate(Filter):
176
175
 
177
176
  async def __call__(self, new_value: Any) -> Any:
178
177
  """Set a new value for the callback."""
179
- if not isinstance(new_value, (float, int, Decimal)):
178
+ if not isinstance(new_value, float | int | Decimal):
180
179
  raise TypeError(
181
180
  "Aggregate filter can only be used with numeric values, got "
182
181
  f"{type(new_value).__name__}: {new_value}"
@@ -187,7 +186,7 @@ class _Aggregate(Filter):
187
186
  time_since_call = current_time - self._last_call_time
188
187
  if time_since_call >= self._timeout or len(self._values) >= self._sample_size:
189
188
  sum_of_values = (
190
- np.sum(self._values) if numpy_installed else sum(self._values)
189
+ np.sum(np.array(self._values)) if numpy_installed else sum(self._values)
191
190
  )
192
191
  result = await self._callback(float(sum_of_values))
193
192
  self._last_call_time = current_time
@@ -326,7 +325,7 @@ class _Deadband(Filter):
326
325
 
327
326
  async def __call__(self, new_value: Any) -> Any:
328
327
  """Set a new value for the callback."""
329
- if not isinstance(new_value, (float, int, Decimal)):
328
+ if not isinstance(new_value, float | int | Decimal):
330
329
  raise TypeError(
331
330
  "Deadband filter can only be used with numeric values, got "
332
331
  f"{type(new_value).__name__}: {new_value}"
@@ -60,12 +60,10 @@ def get_frame_handler(frame_type: int) -> str:
60
60
  return f"frames.{module.lower()}s.{type_name}{module.capitalize()}"
61
61
 
62
62
 
63
- @dataclass
63
+ @dataclass(slots=True)
64
64
  class DataFrameDescription:
65
65
  """Describes what data is provided by the frame."""
66
66
 
67
- __slots__ = ("frame_type", "provides")
68
-
69
67
  frame_type: FrameType
70
68
  provides: str
71
69
 
@@ -95,6 +93,8 @@ class Frame(ABC):
95
93
  _message: bytearray | None
96
94
  _data: dict[str, Any] | None
97
95
 
96
+ __hash__ = object.__hash__
97
+
98
98
  def __init__(
99
99
  self,
100
100
  recipient: DeviceType = DeviceType.ALL,
@@ -114,19 +114,6 @@ class Frame(ABC):
114
114
  self._data = data if not kwargs else ensure_dict(data, kwargs)
115
115
  self._message = message
116
116
 
117
- def __hash__(self) -> int:
118
- """Return a hash of the frame based on its values."""
119
- return hash(
120
- (
121
- self.recipient,
122
- self.sender,
123
- self.econet_type,
124
- self.econet_version,
125
- self._message,
126
- frozenset(self._data.items()) if self._data else None,
127
- )
128
- )
129
-
130
117
  def __eq__(self, other: object) -> bool:
131
118
  """Compare if this frame is equal to other."""
132
119
  if isinstance(other, Frame):
@@ -1,10 +1,8 @@
1
1
  """Contains a simple async cache for caching results of async functions."""
2
2
 
3
- from collections.abc import Awaitable
3
+ from collections.abc import Awaitable, Callable
4
4
  from functools import wraps
5
- from typing import Any, Callable, TypeVar, cast
6
-
7
- from typing_extensions import ParamSpec, TypeAlias
5
+ from typing import Any, ParamSpec, TypeAlias, TypeVar, cast
8
6
 
9
7
  T = TypeVar("T")
10
8
  P = ParamSpec("P")
@@ -5,9 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  from collections.abc import Callable, Coroutine, Generator
7
7
  import inspect
8
- from typing import Any, Generic, TypeVar, overload
9
-
10
- from typing_extensions import TypeAlias
8
+ from typing import Any, Generic, TypeAlias, TypeVar, overload
11
9
 
12
10
  from pyplumio.helpers.task_manager import TaskManager
13
11
 
@@ -7,10 +7,7 @@ import asyncio
7
7
  from contextlib import suppress
8
8
  from dataclasses import asdict, dataclass
9
9
  import logging
10
- from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, get_args
11
-
12
- from dataslots import dataslots
13
- from typing_extensions import TypeAlias
10
+ from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar, get_args
14
11
 
15
12
  from pyplumio.const import BYTE_UNDEFINED, STATE_OFF, STATE_ON, State, UnitOfMeasurement
16
13
  from pyplumio.frames import Request
@@ -47,19 +44,16 @@ def is_valid_parameter(data: bytearray) -> bool:
47
44
  return any(x for x in data if x != BYTE_UNDEFINED)
48
45
 
49
46
 
50
- @dataclass
47
+ @dataclass(slots=True)
51
48
  class ParameterValues:
52
49
  """Represents a parameter values."""
53
50
 
54
- __slots__ = ("value", "min_value", "max_value")
55
-
56
51
  value: int
57
52
  min_value: int
58
53
  max_value: int
59
54
 
60
55
 
61
- @dataslots
62
- @dataclass
56
+ @dataclass(slots=True)
63
57
  class ParameterDescription:
64
58
  """Represents a parameter description."""
65
59
 
@@ -67,7 +61,7 @@ class ParameterDescription:
67
61
  optimistic: bool = False
68
62
 
69
63
 
70
- NumericType: TypeAlias = Union[int, float]
64
+ NumericType: TypeAlias = int | float
71
65
 
72
66
 
73
67
  class Parameter(ABC):
@@ -128,7 +122,7 @@ class Parameter(ABC):
128
122
  handler = getattr(self.values.value, method_to_call)
129
123
  return handler(other.value)
130
124
 
131
- if isinstance(other, (int, float, bool)) or other in get_args(State):
125
+ if isinstance(other, int | float | bool) or other in get_args(State):
132
126
  handler = getattr(self.values.value, method_to_call)
133
127
  return handler(self._pack_value(other))
134
128
  else:
@@ -317,8 +311,7 @@ class Parameter(ABC):
317
311
  """Create a request to change the parameter."""
318
312
 
319
313
 
320
- @dataslots
321
- @dataclass
314
+ @dataclass(slots=True)
322
315
  class NumberDescription(ParameterDescription):
323
316
  """Represents a parameter description."""
324
317
 
@@ -395,8 +388,7 @@ class Number(Parameter):
395
388
  return self.description.unit_of_measurement
396
389
 
397
390
 
398
- @dataslots
399
- @dataclass
391
+ @dataclass(slots=True)
400
392
  class OffsetNumberDescription(NumberDescription):
401
393
  """Represents a parameter description."""
402
394
 
@@ -419,8 +411,7 @@ class OffsetNumber(Number):
419
411
  return super()._unpack_value(value - self.description.offset)
420
412
 
421
413
 
422
- @dataslots
423
- @dataclass
414
+ @dataclass(slots=True)
424
415
  class SwitchDescription(ParameterDescription):
425
416
  """Represents a switch description."""
426
417
 
@@ -15,22 +15,18 @@ from pyplumio.utils import to_camelcase
15
15
  _LOGGER = logging.getLogger(__name__)
16
16
 
17
17
 
18
- @dataclass
18
+ @dataclass(slots=True, kw_only=True)
19
19
  class Signature:
20
20
  """Represents a product signature."""
21
21
 
22
- __slots__ = ("id", "model")
23
-
24
22
  id: int
25
23
  model: str
26
24
 
27
25
 
28
- @dataclass
26
+ @dataclass(slots=True, kw_only=True)
29
27
  class CustomParameter:
30
28
  """Represents a custom parameter."""
31
29
 
32
- __slots__ = ("original", "replacement")
33
-
34
30
  original: str
35
31
  replacement: ParameterDescription
36
32
 
@@ -6,8 +6,6 @@ from dataclasses import dataclass
6
6
  from functools import partial
7
7
  from typing import TYPE_CHECKING
8
8
 
9
- from dataslots import dataslots
10
-
11
9
  from pyplumio.const import (
12
10
  ATTR_INDEX,
13
11
  ATTR_OFFSET,
@@ -78,12 +76,10 @@ class EcomaxParameter(Parameter):
78
76
  )
79
77
 
80
78
 
81
- @dataclass
79
+ @dataclass(slots=True)
82
80
  class EcomaxNumberDescription(EcomaxParameterDescription, OffsetNumberDescription):
83
81
  """Represents an ecoMAX number description."""
84
82
 
85
- __slots__ = ()
86
-
87
83
 
88
84
  class EcomaxNumber(EcomaxParameter, OffsetNumber):
89
85
  """Represents a ecoMAX number."""
@@ -93,8 +89,7 @@ class EcomaxNumber(EcomaxParameter, OffsetNumber):
93
89
  description: EcomaxNumberDescription
94
90
 
95
91
 
96
- @dataslots
97
- @dataclass
92
+ @dataclass(slots=True)
98
93
  class EcomaxSwitchDescription(EcomaxParameterDescription, SwitchDescription):
99
94
  """Represents an ecoMAX switch description."""
100
95
 
@@ -6,8 +6,6 @@ from dataclasses import dataclass
6
6
  from functools import cache
7
7
  from typing import TYPE_CHECKING
8
8
 
9
- from dataslots import dataslots
10
-
11
9
  from pyplumio.const import (
12
10
  ATTR_DEVICE_INDEX,
13
11
  ATTR_INDEX,
@@ -59,12 +57,10 @@ class MixerParameter(Parameter):
59
57
  )
60
58
 
61
59
 
62
- @dataclass
60
+ @dataclass(slots=True)
63
61
  class MixerNumberDescription(MixerParameterDescription, OffsetNumberDescription):
64
62
  """Represent a mixer number description."""
65
63
 
66
- __slots__ = ()
67
-
68
64
 
69
65
  class MixerNumber(MixerParameter, OffsetNumber):
70
66
  """Represents a mixer number."""
@@ -74,8 +70,7 @@ class MixerNumber(MixerParameter, OffsetNumber):
74
70
  description: MixerNumberDescription
75
71
 
76
72
 
77
- @dataslots
78
- @dataclass
73
+ @dataclass(slots=True)
79
74
  class MixerSwitchDescription(MixerParameterDescription, SwitchDescription):
80
75
  """Represents a mixer switch description."""
81
76
 
@@ -6,8 +6,6 @@ from dataclasses import dataclass
6
6
  from functools import cache
7
7
  from typing import TYPE_CHECKING
8
8
 
9
- from dataslots import dataslots
10
-
11
9
  from pyplumio.const import (
12
10
  ATTR_INDEX,
13
11
  ATTR_OFFSET,
@@ -77,8 +75,7 @@ class ThermostatParameter(Parameter):
77
75
  )
78
76
 
79
77
 
80
- @dataslots
81
- @dataclass
78
+ @dataclass(slots=True)
82
79
  class ThermostatNumberDescription(ThermostatParameterDescription, NumberDescription):
83
80
  """Represent a thermostat number description."""
84
81
 
@@ -91,8 +88,7 @@ class ThermostatNumber(ThermostatParameter, Number):
91
88
  description: ThermostatNumberDescription
92
89
 
93
90
 
94
- @dataslots
95
- @dataclass
91
+ @dataclass(slots=True)
96
92
  class ThermostatSwitchDescription(ThermostatParameterDescription, SwitchDescription):
97
93
  """Represents a thermostat switch description."""
98
94
 
pyplumio/protocol.py CHANGED
@@ -5,10 +5,10 @@ from __future__ import annotations
5
5
  from abc import ABC, abstractmethod
6
6
  import asyncio
7
7
  from collections.abc import Awaitable, Callable
8
- from dataclasses import dataclass
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime
9
10
  import logging
10
-
11
- from typing_extensions import TypeAlias
11
+ from typing import Any, Final, Literal, TypeAlias
12
12
 
13
13
  from pyplumio.const import ATTR_CONNECTED, ATTR_SETUP, DeviceType
14
14
  from pyplumio.devices import PhysicalDevice
@@ -23,6 +23,7 @@ from pyplumio.structures.network_info import (
23
23
  NetworkInfo,
24
24
  WirelessParameters,
25
25
  )
26
+ from pyplumio.structures.regulator_data import ATTR_REGDATA
26
27
 
27
28
  _LOGGER = logging.getLogger(__name__)
28
29
 
@@ -100,12 +101,10 @@ class DummyProtocol(Protocol):
100
101
  await self.close_writer()
101
102
 
102
103
 
103
- @dataclass
104
+ @dataclass(slots=True)
104
105
  class Queues:
105
106
  """Represents asyncio queues."""
106
107
 
107
- __slots__ = ("read", "write")
108
-
109
108
  read: asyncio.Queue[Frame]
110
109
  write: asyncio.Queue[Frame]
111
110
 
@@ -114,6 +113,84 @@ class Queues:
114
113
  await asyncio.gather(self.read.join(), self.write.join())
115
114
 
116
115
 
116
+ NEVER: Final = "never"
117
+
118
+
119
+ @dataclass(slots=True, kw_only=True)
120
+ class Statistics:
121
+ """Represents a connection statistics."""
122
+
123
+ #: Number of received bytes. Resets on reconnect.
124
+ received_bytes: int = 0
125
+
126
+ #: Number of received frames. Resets on reconnect.
127
+ received_frames: int = 0
128
+
129
+ #: Number of sent bytes. Resets on reconnect.
130
+ sent_bytes: int = 0
131
+
132
+ #: Number of sent frames. Resets on reconnect.
133
+ sent_frames: int = 0
134
+
135
+ #: Number of failed frames. Resets on reconnect.
136
+ failed_frames: int = 0
137
+
138
+ #: Datetime object representing connection time
139
+ connected_since: datetime | Literal["never"] = NEVER
140
+
141
+ #: Datetime object representing last connection loss event
142
+ connection_loss_at: datetime | Literal["never"] = NEVER
143
+
144
+ #: Number of connection lost event
145
+ connection_losses: int = 0
146
+
147
+ #: List of statistics for connected devices
148
+ devices: list[DeviceStatistics] = field(default_factory=list)
149
+
150
+ def update_transfer_statistics(
151
+ self, sent: Frame | None = None, received: Frame | None = None
152
+ ) -> None:
153
+ """Update transfer statistics."""
154
+ if sent:
155
+ self.sent_bytes += sent.length
156
+ self.sent_frames += 1
157
+
158
+ if received:
159
+ self.received_bytes += received.length
160
+ self.received_frames += 1
161
+
162
+ def track_connection_loss(self) -> None:
163
+ """Increase connection loss counter and store the datetime."""
164
+ self.connection_losses += 1
165
+ self.connection_loss_at = datetime.now()
166
+
167
+ def reset_transfer_statistics(self) -> None:
168
+ """Reset transfer statistics."""
169
+ self.sent_bytes = 0
170
+ self.sent_frames = 0
171
+ self.received_bytes = 0
172
+ self.received_frames = 0
173
+ self.failed_frames = 0
174
+
175
+
176
+ @dataclass(slots=True)
177
+ class DeviceStatistics:
178
+ """Represents a device statistics."""
179
+
180
+ #: Device name
181
+ name: str
182
+
183
+ #: Datetime object representing connection time
184
+ connected_since: datetime | Literal["never"] = NEVER
185
+
186
+ #: Datetime object representing time when device was last seen
187
+ last_seen: datetime | Literal["never"] = NEVER
188
+
189
+ async def update_last_seen(self, _: Any) -> None:
190
+ """Update last seen property."""
191
+ self.last_seen = datetime.now()
192
+
193
+
117
194
  class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
118
195
  """Represents an async protocol.
119
196
 
@@ -134,6 +211,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
134
211
  _network: NetworkInfo
135
212
  _queues: Queues
136
213
  _entry_lock: asyncio.Lock
214
+ _statistics: Statistics
137
215
 
138
216
  def __init__(
139
217
  self,
@@ -150,6 +228,7 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
150
228
  )
151
229
  self._queues = Queues(read=asyncio.Queue(), write=asyncio.Queue())
152
230
  self._entry_lock = asyncio.Lock()
231
+ self._statistics = Statistics()
153
232
 
154
233
  def connection_established(
155
234
  self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
@@ -172,6 +251,8 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
172
251
  device.dispatch_nowait(ATTR_CONNECTED, True)
173
252
 
174
253
  self.connected.set()
254
+ self.statistics.reset_transfer_statistics()
255
+ self.statistics.connected_since = datetime.now()
175
256
 
176
257
  async def _connection_close(self) -> None:
177
258
  """Close the connection if it is established."""
@@ -200,19 +281,26 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
200
281
  self, queues: Queues, reader: FrameReader, writer: FrameWriter
201
282
  ) -> None:
202
283
  """Handle frame reads and writes."""
284
+ statistics = self.statistics
203
285
  await self.connected.wait()
204
286
  while self.connected.is_set():
205
287
  try:
288
+ request = None
206
289
  if not queues.write.empty():
207
- await writer.write(await queues.write.get())
290
+ request = await queues.write.get()
291
+ await writer.write(request)
208
292
  queues.write.task_done()
209
293
 
210
294
  if response := await reader.read():
211
295
  queues.read.put_nowait(response)
212
296
 
297
+ statistics.update_transfer_statistics(request, response)
298
+
213
299
  except ProtocolError as e:
300
+ statistics.failed_frames += 1
214
301
  _LOGGER.debug("Can't process received frame: %s", e)
215
302
  except (OSError, asyncio.TimeoutError):
303
+ statistics.track_connection_loss()
216
304
  self.create_task(self.connection_lost())
217
305
  break
218
306
  except Exception:
@@ -239,8 +327,21 @@ class AsyncProtocol(Protocol, EventManager[PhysicalDevice]):
239
327
  device.dispatch_nowait(ATTR_CONNECTED, True)
240
328
  device.dispatch_nowait(ATTR_SETUP, True)
241
329
  await self.dispatch(name, device)
330
+ self.statistics.devices.append(
331
+ device_statistics := DeviceStatistics(
332
+ name=name,
333
+ connected_since=datetime.now(),
334
+ last_seen=datetime.now(),
335
+ )
336
+ )
337
+ device.subscribe(ATTR_REGDATA, device_statistics.update_last_seen)
242
338
 
243
339
  return self.data[name]
244
340
 
341
+ @property
342
+ def statistics(self) -> Statistics:
343
+ """Return the statistics."""
344
+ return self._statistics
345
+
245
346
 
246
- __all__ = ["Protocol", "DummyProtocol", "AsyncProtocol"]
347
+ __all__ = ["Protocol", "DummyProtocol", "AsyncProtocol", "Statistics"]
@@ -9,12 +9,10 @@ from typing import Any
9
9
  from pyplumio.frames import Frame
10
10
 
11
11
 
12
- @dataclass
12
+ @dataclass(slots=True)
13
13
  class StructureDataClass:
14
14
  """Represents a structure dataclass mixin."""
15
15
 
16
- __slots__ = ("frame",)
17
-
18
16
  frame: Frame
19
17
 
20
18
 
@@ -56,12 +56,10 @@ def seconds_to_datetime(timestamp: int) -> datetime:
56
56
  return datetime(**dict(datetime_kwargs(timestamp)))
57
57
 
58
58
 
59
- @dataclass
59
+ @dataclass(slots=True)
60
60
  class Alert:
61
61
  """Represents a device alert."""
62
62
 
63
- __slots__ = ("code", "from_dt", "to_dt")
64
-
65
63
  code: int
66
64
  from_dt: datetime
67
65
  to_dt: datetime | None
@@ -6,8 +6,6 @@ from dataclasses import dataclass
6
6
  import struct
7
7
  from typing import Any, Final
8
8
 
9
- from dataslots import dataslots
10
-
11
9
  from pyplumio.const import BYTE_UNDEFINED
12
10
  from pyplumio.structures import StructureDecoder
13
11
  from pyplumio.utils import ensure_dict
@@ -32,8 +30,7 @@ struct_version = struct.Struct("<BBB")
32
30
  struct_vendor = struct.Struct("<BB")
33
31
 
34
32
 
35
- @dataslots
36
- @dataclass
33
+ @dataclass(slots=True)
37
34
  class ConnectedModules:
38
35
  """Represents a firmware version info for connected module."""
39
36
 
@@ -18,7 +18,7 @@ DEFAULT_NETMASK: Final = "255.255.255.0"
18
18
  NETWORK_INFO_SIZE: Final = 25
19
19
 
20
20
 
21
- @dataclass(frozen=True)
21
+ @dataclass(frozen=True, slots=True, kw_only=True)
22
22
  class EthernetParameters:
23
23
  """Represents an ethernet parameters."""
24
24
 
@@ -35,7 +35,7 @@ class EthernetParameters:
35
35
  status: bool = True
36
36
 
37
37
 
38
- @dataclass(frozen=True)
38
+ @dataclass(frozen=True, slots=True, kw_only=True)
39
39
  class WirelessParameters(EthernetParameters):
40
40
  """Represents a wireless network parameters."""
41
41
 
@@ -50,7 +50,7 @@ class WirelessParameters(EthernetParameters):
50
50
  signal_quality: int = 100
51
51
 
52
52
 
53
- @dataclass(frozen=True)
53
+ @dataclass(frozen=True, slots=True, kw_only=True)
54
54
  class NetworkInfo:
55
55
  """Represents a network parameters."""
56
56
 
@@ -63,12 +63,10 @@ def format_model_name(model_name: str) -> str:
63
63
  return model_name
64
64
 
65
65
 
66
- @dataclass(frozen=True)
66
+ @dataclass(frozen=True, slots=True)
67
67
  class ProductInfo:
68
68
  """Represents a product info provided by an UID response."""
69
69
 
70
- __slots__ = ("type", "id", "uid", "logo", "image", "model")
71
-
72
70
  type: ProductType
73
71
  id: int
74
72
  uid: str
@@ -6,8 +6,6 @@ from dataclasses import dataclass
6
6
  import struct
7
7
  from typing import Any, Final
8
8
 
9
- from dataslots import dataslots
10
-
11
9
  from pyplumio._version import __version_tuple__
12
10
  from pyplumio.structures import Structure
13
11
  from pyplumio.utils import ensure_dict
@@ -21,8 +19,7 @@ SOFTWARE_VERSION: Final = ".".join(str(x) for x in __version_tuple__[0:3])
21
19
  struct_program_version = struct.Struct("<2sB2s3s3HB")
22
20
 
23
21
 
24
- @dataslots
25
- @dataclass
22
+ @dataclass(slots=True)
26
23
  class VersionInfo:
27
24
  """Represents a version info provided in program version response."""
28
25
 
@@ -8,8 +8,6 @@ import datetime as dt
8
8
  from functools import lru_cache, reduce
9
9
  from typing import Annotated, Any, Final, get_args
10
10
 
11
- from dataslots import dataslots
12
-
13
11
  from pyplumio.const import (
14
12
  ATTR_PARAMETER,
15
13
  ATTR_SCHEDULE,
@@ -91,8 +89,6 @@ SCHEDULES: tuple[str, ...] = (
91
89
  class ScheduleParameterDescription(ParameterDescription):
92
90
  """Represent a schedule parameter description."""
93
91
 
94
- __slots__ = ()
95
-
96
92
 
97
93
  class ScheduleParameter(Parameter):
98
94
  """Represents a schedule parameter."""
@@ -112,8 +108,7 @@ class ScheduleParameter(Parameter):
112
108
  )
113
109
 
114
110
 
115
- @dataslots
116
- @dataclass
111
+ @dataclass(slots=True)
117
112
  class ScheduleNumberDescription(ScheduleParameterDescription, NumberDescription):
118
113
  """Represents a schedule number description."""
119
114
 
@@ -126,8 +121,7 @@ class ScheduleNumber(ScheduleParameter, Number):
126
121
  description: ScheduleNumberDescription
127
122
 
128
123
 
129
- @dataslots
130
- @dataclass
124
+ @dataclass(slots=True)
131
125
  class ScheduleSwitchDescription(ScheduleParameterDescription, SwitchDescription):
132
126
  """Represents a schedule switch description."""
133
127
 
@@ -273,22 +267,10 @@ class ScheduleDay(MutableMapping):
273
267
  return cls({get_time(index): state for index, state in enumerate(intervals)})
274
268
 
275
269
 
276
- @dataclass
270
+ @dataclass(slots=True)
277
271
  class Schedule(Iterable):
278
272
  """Represents a weekly schedule."""
279
273
 
280
- __slots__ = (
281
- "name",
282
- "device",
283
- "sunday",
284
- "monday",
285
- "tuesday",
286
- "wednesday",
287
- "thursday",
288
- "friday",
289
- "saturday",
290
- )
291
-
292
274
  name: str
293
275
  device: PhysicalDevice
294
276
 
pyplumio/utils.py CHANGED
@@ -5,9 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  from collections.abc import Awaitable, Callable, Mapping
7
7
  from functools import wraps
8
- from typing import TypeVar
9
-
10
- from typing_extensions import ParamSpec
8
+ from typing import ParamSpec, TypeVar
11
9
 
12
10
  KT = TypeVar("KT") # Key type.
13
11
  VT = TypeVar("VT") # Value type.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyPlumIO
3
- Version: 0.5.55
3
+ Version: 0.6.0
4
4
  Summary: PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
5
5
  Author-email: Denis Paavilainen <denpa@denpa.pro>
6
6
  License: MIT License
@@ -13,30 +13,27 @@ Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Operating System :: OS Independent
16
- Classifier: Programming Language :: Python :: 3.9
17
16
  Classifier: Programming Language :: Python :: 3.10
18
17
  Classifier: Programming Language :: Python :: 3.11
19
18
  Classifier: Programming Language :: Python :: 3.12
20
19
  Classifier: Programming Language :: Python :: 3.13
21
20
  Classifier: Topic :: Software Development :: Libraries
22
21
  Classifier: Topic :: Home Automation
23
- Requires-Python: >=3.9
22
+ Requires-Python: >=3.10
24
23
  Description-Content-Type: text/markdown
25
24
  License-File: LICENSE
26
- Requires-Dist: dataslots==1.2.0
27
25
  Requires-Dist: pyserial-asyncio==0.6
28
- Requires-Dist: typing-extensions<5.0,>=4.14.0
29
26
  Provides-Extra: test
30
27
  Requires-Dist: codespell==2.4.1; extra == "test"
31
- Requires-Dist: coverage==7.10.5; extra == "test"
28
+ Requires-Dist: coverage==7.10.6; extra == "test"
32
29
  Requires-Dist: freezegun==1.5.5; extra == "test"
33
- Requires-Dist: mypy==1.17.1; extra == "test"
30
+ Requires-Dist: mypy==1.18.1; extra == "test"
34
31
  Requires-Dist: numpy<3.0.0,>=2.0.0; extra == "test"
35
32
  Requires-Dist: pyserial-asyncio-fast==0.16; extra == "test"
36
- Requires-Dist: pytest==8.4.1; extra == "test"
33
+ Requires-Dist: pytest==8.4.2; extra == "test"
37
34
  Requires-Dist: pytest-asyncio==1.1.0; extra == "test"
38
- Requires-Dist: ruff==0.12.10; extra == "test"
39
- Requires-Dist: tox==4.28.4; extra == "test"
35
+ Requires-Dist: ruff==0.13.0; extra == "test"
36
+ Requires-Dist: tox==4.30.2; extra == "test"
40
37
  Requires-Dist: types-pyserial==3.5.0.20250822; extra == "test"
41
38
  Provides-Extra: docs
42
39
  Requires-Dist: sphinx==8.1.3; extra == "docs"
@@ -53,8 +50,8 @@ Dynamic: license-file
53
50
  [![PyPI version](https://badge.fury.io/py/PyPlumIO.svg)](https://badge.fury.io/py/PyPlumIO)
54
51
  [![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/pyplumio.svg)](https://pypi.python.org/pypi/pyplumio/)
55
52
  [![PyPlumIO CI](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml/badge.svg)](https://github.com/denpamusic/PyPlumIO/actions/workflows/ci.yml)
56
- [![Maintainability](https://qlty.sh/badges/2455933c-6c18-45bc-a205-da5cc0b49d0b/maintainability.svg)](https://qlty.sh/gh/denpamusic/projects/PyPlumIO)
57
- [![Code Coverage](https://qlty.sh/badges/2455933c-6c18-45bc-a205-da5cc0b49d0b/test_coverage.svg)](https://qlty.sh/gh/denpamusic/projects/PyPlumIO)
53
+ [![Maintainability](https://qlty.sh/gh/denpamusic/projects/PyPlumIO/maintainability.svg)](https://qlty.sh/gh/denpamusic/projects/PyPlumIO)
54
+ [![Code Coverage](https://qlty.sh/gh/denpamusic/projects/PyPlumIO/coverage.svg)](https://qlty.sh/gh/denpamusic/projects/PyPlumIO)
58
55
  [![stability-release-candidate](https://img.shields.io/badge/stability-pre--release-48c9b0.svg)](https://guidelines.denpa.pro/stability#release-candidate)
59
56
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
60
57
 
@@ -82,11 +79,13 @@ through network by using RS-485 to Ethernet/WiFi converter.
82
79
  - [Callbacks](https://pyplumio.denpa.pro/callbacks.html)
83
80
  - [Mixers/Thermostats](https://pyplumio.denpa.pro/mixers_thermostats.html)
84
81
  - [Schedules](https://pyplumio.denpa.pro/schedules.html)
82
+ - [Statistics](https://pyplumio.denpa.pro/statistics.html)
85
83
  - [Protocol](https://pyplumio.denpa.pro/protocol.html)
86
84
  - [Frame Structure](https://pyplumio.denpa.pro/protocol.html#frame-structure)
87
85
  - [Requests and Responses](https://pyplumio.denpa.pro/protocol.html#requests-and-responses)
88
86
  - [Communication](https://pyplumio.denpa.pro/protocol.html#communication)
89
87
  - [Versioning](https://pyplumio.denpa.pro/protocol.html#versioning)
88
+ - [Supported frames](https://pyplumio.denpa.pro/frames.html)
90
89
 
91
90
  ## Quickstart
92
91
 
@@ -1,37 +1,37 @@
1
- pyplumio/__init__.py,sha256=3H5SO4WFw5mBTFeEyD4w0H8-MsNo93NyOH3RyMN7IS0,3337
1
+ pyplumio/__init__.py,sha256=DQg-ZTAxLYuNKyqsGrcO0QrVAMw9aPA69Bv2mZ7ubXQ,3314
2
2
  pyplumio/__main__.py,sha256=3IwHHSq-iay5FaeMc95klobe-xv82yydSKcBE7BFZ6M,500
3
- pyplumio/_version.py,sha256=G1TzBNDMFivoxxcx0zhCWDpZU92nh3Fwl-CwzCVn15g,706
4
- pyplumio/connection.py,sha256=u-iOzEUqoEEL4YLpLtzBWi5Qy8_RABgKD8DyXf-er-4,5892
5
- pyplumio/const.py,sha256=eoq-WNJ8TO3YlP7dC7KkVQRKGjt9FbRZ6M__s29vb1U,5659
3
+ pyplumio/_version.py,sha256=MAYWefOLb6kbIRub18WSzK6ggSjz1LNLy9aDRlX9Ea4,704
4
+ pyplumio/connection.py,sha256=9Gzg6FXMU-HjspsDnm9XH8ZPBO29AZ6dKS2-eg8P8Z0,7686
5
+ pyplumio/const.py,sha256=oYwXB3N6bvFLc6411icbABbBkSoQcj5BGuyD-NaKYp8,5629
6
6
  pyplumio/data_types.py,sha256=BTDxwErRo_odvFT5DNfIniNh8ZfyjRKEDaJmoEJqdEg,9426
7
7
  pyplumio/exceptions.py,sha256=_B_0EgxDxd2XyYv3WpZM733q0cML5m6J-f55QOvYRpI,996
8
- pyplumio/filters.py,sha256=QEtOptXym2Fb82cdPpS1dajkTpvYi3VuQaYoLl4CSQ4,15658
9
- pyplumio/protocol.py,sha256=DWM-yJnm2EQPLvGzXNlkQ0IpKQn44e-WkNB_DqZAag8,8313
8
+ pyplumio/filters.py,sha256=sBEnr0i_1XMbIwIEA24npbpe5yevSRneynlsqJMyfko,15642
9
+ pyplumio/protocol.py,sha256=jGx5b8y_1jbdFbjL_ZbUjpDvgBYhn5JUBsVsf_De6Ls,11614
10
10
  pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  pyplumio/stream.py,sha256=zFMKZ_GxsSGcaBTJigVM1CK3uGjlEJXgcvKqus8MDzk,7740
12
- pyplumio/utils.py,sha256=ktV8_Th2DiwQ0W6afOCau9kBJ8pOrqR-SM2Y2GRy-xE,1869
12
+ pyplumio/utils.py,sha256=SV47Y6QC_KL-gPmk6KQgx7ArExzNHGKuaddGAHjT9rs,1839
13
13
  pyplumio/devices/__init__.py,sha256=d0E5hTV7UPa8flq8TNlKf_jt4cOSbRigSE9jjDHrmDI,8302
14
14
  pyplumio/devices/ecomax.py,sha256=1QasnLFgNCplSoDXXe5wUr8JQjr6ChSEGijamXtJZVM,16356
15
15
  pyplumio/devices/ecoster.py,sha256=X46ky5XT8jHMFq9sBW0ve8ZI_tjItQDMt4moXsW-ogY,307
16
16
  pyplumio/devices/mixer.py,sha256=7WdUVgwO4VXmaPNzh3ZWpKr2ooRXWemz2KFHAw35_Rk,2731
17
17
  pyplumio/devices/thermostat.py,sha256=MHMKe45fQ7jKlhBVObJ7McbYQKuF6-LOKSHy-9VNsCU,2253
18
- pyplumio/frames/__init__.py,sha256=QAjdvZxQj5Av1OKFqdlSgZBn3RbCcRVMQv_lCnTkW5M,8272
18
+ pyplumio/frames/__init__.py,sha256=jIYP31yP60FVXp8ygOcKkbJCosodiqWCvnrY9FOgH4g,7885
19
19
  pyplumio/frames/messages.py,sha256=ImQGWFFTa2eaXfytQmFZKC-IxyPRkxD8qp0bEm16-ws,3628
20
20
  pyplumio/frames/requests.py,sha256=jr-_XSSCCDDTbAmrw95CKyWa5nb7JNeGzZ2jDXIxlAo,7348
21
21
  pyplumio/frames/responses.py,sha256=M6Ky4gg2AoShmRXX0x6nftajxrvmQLKPVRWbwyhvI0E,6663
22
22
  pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
23
- pyplumio/helpers/async_cache.py,sha256=EGQcU8LWJpVx3Hk6iaI-3mqAhnR5ACfBOGb9tWw-VSY,1305
24
- pyplumio/helpers/event_manager.py,sha256=aKNlhsPNTy3eOSfWVb9TJxtIsN9GAQv9XxhOi_BOhlM,8097
23
+ pyplumio/helpers/async_cache.py,sha256=PUkUTo3lmIhslejg0dGWjbcES09E62d9YYgDcBK_G6Q,1275
24
+ pyplumio/helpers/event_manager.py,sha256=cev3_X5a7rBvT4KXIwGpyAnOdWd-3svWDETN3yumkhg,8067
25
25
  pyplumio/helpers/factory.py,sha256=c3sitnkUjJWz7fPpTE9uRIpa8h46Qim3xsAblMw3eDo,1049
26
26
  pyplumio/helpers/task_manager.py,sha256=N71F6Ag1HHxdf5zJeCMcEziFdH9lmJKtMPoRGjJ-E40,1209
27
- pyplumio/parameters/__init__.py,sha256=YKIXmb_-E2HeIVljDHdD2bLbsfUsQ8sVhl9sl7kLzyo,16220
28
- pyplumio/parameters/ecomax.py,sha256=4UqI7cokCt7qS9Du4-7JgLhM7mhHCHt8SWPl_qncmXQ,26239
29
- pyplumio/parameters/mixer.py,sha256=RjhfUU_62STyNV0Ud9A4G4FEvVwo02qGVl8h1QvqQXI,6646
30
- pyplumio/parameters/thermostat.py,sha256=-DK2Mb78CGrKmdhwAD0M3GiGJatczPnl1e2gVeT19tI,5070
31
- pyplumio/parameters/custom/__init__.py,sha256=o1khThLf4FMrjErFIcikAc6jI9gn5IyZlo7LNKKqJG4,3194
27
+ pyplumio/parameters/__init__.py,sha256=d2gLX-Ve6UxwxLbiTQU6AGNYrC4ywXK1dJ7F92OkfgM,16108
28
+ pyplumio/parameters/ecomax.py,sha256=KjHlkVZK2XYEl4HNSdCRLAnv0KEn7gjnEO_CsKFZwIw,26199
29
+ pyplumio/parameters/mixer.py,sha256=cjwe6AJdboAIEnCeiYNqIRmOVo3dSQqbMTWgiCSx8J8,6606
30
+ pyplumio/parameters/thermostat.py,sha256=sRAndI87jANM8uvdQc1LdkT6_baDxf0AEAFVYRstzNE,5039
31
+ pyplumio/parameters/custom/__init__.py,sha256=EeddoseRsh2Gxche3e3woRBgNszraOnLUs9TciK7dCA,3168
32
32
  pyplumio/parameters/custom/ecomax_860d3_hb.py,sha256=IsNgDXmV90QpBilDV4fGSBtIUEQJJbR9rjnfCr3-pHE,2840
33
- pyplumio/structures/__init__.py,sha256=emZVH5OFgdTUPbEJoznMKitmK0nlPm0I4SmF86It1Do,1345
34
- pyplumio/structures/alerts.py,sha256=Whl_WyHV9sXr321SuJAYBc1wUawNzi7xMZc41M8qToY,3724
33
+ pyplumio/structures/__init__.py,sha256=tb62y-x466WSogdjNpsvqcD3Kiz7xMW604m2-yJH3jc,1329
34
+ pyplumio/structures/alerts.py,sha256=9cBzxo1R5erJVeQUdWOmEDG82wXgm7vAU9X6xJjRAjk,3690
35
35
  pyplumio/structures/boiler_load.py,sha256=e-6itp9L6iJeeOyhSTiOclHLuYmqG7KkcepsHwJSQSI,894
36
36
  pyplumio/structures/boiler_power.py,sha256=7CdOk-pYLEpy06oRBAeichvq8o-a2RcesB0tzo9ccBs,951
37
37
  pyplumio/structures/ecomax_parameters.py,sha256=E_s5bO0RqX8p1rM5DtYAsEXcHqS8P6Tg4AGm21cxsnM,1663
@@ -42,22 +42,22 @@ pyplumio/structures/fuel_level.py,sha256=-zUKApVJaZZzc1q52vqO3K2Mya43c6vRgw45d2x
42
42
  pyplumio/structures/lambda_sensor.py,sha256=09nM4Hwn1X275LzFpDihtpzkazwgJXAbx4NFqUkhbNM,1609
43
43
  pyplumio/structures/mixer_parameters.py,sha256=JMSySqI7TUGKdFtDp1P5DJm5EAbijMSz-orRrAe1KlQ,2041
44
44
  pyplumio/structures/mixer_sensors.py,sha256=ChgLhC3p4fyoPy1EKe0BQTvXOPZEISbcK2HyamrNaN8,2450
45
- pyplumio/structures/modules.py,sha256=LviFz3_pPvSQ5i_Mr2S9o5miVad8O4qn48CR_z7yG24,2791
46
- pyplumio/structures/network_info.py,sha256=HYhROSMbVxqYxsOa7aF3xetQXEs0xGvhzH-4OHETZVQ,4327
45
+ pyplumio/structures/modules.py,sha256=-8krDmCtrLwP3GvVMe3e-dN8Zbe4R0F1cZZuEo6N2zc,2759
46
+ pyplumio/structures/network_info.py,sha256=g5SkVS8QhUKa-Pt8rxJ5BAbz09hck6-npZb5a8KjIEg,4405
47
47
  pyplumio/structures/output_flags.py,sha256=upVIgAH2JNncHFXvjE-t6oTFF-JNwwZbyGjfrcKWtz0,1508
48
48
  pyplumio/structures/outputs.py,sha256=3NP5lArzQiihRC4QzBuWAHL9hhjvGxNkKmeoYZnDD-0,2291
49
49
  pyplumio/structures/pending_alerts.py,sha256=b1uMmDHTGv8eE0h1vGBrKsPxlwBmUad7HgChnDDLK_g,801
50
- pyplumio/structures/product_info.py,sha256=Y5Q5UzKcxrixkB3Fd_BZaj1DdUNvUw1XASqR1oKMqn0,3308
51
- pyplumio/structures/program_version.py,sha256=qHmmPComCOa-dgq7cFAucEGuRS-jWYwWi40VCiPS7cc,2621
50
+ pyplumio/structures/product_info.py,sha256=QEr2x8GAoXCc1_UzdaVWALRdi2ouMrBtGb684OBNgAQ,3255
51
+ pyplumio/structures/program_version.py,sha256=QLe_jFZcUOjWsEXrnXRiueFb4MR0coIGOymTtBiYtyg,2589
52
52
  pyplumio/structures/regulator_data.py,sha256=SYKI1YPC3mDAth-SpYejttbD0IzBfobjgC-uy_uUKnw,2333
53
53
  pyplumio/structures/regulator_data_schema.py,sha256=0SapbZCGzqAHmHC7dwhufszJ9FNo_ZO_XMrFGNiUe-w,1547
54
- pyplumio/structures/schedules.py,sha256=SGD9p12G_BVU2PSR1k5AS1cgx_bujFw8rqKSFohtEbc,12052
54
+ pyplumio/structures/schedules.py,sha256=IJ7hxGmLxgCtzGrMncXJBRddbc4quf91Wv_R-Y3ZJXA,11820
55
55
  pyplumio/structures/statuses.py,sha256=1h-EUw1UtuS44E19cNOSavUgZeAxsLgX3iS0eVC8pLI,1325
56
56
  pyplumio/structures/temperatures.py,sha256=2VD3P_vwp9PEBkOn2-WhifOR8w-UYNq35aAxle0z2Vg,2831
57
57
  pyplumio/structures/thermostat_parameters.py,sha256=st3x3HkjQm3hqBrn_fpvPDQu8fuc-Sx33ONB19ViQak,3007
58
58
  pyplumio/structures/thermostat_sensors.py,sha256=rO9jTZWGQpThtJqVdbbv8sYMYHxJi4MfwZQza69L2zw,3399
59
- pyplumio-0.5.55.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
60
- pyplumio-0.5.55.dist-info/METADATA,sha256=85fX-7JuFsPkNo-zO8RJrM-m82-c_V3eJHr6pMNblOk,5617
61
- pyplumio-0.5.55.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
- pyplumio-0.5.55.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
63
- pyplumio-0.5.55.dist-info/RECORD,,
59
+ pyplumio-0.6.0.dist-info/licenses/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
60
+ pyplumio-0.6.0.dist-info/METADATA,sha256=fyZvRedY6toO1OuENZe4ytYtPA0EFJqYb7CJy-Tswlo,5579
61
+ pyplumio-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
62
+ pyplumio-0.6.0.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
63
+ pyplumio-0.6.0.dist-info/RECORD,,