PyPlumIO 0.5.15__py3-none-any.whl → 0.5.17__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PyPlumIO
3
- Version: 0.5.15
3
+ Version: 0.5.17
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
@@ -25,22 +25,22 @@ License-File: LICENSE
25
25
  Requires-Dist: pyserial-asyncio ==0.6
26
26
  Provides-Extra: dev
27
27
  Requires-Dist: pyplumio[docs,test] ; extra == 'dev'
28
- Requires-Dist: pre-commit ==3.6.1 ; extra == 'dev'
28
+ Requires-Dist: pre-commit ==3.7.0 ; extra == 'dev'
29
29
  Requires-Dist: tomli ==2.0.1 ; extra == 'dev'
30
30
  Provides-Extra: docs
31
- Requires-Dist: sphinx ==7.2.6 ; extra == 'docs'
31
+ Requires-Dist: sphinx ==7.3.7 ; extra == 'docs'
32
32
  Requires-Dist: sphinx-rtd-theme ==2.0.0 ; extra == 'docs'
33
33
  Requires-Dist: readthedocs-sphinx-search ==0.3.2 ; extra == 'docs'
34
34
  Provides-Extra: test
35
35
  Requires-Dist: codespell ==2.2.6 ; extra == 'test'
36
- Requires-Dist: coverage ==7.4.1 ; extra == 'test'
37
- Requires-Dist: mypy ==1.8.0 ; extra == 'test'
36
+ Requires-Dist: coverage ==7.5.0 ; extra == 'test'
37
+ Requires-Dist: mypy ==1.9.0 ; extra == 'test'
38
38
  Requires-Dist: pyserial-asyncio-fast ==0.11 ; extra == 'test'
39
- Requires-Dist: pytest ==8.0.0 ; extra == 'test'
40
- Requires-Dist: pytest-asyncio ==0.23.5 ; extra == 'test'
41
- Requires-Dist: ruff ==0.2.1 ; extra == 'test'
42
- Requires-Dist: tox ==4.12.1 ; extra == 'test'
43
- Requires-Dist: types-pyserial ==3.5.0.20240205 ; extra == 'test'
39
+ Requires-Dist: pytest ==8.2.0 ; extra == 'test'
40
+ Requires-Dist: pytest-asyncio ==0.23.6 ; extra == 'test'
41
+ Requires-Dist: ruff ==0.4.2 ; extra == 'test'
42
+ Requires-Dist: tox ==4.15.0 ; extra == 'test'
43
+ Requires-Dist: types-pyserial ==3.5.0.20240311 ; extra == 'test'
44
44
 
45
45
  # PyPlumIO is a native ecoNET library for Plum ecoMAX controllers.
46
46
  [![PyPI version](https://badge.fury.io/py/PyPlumIO.svg)](https://badge.fury.io/py/PyPlumIO)
@@ -1,44 +1,44 @@
1
1
  pyplumio/__init__.py,sha256=4v9BaIkJ440qu2ITrcwVLOg9KO8k2W1gnGHe86sDLM8,3292
2
2
  pyplumio/__main__.py,sha256=oop76iR-XDHhMFhW4LO8-xTnBHsqzUQ5VVh__94OaqY,499
3
- pyplumio/_version.py,sha256=ouj2OiQxA8zuc1Xu0BZzw9hz_rARUPrlwVGV3wH8AoI,413
4
- pyplumio/connection.py,sha256=cgfd4jHuaH56CxvNwkGL-zXcY3iQOH1nwJQ7QMH4aao,6444
3
+ pyplumio/_version.py,sha256=dQYxLlSh6o3oTwPaW5cCsGzUzw8dHq1HmpZtnNncm_0,413
4
+ pyplumio/connection.py,sha256=0PmpNOcV7RgSfEeM7Wnhtbw0TpdtMTyqpJPQ2h7IX9M,6199
5
5
  pyplumio/const.py,sha256=4_VRx5mY7qf5fwaUluVlhuK_NIb7bClzhnhRLA-3GkY,3914
6
6
  pyplumio/exceptions.py,sha256=3tgfe0GD-T-DV2TUybsv7dwsk9P9f2g-4Gd8jnXw6DI,698
7
7
  pyplumio/filters.py,sha256=kXR4SUS7YXGaljW35NpEB9prHgxDKpsf_U1ACMTnh5I,11174
8
- pyplumio/protocol.py,sha256=1TTSLn2UoTruRCiChKE_ZFw6a8pJSzqNKpPMKvO6fSY,8663
8
+ pyplumio/protocol.py,sha256=Gl3aUxdcK5Z_ZkO88bPwiv9dBR_JO8VQR_H7aKKbHiM,8756
9
9
  pyplumio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- pyplumio/stream.py,sha256=sOg4jTPRVqQFvp0UartRFZWTmzdp3DSns-5VKqh2HgU,4352
10
+ pyplumio/stream.py,sha256=UPqlUmNBobSS4SvS5BI19g_FeSTLpLj1k3l7OeE32bI,4374
11
11
  pyplumio/utils.py,sha256=h46IS7hOHFbAFGGmnyAUVwlfk0m_eSwa3U8oZq6-bJY,687
12
- pyplumio/devices/__init__.py,sha256=qvj8P4dcAxYUcIIphS2eaYAHmi6Y_T3uv462nynSyJ8,6357
13
- pyplumio/devices/ecomax.py,sha256=3163pEUcxIZYVRe9nTaT55ysJoyBDV2ca6czWYkNFf8,16547
12
+ pyplumio/devices/__init__.py,sha256=zw0DDvXLdASaMeTUMZog-osKhsugNpPMYU4dV1Jr4iU,6379
13
+ pyplumio/devices/ecomax.py,sha256=cdPZ2LbUvH0l8hpwWxZiB570OloIpeilXWdHEDlixW4,16795
14
14
  pyplumio/devices/ecoster.py,sha256=QH68bzs199x9Id-wOY6J5WddyR69STNQi8SxaWDwKG4,312
15
15
  pyplumio/devices/mixer.py,sha256=a-1klZuEO7dLQfpdOor0vUZ9ksllf8D1w0AbMeGmE_g,2946
16
16
  pyplumio/devices/thermostat.py,sha256=CXHAWwqyDATcI8UYNcTv6TK3YHYDSUyHE3Txyy9fVAI,2401
17
- pyplumio/frames/__init__.py,sha256=twNWMxwtKmytRBe9SmCBipq6Dmddgg6U1eoKMGZwsKo,7270
18
- pyplumio/frames/messages.py,sha256=c--VWz-GUKUb0XxgHwFLW3SvQ6wMShp1_DHTP5xC1Pc,3628
19
- pyplumio/frames/requests.py,sha256=s7DkqPTVLLSaoK1u5uI9x-xwiXAhIH-Ipi-fvdIB8yg,7322
20
- pyplumio/frames/responses.py,sha256=cf6Mf0ohhycDSDF6aHORvr9K_rpUA4BgHm2OYEd5gd0,6615
17
+ pyplumio/frames/__init__.py,sha256=p3YFapqx7q9KJt3KhmG4qKFmQqNu05Y7hK1k9B9tENY,7264
18
+ pyplumio/frames/messages.py,sha256=hZn8mvPuSw4kD7EL8Xtroh1sDZe6CqLc5HyvctMzGMc,3616
19
+ pyplumio/frames/requests.py,sha256=F64kJ29jLfyy1YRqWLVTqib5MALGeB72NY8-kAx42IY,7220
20
+ pyplumio/frames/responses.py,sha256=7MHwXkNVTcwYpWRAXBMiPxmaJw3vgX9kgoaBTXeRy60,6531
21
21
  pyplumio/helpers/__init__.py,sha256=H2xxdkF-9uADLwEbfBUoxNTdwru3L5Z2cfJjgsuRsn0,31
22
22
  pyplumio/helpers/data_types.py,sha256=UIbJu0JfXaNCiHlg8EnYOfeyKq9Mxiu6j_D8GyRudZk,8211
23
23
  pyplumio/helpers/event_manager.py,sha256=E9gMGCxOOBNZgnHCwpb8rtFgKsNIkjHvIN73XBgwQDk,6004
24
- pyplumio/helpers/factory.py,sha256=6GUgny_h6FZUabpL9p6Sg3voaR7U10utNgpv6OsM2j0,562
25
- pyplumio/helpers/parameter.py,sha256=6fqrPovJLlDoN5PydhcL0myJyEmRxxkMzJtYiPicpTE,8682
26
- pyplumio/helpers/schedule.py,sha256=UOo672ZJJbO4DjwWqU_6J30myiW5fqnHVgU-goqeuRI,4943
24
+ pyplumio/helpers/factory.py,sha256=9U0vYy137nDI5Zl0CjlWhm6p0ztGeafOtkydZU0kA6I,694
25
+ pyplumio/helpers/parameter.py,sha256=ihdom_tON2urYXmS7x_2-zk2I-PcogdjILE7tOiGR8M,8698
26
+ pyplumio/helpers/schedule.py,sha256=zfVmawRjpKa5JpDGg2n9E985UtW-hEWEyn6cKSm6U6c,4911
27
27
  pyplumio/helpers/task_manager.py,sha256=P17Nw9HDbA9NMSdkJo2WQRbEsykzzFSwQyyRzI3uFPk,1089
28
28
  pyplumio/helpers/timeout.py,sha256=bWBWvLPpgjCvdG5hlrSTXok_CsLme-jGnY9rHwupRc0,1286
29
29
  pyplumio/helpers/typing.py,sha256=V3uYCMyC4oePM7YzL0S-xEsyTgjgDbkOM0VNe-1LBPo,684
30
30
  pyplumio/helpers/uid.py,sha256=FTwuET3WLNyEkAHpxD4ZA3hu-l_JCS6pq12xibAQs6s,974
31
31
  pyplumio/structures/__init__.py,sha256=-nbLcQvbWcs2EmnChqJmMVo1CUfj8lqMHb8yK1mV4j8,1298
32
- pyplumio/structures/alerts.py,sha256=ZnW5DKdoAacJZR9HTx2L3kHBEv2Q1F-q10AVgEKRvmg,3169
32
+ pyplumio/structures/alerts.py,sha256=v--wbEKoB07r9KHMuZgMRT1dk4r8dPwxxB5cPv4lzZY,3259
33
33
  pyplumio/structures/boiler_load.py,sha256=HVPKt53VWvp2KDuSK1B9kcpX1h3Bz3GPBqBI4Oscm2Q,837
34
34
  pyplumio/structures/boiler_power.py,sha256=tP00IMz5qVQaePr70uTz_jCLoXHnmj_g3pvKxdNvenc,900
35
- pyplumio/structures/ecomax_parameters.py,sha256=djm8BY9bxWUOcHUxn8NQ26nvdsAXiU_hrxh1ljIdDY8,26133
35
+ pyplumio/structures/ecomax_parameters.py,sha256=D4IFbRzMtGK91XUCAzGp2VJ9LouYnRcPa-QP2mH2lnA,26184
36
36
  pyplumio/structures/fan_power.py,sha256=fGU_BTPTtAplnjmTQQHhMXdoiHVYOdLlZ_PDA9F5u9c,870
37
37
  pyplumio/structures/frame_versions.py,sha256=9lFnrlxkok_3CTbXz38cntacnubWYGcaubDnYq9NOAM,1559
38
38
  pyplumio/structures/fuel_consumption.py,sha256=H23GVw9s0fbURvJHAfReTEvGp5q-oPOidh1mToOO3ec,975
39
39
  pyplumio/structures/fuel_level.py,sha256=27QfxB_LXAREmHs7q8KyohpLbagLUZolCQoZJUDTPz0,1068
40
40
  pyplumio/structures/lambda_sensor.py,sha256=JdKHWN2RcMgCXhMZ0IhYCYrHWnw2TgsR-iitLn7QLe0,1586
41
- pyplumio/structures/mixer_parameters.py,sha256=z29fhv8NQmdX_uPNSLCn505XhGs37qnzAkZ0P93GJH0,8304
41
+ pyplumio/structures/mixer_parameters.py,sha256=UsOey8D5d7rrUACwOcoBB_x2lHQYKC2Xsoxqcpf6DG4,8380
42
42
  pyplumio/structures/mixer_sensors.py,sha256=f6BjpX-ENGlKIDOOWUHMlb0TZ5HMYD9dIkRoSvgRXHg,2259
43
43
  pyplumio/structures/modules.py,sha256=t332XxB-6dTtDbY9mNuqTqb21zDy2XG_17pmWgJfGvw,2519
44
44
  pyplumio/structures/network_info.py,sha256=pLbCB5Z8nCB6efb6ZZImw3TaVAOB3Af5K1z9vVbuGgo,4054
@@ -49,13 +49,13 @@ pyplumio/structures/product_info.py,sha256=Os0AS88geH2JqvVRC2zQxEucmGJoDzR3ed3Pl
49
49
  pyplumio/structures/program_version.py,sha256=WX5tM3X3aaJCjNd5rsAN6EXivBXM-kLXQvhfQR8wKIM,2358
50
50
  pyplumio/structures/regulator_data.py,sha256=9d7fGcZ1SI2QosUQSd0CpPqaHpgXihqV5-QgHWGzkNU,2379
51
51
  pyplumio/structures/regulator_data_schema.py,sha256=5uwdw8nCqSX9Ex5jH9t4pghj6fLRRsCnGQx69LVGAy0,1504
52
- pyplumio/structures/schedules.py,sha256=9CaWAWJ1LAFUcIjqE05djc4AFJA7uaKex41D2dJQEdI,6381
52
+ pyplumio/structures/schedules.py,sha256=j7kuT7gYesFKJJ4yMrW6JVO0aJX9O6PT2D_PTCQre04,6441
53
53
  pyplumio/structures/statuses.py,sha256=zjDQTU5fdORRzRkvlkzplgVS8A3AdyHG2qihfkdOHIM,1165
54
54
  pyplumio/structures/temperatures.py,sha256=O8rANFN-wz5NvTWqh_25oUfFajjVyI9hoOMmz8vkKHg,2336
55
- pyplumio/structures/thermostat_parameters.py,sha256=k35TdGTqVYXr7Eug-kRENOc-2UMy3nAYjY9vgmqZGv0,7846
55
+ pyplumio/structures/thermostat_parameters.py,sha256=1SJzHguN12JE-e2utwA448VgUkFddQxBQy3m66qCobk,7934
56
56
  pyplumio/structures/thermostat_sensors.py,sha256=N9nm3Rp1Rhb8wUPUkJIX56olZztixoxyFxIxP4R5P2g,3176
57
- PyPlumIO-0.5.15.dist-info/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
58
- PyPlumIO-0.5.15.dist-info/METADATA,sha256=uMuV7ATpqajSQuk401CO7xXs1RTgS_otlebFhi9_hoc,5414
59
- PyPlumIO-0.5.15.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
60
- PyPlumIO-0.5.15.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
61
- PyPlumIO-0.5.15.dist-info/RECORD,,
57
+ PyPlumIO-0.5.17.dist-info/LICENSE,sha256=m-UuZFjXJ22uPTGm9kSHS8bqjsf5T8k2wL9bJn1Y04o,1088
58
+ PyPlumIO-0.5.17.dist-info/METADATA,sha256=hYBntfwRNAwg3Zdb_S0HailPnsFm9ewT99rRNys494U,5414
59
+ PyPlumIO-0.5.17.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
60
+ PyPlumIO-0.5.17.dist-info/top_level.txt,sha256=kNBz9UPPkPD9teDn3U_sEy5LjzwLm9KfADCXtBlbw8A,9
61
+ PyPlumIO-0.5.17.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
pyplumio/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.5.15'
16
- __version_tuple__ = version_tuple = (0, 5, 15)
15
+ __version__ = version = '0.5.17'
16
+ __version_tuple__ = version_tuple = (0, 5, 17)
pyplumio/connection.py CHANGED
@@ -10,6 +10,7 @@ from typing import Any, Final, cast
10
10
  from serial import EIGHTBITS, PARITY_NONE, STOPBITS_ONE, SerialException
11
11
 
12
12
  from pyplumio.exceptions import ConnectionFailedError
13
+ from pyplumio.helpers.task_manager import TaskManager
13
14
  from pyplumio.helpers.timeout import timeout
14
15
  from pyplumio.protocol import AsyncProtocol, Protocol
15
16
 
@@ -26,13 +27,12 @@ except ImportError:
26
27
  import serial_asyncio as pyserial_asyncio
27
28
 
28
29
 
29
- class Connection(ABC):
30
+ class Connection(ABC, TaskManager):
30
31
  """Represents a connection.
31
32
 
32
33
  All specific connection classes MUST be inherited from this class.
33
34
  """
34
35
 
35
- _closing: bool
36
36
  _protocol: Protocol
37
37
  _reconnect_on_failure: bool
38
38
  _kwargs: MutableMapping[str, Any]
@@ -44,13 +44,13 @@ class Connection(ABC):
44
44
  **kwargs: Any,
45
45
  ) -> None:
46
46
  """Initialize a new connection."""
47
+ super().__init__()
47
48
  if protocol is None:
48
49
  protocol = AsyncProtocol()
49
50
 
50
51
  if reconnect_on_failure:
51
- protocol.on_connection_lost.add(self._connection_lost)
52
+ protocol.on_connection_lost.add(self._reconnect)
52
53
 
53
- self._closing = False
54
54
  self._reconnect_on_failure = reconnect_on_failure
55
55
  self._protocol = protocol
56
56
  self._kwargs = kwargs
@@ -76,29 +76,20 @@ class Connection(ABC):
76
76
  await self._open_connection(),
77
77
  )
78
78
  self.protocol.connection_established(reader, writer)
79
- except (
80
- OSError,
81
- SerialException,
82
- asyncio.TimeoutError,
83
- ) as connection_error:
84
- raise ConnectionFailedError from connection_error
79
+ except (OSError, SerialException, asyncio.TimeoutError) as err:
80
+ raise ConnectionFailedError from err
85
81
 
86
82
  async def _reconnect(self) -> None:
87
83
  """Try to connect and reconnect on failure."""
88
84
  try:
89
- return await self._connect()
85
+ await self._connect()
90
86
  except ConnectionFailedError:
91
- await self._connection_lost()
92
-
93
- async def _connection_lost(self) -> None:
94
- """Resume connection on the connection loss."""
95
- if not self._closing:
96
87
  _LOGGER.error(
97
88
  "Can't connect to the device, retrying in %.1f seconds",
98
89
  RECONNECT_TIMEOUT,
99
90
  )
100
91
  await asyncio.sleep(RECONNECT_TIMEOUT)
101
- await self._reconnect()
92
+ self.create_task(self._reconnect())
102
93
 
103
94
  async def connect(self) -> None:
104
95
  """Open the connection.
@@ -106,14 +97,11 @@ class Connection(ABC):
106
97
  Initialize a connection via connect or reconnect
107
98
  routines, depending on '_reconnect_on_failure' property.
108
99
  """
109
- if self._reconnect_on_failure:
110
- await self._reconnect()
111
- else:
112
- await self._connect()
100
+ await (self._reconnect if self._reconnect_on_failure else self._connect)()
113
101
 
114
102
  async def close(self) -> None:
115
103
  """Close the connection."""
116
- self._closing = True
104
+ self.cancel_tasks()
117
105
  await self.protocol.shutdown()
118
106
 
119
107
  @property
@@ -11,7 +11,7 @@ from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType
11
11
  from pyplumio.exceptions import UnknownDeviceError
12
12
  from pyplumio.frames import DataFrameDescription, Frame, Request, get_frame_handler
13
13
  from pyplumio.helpers.event_manager import EventManager
14
- from pyplumio.helpers.factory import factory
14
+ from pyplumio.helpers.factory import create_instance
15
15
  from pyplumio.helpers.parameter import SET_RETRIES, Parameter
16
16
  from pyplumio.helpers.typing import ParameterValueType
17
17
  from pyplumio.structures.network_info import NetworkInfo
@@ -160,7 +160,7 @@ class AddressableDevice(Device, ABC):
160
160
 
161
161
  If value is not available before timeout, retry request.
162
162
  """
163
- request: Request = factory(
163
+ request: Request = await create_instance(
164
164
  get_frame_handler(frame_type), recipient=self.address
165
165
  )
166
166
 
@@ -28,7 +28,7 @@ from pyplumio.frames import (
28
28
  get_frame_handler,
29
29
  is_known_frame_type,
30
30
  )
31
- from pyplumio.helpers.factory import factory
31
+ from pyplumio.helpers.factory import create_instance
32
32
  from pyplumio.helpers.parameter import ParameterValues
33
33
  from pyplumio.helpers.schedule import Schedule, ScheduleDay
34
34
  from pyplumio.structures.alerts import ATTR_TOTAL_ALERTS
@@ -235,7 +235,7 @@ class EcoMAX(AddressableDevice):
235
235
  and not self._has_frame_version(frame_type, version)
236
236
  ):
237
237
  # We don't have this frame or it's version has changed.
238
- request: Request = factory(
238
+ request: Request = await create_instance(
239
239
  get_frame_handler(frame_type), recipient=self.address
240
240
  )
241
241
  self.queue.put_nowait(request)
@@ -346,16 +346,23 @@ class EcoMAX(AddressableDevice):
346
346
 
347
347
  return True
348
348
 
349
- async def _add_ecomax_control_parameter(self, mode: int) -> None:
349
+ async def _add_ecomax_control_parameter(self, mode: DeviceState) -> None:
350
350
  """Create ecoMAX control parameter instance and dispatch an event."""
351
+ description = ECOMAX_CONTROL_PARAMETER
352
+ name = description.name
353
+ values = ParameterValues(
354
+ value=int(mode != DeviceState.OFF), min_value=0, max_value=1
355
+ )
356
+
357
+ if name in self.data:
358
+ parameter: EcomaxBinaryParameter = self.data[name]
359
+ parameter.values = values
360
+ return await self.dispatch(name, parameter)
361
+
351
362
  await self.dispatch(
352
- ECOMAX_CONTROL_PARAMETER.name,
363
+ name,
353
364
  EcomaxBinaryParameter(
354
- device=self,
355
- description=ECOMAX_CONTROL_PARAMETER,
356
- values=ParameterValues(
357
- value=int(mode != DeviceState.OFF), min_value=0, max_value=1
358
- ),
365
+ device=self, description=ECOMAX_CONTROL_PARAMETER, values=values
359
366
  ),
360
367
  )
361
368
 
@@ -80,7 +80,7 @@ class Frame(ABC):
80
80
  sender: DeviceType | AddressableDevice | int
81
81
  sender_type: int
82
82
  econet_version: int
83
- frame_type: ClassVar[FrameType | int]
83
+ frame_type: ClassVar[FrameType]
84
84
  _message: bytearray | None
85
85
  _data: dict[str, Any] | None
86
86
 
@@ -35,7 +35,7 @@ class RegulatorDataMessage(Message):
35
35
 
36
36
  __slots__ = ()
37
37
 
38
- frame_type: ClassVar[FrameType | int] = FrameType.MESSAGE_REGULATOR_DATA
38
+ frame_type: ClassVar[FrameType] = FrameType.MESSAGE_REGULATOR_DATA
39
39
 
40
40
  def decode_message(self, message: bytearray) -> dict[str, Any]:
41
41
  """Decode a frame message."""
@@ -47,7 +47,7 @@ class SensorDataMessage(Message):
47
47
 
48
48
  __slots__ = ()
49
49
 
50
- frame_type: ClassVar[FrameType | int] = FrameType.MESSAGE_SENSOR_DATA
50
+ frame_type: ClassVar[FrameType] = FrameType.MESSAGE_SENSOR_DATA
51
51
 
52
52
  def decode_message(self, message: bytearray) -> dict[str, Any]:
53
53
  """Decode a frame message."""
@@ -24,7 +24,7 @@ class ProgramVersionRequest(Request):
24
24
 
25
25
  __slots__ = ()
26
26
 
27
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_PROGRAM_VERSION
27
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_PROGRAM_VERSION
28
28
 
29
29
  def response(self, **kwargs: Any) -> Response | None:
30
30
  """Return a response frame."""
@@ -36,7 +36,7 @@ class CheckDeviceRequest(Request):
36
36
 
37
37
  __slots__ = ()
38
38
 
39
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_CHECK_DEVICE
39
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_CHECK_DEVICE
40
40
 
41
41
  def response(self, **kwargs: Any) -> Response | None:
42
42
  """Return a response frame."""
@@ -48,7 +48,7 @@ class UIDRequest(Request):
48
48
 
49
49
  __slots__ = ()
50
50
 
51
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_UID
51
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_UID
52
52
 
53
53
 
54
54
  class PasswordRequest(Request):
@@ -56,7 +56,7 @@ class PasswordRequest(Request):
56
56
 
57
57
  __slots__ = ()
58
58
 
59
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_PASSWORD
59
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_PASSWORD
60
60
 
61
61
 
62
62
  class EcomaxParametersRequest(Request):
@@ -67,7 +67,7 @@ class EcomaxParametersRequest(Request):
67
67
 
68
68
  __slots__ = ()
69
69
 
70
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_ECOMAX_PARAMETERS
70
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_ECOMAX_PARAMETERS
71
71
 
72
72
  def create_message(self, data: dict[str, Any]) -> bytearray:
73
73
  """Create a frame message."""
@@ -83,7 +83,7 @@ class MixerParametersRequest(Request):
83
83
 
84
84
  __slots__ = ()
85
85
 
86
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_MIXER_PARAMETERS
86
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_MIXER_PARAMETERS
87
87
 
88
88
  def create_message(self, data: dict[str, Any]) -> bytearray:
89
89
  """Create a frame message."""
@@ -99,7 +99,7 @@ class ThermostatParametersRequest(Request):
99
99
 
100
100
  __slots__ = ()
101
101
 
102
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_THERMOSTAT_PARAMETERS
102
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_THERMOSTAT_PARAMETERS
103
103
 
104
104
  def create_message(self, data: dict[str, Any]) -> bytearray:
105
105
  """Create a frame message."""
@@ -111,7 +111,7 @@ class RegulatorDataSchemaRequest(Request):
111
111
 
112
112
  __slots__ = ()
113
113
 
114
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_REGULATOR_DATA_SCHEMA
114
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_REGULATOR_DATA_SCHEMA
115
115
 
116
116
 
117
117
  class SetEcomaxParameterRequest(Request):
@@ -122,7 +122,7 @@ class SetEcomaxParameterRequest(Request):
122
122
 
123
123
  __slots__ = ()
124
124
 
125
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_SET_ECOMAX_PARAMETER
125
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_SET_ECOMAX_PARAMETER
126
126
 
127
127
  def create_message(self, data: dict[str, Any]) -> bytearray:
128
128
  """Create a frame message."""
@@ -140,7 +140,7 @@ class SetMixerParameterRequest(Request):
140
140
 
141
141
  __slots__ = ()
142
142
 
143
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_SET_MIXER_PARAMETER
143
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_SET_MIXER_PARAMETER
144
144
 
145
145
  def create_message(self, data: dict[str, Any]) -> bytearray:
146
146
  """Create a frame message."""
@@ -168,7 +168,7 @@ class SetThermostatParameterRequest(Request):
168
168
 
169
169
  __slots__ = ()
170
170
 
171
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_SET_THERMOSTAT_PARAMETER
171
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_SET_THERMOSTAT_PARAMETER
172
172
 
173
173
  def create_message(self, data: dict[str, Any]) -> bytearray:
174
174
  """Create a frame message."""
@@ -191,7 +191,7 @@ class EcomaxControlRequest(Request):
191
191
 
192
192
  __slots__ = ()
193
193
 
194
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_ECOMAX_CONTROL
194
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_ECOMAX_CONTROL
195
195
 
196
196
  def create_message(self, data: dict[str, Any]) -> bytearray:
197
197
  """Create a frame message."""
@@ -210,7 +210,7 @@ class StartMasterRequest(Request):
210
210
 
211
211
  __slots__ = ()
212
212
 
213
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_START_MASTER
213
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_START_MASTER
214
214
 
215
215
 
216
216
  class StopMasterRequest(Request):
@@ -222,7 +222,7 @@ class StopMasterRequest(Request):
222
222
 
223
223
  __slots__ = ()
224
224
 
225
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_STOP_MASTER
225
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_STOP_MASTER
226
226
 
227
227
 
228
228
  class AlertsRequest(Request):
@@ -234,7 +234,7 @@ class AlertsRequest(Request):
234
234
 
235
235
  __slots__ = ()
236
236
 
237
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_ALERTS
237
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_ALERTS
238
238
 
239
239
  def create_message(self, data: dict[str, Any]) -> bytearray:
240
240
  """Create a frame message."""
@@ -246,7 +246,7 @@ class SchedulesRequest(Request):
246
246
 
247
247
  __slots__ = ()
248
248
 
249
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_SCHEDULES
249
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_SCHEDULES
250
250
 
251
251
 
252
252
  class SetScheduleRequest(Request):
@@ -254,7 +254,7 @@ class SetScheduleRequest(Request):
254
254
 
255
255
  __slots__ = ()
256
256
 
257
- frame_type: ClassVar[FrameType | int] = FrameType.REQUEST_SET_SCHEDULE
257
+ frame_type: ClassVar[FrameType] = FrameType.REQUEST_SET_SCHEDULE
258
258
 
259
259
  def create_message(self, data: dict[str, Any]) -> bytearray:
260
260
  """Create a frame message."""
@@ -24,7 +24,7 @@ class ProgramVersionResponse(Response):
24
24
 
25
25
  __slots__ = ()
26
26
 
27
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_PROGRAM_VERSION
27
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_PROGRAM_VERSION
28
28
 
29
29
  def create_message(self, data: dict[str, Any]) -> bytearray:
30
30
  """Create a frame message."""
@@ -43,7 +43,7 @@ class DeviceAvailableResponse(Response):
43
43
 
44
44
  __slots__ = ()
45
45
 
46
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_DEVICE_AVAILABLE
46
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_DEVICE_AVAILABLE
47
47
 
48
48
  def create_message(self, data: dict[str, Any]) -> bytearray:
49
49
  """Create a frame message."""
@@ -62,7 +62,7 @@ class UIDResponse(Response):
62
62
 
63
63
  __slots__ = ()
64
64
 
65
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_UID
65
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_UID
66
66
 
67
67
  def create_message(self, data: dict[str, Any]) -> bytearray:
68
68
  """Create a frame message."""
@@ -81,7 +81,7 @@ class PasswordResponse(Response):
81
81
 
82
82
  __slots__ = ()
83
83
 
84
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_PASSWORD
84
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_PASSWORD
85
85
 
86
86
  def decode_message(self, message: bytearray) -> dict[str, Any]:
87
87
  """Decode a frame message."""
@@ -97,7 +97,7 @@ class EcomaxParametersResponse(Response):
97
97
 
98
98
  __slots__ = ()
99
99
 
100
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_ECOMAX_PARAMETERS
100
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_ECOMAX_PARAMETERS
101
101
 
102
102
  def decode_message(self, message: bytearray) -> dict[str, Any]:
103
103
  """Decode a frame message."""
@@ -112,7 +112,7 @@ class MixerParametersResponse(Response):
112
112
 
113
113
  __slots__ = ()
114
114
 
115
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_MIXER_PARAMETERS
115
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_MIXER_PARAMETERS
116
116
 
117
117
  def decode_message(self, message: bytearray) -> dict[str, Any]:
118
118
  """Decode a frame message."""
@@ -127,7 +127,7 @@ class ThermostatParametersResponse(Response):
127
127
 
128
128
  __slots__ = ()
129
129
 
130
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_THERMOSTAT_PARAMETERS
130
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_THERMOSTAT_PARAMETERS
131
131
 
132
132
  def decode_message(self, message: bytearray) -> dict[str, Any]:
133
133
  """Decode a frame message."""
@@ -143,7 +143,7 @@ class RegulatorDataSchemaResponse(Response):
143
143
 
144
144
  __slots__ = ()
145
145
 
146
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_REGULATOR_DATA_SCHEMA
146
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_REGULATOR_DATA_SCHEMA
147
147
 
148
148
  def decode_message(self, message: bytearray) -> dict[str, Any]:
149
149
  """Decode a frame message."""
@@ -159,7 +159,7 @@ class SetEcomaxParameterResponse(Response):
159
159
 
160
160
  __slots__ = ()
161
161
 
162
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_SET_ECOMAX_PARAMETER
162
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_SET_ECOMAX_PARAMETER
163
163
 
164
164
 
165
165
  class SetMixerParameterResponse(Response):
@@ -171,7 +171,7 @@ class SetMixerParameterResponse(Response):
171
171
 
172
172
  __slots__ = ()
173
173
 
174
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_SET_MIXER_PARAMETER
174
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_SET_MIXER_PARAMETER
175
175
 
176
176
 
177
177
  class SetThermostatParameterResponse(Response):
@@ -183,7 +183,7 @@ class SetThermostatParameterResponse(Response):
183
183
 
184
184
  __slots__ = ()
185
185
 
186
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_SET_THERMOSTAT_PARAMETER
186
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_SET_THERMOSTAT_PARAMETER
187
187
 
188
188
 
189
189
  class EcomaxControlResponse(Response):
@@ -195,7 +195,7 @@ class EcomaxControlResponse(Response):
195
195
 
196
196
  __slots__ = ()
197
197
 
198
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_ECOMAX_CONTROL
198
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_ECOMAX_CONTROL
199
199
 
200
200
 
201
201
  class AlertsResponse(Response):
@@ -203,7 +203,7 @@ class AlertsResponse(Response):
203
203
 
204
204
  __slots__ = ()
205
205
 
206
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_ALERTS
206
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_ALERTS
207
207
 
208
208
  def decode_message(self, message: bytearray) -> dict[str, Any]:
209
209
  """Decode a frame message."""
@@ -215,7 +215,7 @@ class SchedulesResponse(Response):
215
215
 
216
216
  __slots__ = ()
217
217
 
218
- frame_type: ClassVar[FrameType | int] = FrameType.RESPONSE_SCHEDULES
218
+ frame_type: ClassVar[FrameType] = FrameType.RESPONSE_SCHEDULES
219
219
 
220
220
  def decode_message(self, message: bytearray) -> dict[str, Any]:
221
221
  """Decode a frame message."""
@@ -1,20 +1,23 @@
1
1
  """Contains a factory helper."""
2
2
  from __future__ import annotations
3
3
 
4
- import importlib
4
+ import asyncio
5
+ from importlib import import_module
5
6
  import logging
6
7
  from typing import Any
7
8
 
8
9
  _LOGGER = logging.getLogger(__name__)
9
10
 
10
11
 
11
- def factory(class_path: str, **kwargs: Any) -> Any:
12
- """Return class instance from the class path."""
12
+ async def create_instance(class_path: str, **kwargs: Any) -> Any:
13
+ """Return a class instance from the class path."""
14
+ loop = asyncio.get_running_loop()
15
+ module_name, class_name = class_path.rsplit(".", 1)
13
16
  try:
14
- module_name, class_name = class_path.rsplit(".", 1)
15
- return getattr(
16
- importlib.import_module("." + module_name, "pyplumio"), class_name
17
- )(**kwargs)
17
+ module = await loop.run_in_executor(
18
+ None, import_module, "." + module_name, "pyplumio"
19
+ )
20
+ return getattr(module, class_name)(**kwargs)
18
21
  except Exception:
19
22
  _LOGGER.error("Failed to load module (%s)", class_path)
20
23
  raise
@@ -166,6 +166,10 @@ class Parameter(ABC):
166
166
  """Set parameter as no longer pending update."""
167
167
  self._pending_update = False
168
168
 
169
+ async def create_request(self) -> Request:
170
+ """Create a request to change the parameter."""
171
+ raise NotImplementedError
172
+
169
173
  async def set(self, value: ParameterValueType, retries: int = SET_RETRIES) -> bool:
170
174
  """Set a parameter value."""
171
175
  if (value := _normalize_parameter_value(value)) == self.values.value:
@@ -188,7 +192,7 @@ class Parameter(ABC):
188
192
  self.device.unsubscribe(self.description.name, self._confirm_update)
189
193
  return False
190
194
 
191
- await self.device.queue.put(self.request)
195
+ await self.device.queue.put(await self.create_request())
192
196
  await asyncio.sleep(SET_TIMEOUT)
193
197
  retries -= 1
194
198
 
@@ -223,11 +227,6 @@ class Parameter(ABC):
223
227
  """Return the unit of measurement."""
224
228
  return self.description.unit_of_measurement
225
229
 
226
- @property
227
- def request(self) -> Request:
228
- """Return request to change the parameter."""
229
- raise NotImplementedError
230
-
231
230
 
232
231
  class BinaryParameter(Parameter):
233
232
  """Represents binary device parameter."""
@@ -9,7 +9,7 @@ from typing import Final, Literal
9
9
 
10
10
  from pyplumio.const import STATE_OFF, STATE_ON
11
11
  from pyplumio.devices import AddressableDevice
12
- from pyplumio.helpers.factory import factory
12
+ from pyplumio.frames.requests import SetScheduleRequest
13
13
  from pyplumio.structures.schedules import collect_schedule_data
14
14
 
15
15
  TIME_FORMAT: Final = "%H:%M"
@@ -161,8 +161,7 @@ class Schedule(Iterable):
161
161
  def commit(self) -> None:
162
162
  """Commit a weekly schedule to the device."""
163
163
  self.device.queue.put_nowait(
164
- factory(
165
- "frames.requests.SetScheduleRequest",
164
+ SetScheduleRequest(
166
165
  recipient=self.device.address,
167
166
  data=collect_schedule_data(self.name, self.device),
168
167
  )
pyplumio/protocol.py CHANGED
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
  from abc import ABC, abstractmethod
5
5
  import asyncio
6
6
  from collections.abc import Awaitable, Callable
7
- from functools import cache
8
7
  import logging
9
8
  from typing import cast
10
9
 
@@ -19,7 +18,7 @@ from pyplumio.exceptions import (
19
18
  from pyplumio.frames import Frame
20
19
  from pyplumio.frames.requests import StartMasterRequest
21
20
  from pyplumio.helpers.event_manager import EventManager
22
- from pyplumio.helpers.factory import factory
21
+ from pyplumio.helpers.factory import create_instance
23
22
  from pyplumio.stream import FrameReader, FrameWriter
24
23
  from pyplumio.structures.network_info import (
25
24
  EthernetParameters,
@@ -202,7 +201,9 @@ class AsyncProtocol(Protocol, EventManager):
202
201
  write_queue.task_done()
203
202
 
204
203
  if (response := await reader.read()) is not None:
205
- device = self.get_device_entry(response.sender)
204
+ device = await self.get_device_entry(
205
+ cast(DeviceType, response.sender)
206
+ )
206
207
  read_queue.put_nowait((device, response))
207
208
 
208
209
  except FrameDataError as e:
@@ -229,16 +230,17 @@ class AsyncProtocol(Protocol, EventManager):
229
230
  device.handle_frame(frame)
230
231
  read_queue.task_done()
231
232
 
232
- @cache
233
- def get_device_entry(self, device_type: DeviceType) -> AddressableDevice:
233
+ async def get_device_entry(self, device_type: DeviceType) -> AddressableDevice:
234
234
  """Set up device entry."""
235
235
  handler, name = get_device_handler_and_name(device_type)
236
- return self.data.setdefault(name, self._create_device_entry(name, handler))
236
+ return self.data.setdefault(
237
+ name, await self._create_device_entry(name, handler)
238
+ )
237
239
 
238
- def _create_device_entry(self, name: str, handler: str) -> AddressableDevice:
240
+ async def _create_device_entry(self, name: str, handler: str) -> AddressableDevice:
239
241
  """Create device entry."""
240
242
  write_queue = self.queues[1]
241
- device: AddressableDevice = factory(
243
+ device: AddressableDevice = await create_instance(
242
244
  handler, queue=write_queue, network=self._network
243
245
  )
244
246
  device.dispatch_nowait(ATTR_CONNECTED, True)
pyplumio/stream.py CHANGED
@@ -9,7 +9,7 @@ from typing import Final
9
9
  from pyplumio.const import DeviceType
10
10
  from pyplumio.exceptions import ChecksumError, ReadError
11
11
  from pyplumio.frames import FRAME_START, Frame, bcc, get_frame_handler, struct_header
12
- from pyplumio.helpers.factory import factory
12
+ from pyplumio.helpers.factory import create_instance
13
13
  from pyplumio.helpers.timeout import timeout
14
14
 
15
15
  READER_TIMEOUT: Final = 10
@@ -132,7 +132,7 @@ class FrameReader:
132
132
  if payload[-2] != bcc(header + payload[:-2]):
133
133
  raise ChecksumError(f"Incorrect frame checksum ({payload[-2]})")
134
134
 
135
- frame: Frame = factory(
135
+ frame: Frame = await create_instance(
136
136
  get_frame_handler(frame_type=payload[0]),
137
137
  recipient=recipient,
138
138
  message=payload[1:-2],
@@ -15,6 +15,8 @@ from pyplumio.utils import ensure_dict
15
15
  ATTR_ALERTS: Final = "alerts"
16
16
  ATTR_TOTAL_ALERTS: Final = "total_alerts"
17
17
 
18
+ MAX_UINT32: Final = 4294967295
19
+
18
20
 
19
21
  @lru_cache(maxsize=10)
20
22
  def _convert_to_datetime(seconds: int) -> datetime:
@@ -72,7 +74,11 @@ class AlertsStructure(StructureDecoder):
72
74
  self._offset += to_seconds.size
73
75
 
74
76
  from_dt = _convert_to_datetime(from_seconds.value)
75
- to_dt = _convert_to_datetime(to_seconds.value) if to_seconds.value > 0 else None
77
+ to_dt = (
78
+ None
79
+ if to_seconds.value == MAX_UINT32
80
+ else _convert_to_datetime(to_seconds.value)
81
+ )
76
82
 
77
83
  return Alert(code, from_dt, to_dt)
78
84
 
@@ -16,7 +16,7 @@ from pyplumio.const import (
16
16
  )
17
17
  from pyplumio.devices import AddressableDevice
18
18
  from pyplumio.frames import Request
19
- from pyplumio.helpers.factory import factory
19
+ from pyplumio.helpers.factory import create_instance
20
20
  from pyplumio.helpers.parameter import (
21
21
  BinaryParameter,
22
22
  BinaryParameterDescription,
@@ -44,39 +44,10 @@ class EcomaxParameter(Parameter):
44
44
  device: AddressableDevice
45
45
  description: EcomaxParameterDescription
46
46
 
47
- async def set(self, value: ParameterValueType, retries: int = 5) -> bool:
48
- """Set a parameter value."""
49
- if isinstance(value, (int, float)):
50
- value = int((value + self.description.offset) / self.description.multiplier)
51
-
52
- return await super().set(value, retries)
53
-
54
- @property
55
- def value(self) -> ParameterValueType:
56
- """Return the parameter value."""
57
- return (
58
- self.values.value - self.description.offset
59
- ) * self.description.multiplier
60
-
61
- @property
62
- def min_value(self) -> ParameterValueType:
63
- """Return the minimum allowed value."""
64
- return (
65
- self.values.min_value - self.description.offset
66
- ) * self.description.multiplier
67
-
68
- @property
69
- def max_value(self) -> ParameterValueType:
70
- """Return the maximum allowed value."""
71
- return (
72
- self.values.max_value - self.description.offset
73
- ) * self.description.multiplier
74
-
75
- @property
76
- def request(self) -> Request:
77
- """Return request to change the parameter."""
47
+ async def create_request(self) -> Request:
48
+ """Create a request to change the parameter."""
78
49
  if self.description.name == ATTR_ECOMAX_CONTROL:
79
- request: Request = factory(
50
+ request: Request = await create_instance(
80
51
  "frames.requests.EcomaxControlRequest",
81
52
  recipient=self.device.address,
82
53
  data={
@@ -85,7 +56,7 @@ class EcomaxParameter(Parameter):
85
56
  )
86
57
 
87
58
  elif self.description.name == ATTR_THERMOSTAT_PROFILE:
88
- request = factory(
59
+ request = await create_instance(
89
60
  "frames.requests.SetThermostatParameterRequest",
90
61
  recipient=self.device.address,
91
62
  data={
@@ -97,7 +68,7 @@ class EcomaxParameter(Parameter):
97
68
  )
98
69
 
99
70
  else:
100
- request = factory(
71
+ request = await create_instance(
101
72
  "frames.requests.SetEcomaxParameterRequest",
102
73
  recipient=self.device.address,
103
74
  data={
@@ -108,6 +79,34 @@ class EcomaxParameter(Parameter):
108
79
 
109
80
  return request
110
81
 
82
+ async def set(self, value: ParameterValueType, retries: int = 5) -> bool:
83
+ """Set a parameter value."""
84
+ if isinstance(value, (int, float)):
85
+ value = int((value + self.description.offset) / self.description.multiplier)
86
+
87
+ return await super().set(value, retries)
88
+
89
+ @property
90
+ def value(self) -> ParameterValueType:
91
+ """Return the parameter value."""
92
+ return (
93
+ self.values.value - self.description.offset
94
+ ) * self.description.multiplier
95
+
96
+ @property
97
+ def min_value(self) -> ParameterValueType:
98
+ """Return the minimum allowed value."""
99
+ return (
100
+ self.values.min_value - self.description.offset
101
+ ) * self.description.multiplier
102
+
103
+ @property
104
+ def max_value(self) -> ParameterValueType:
105
+ """Return the maximum allowed value."""
106
+ return (
107
+ self.values.max_value - self.description.offset
108
+ ) * self.description.multiplier
109
+
111
110
 
112
111
  class EcomaxBinaryParameter(BinaryParameter, EcomaxParameter):
113
112
  """Represents an ecoMAX binary parameter."""
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  from collections.abc import Generator
5
5
  from dataclasses import dataclass
6
- from typing import TYPE_CHECKING, Any, Final
6
+ from typing import TYPE_CHECKING, Any, Final, cast
7
7
 
8
8
  from pyplumio.const import (
9
9
  ATTR_DEVICE_INDEX,
@@ -13,7 +13,7 @@ from pyplumio.const import (
13
13
  UnitOfMeasurement,
14
14
  )
15
15
  from pyplumio.frames import Request
16
- from pyplumio.helpers.factory import factory
16
+ from pyplumio.helpers.factory import create_instance
17
17
  from pyplumio.helpers.parameter import (
18
18
  BinaryParameter,
19
19
  BinaryParameterDescription,
@@ -42,6 +42,21 @@ class MixerParameter(Parameter):
42
42
  device: Mixer
43
43
  description: MixerParameterDescription
44
44
 
45
+ async def create_request(self) -> Request:
46
+ """Create a request to change the parameter."""
47
+ return cast(
48
+ Request,
49
+ await create_instance(
50
+ "frames.requests.SetMixerParameterRequest",
51
+ recipient=self.device.parent.address,
52
+ data={
53
+ ATTR_INDEX: self._index,
54
+ ATTR_VALUE: self.values.value,
55
+ ATTR_DEVICE_INDEX: self.device.index,
56
+ },
57
+ ),
58
+ )
59
+
45
60
  async def set(self, value: ParameterValueType, retries: int = 5) -> bool:
46
61
  """Set a parameter value."""
47
62
  if isinstance(value, (int, float)):
@@ -70,20 +85,6 @@ class MixerParameter(Parameter):
70
85
  self.values.max_value - self.description.offset
71
86
  ) * self.description.multiplier
72
87
 
73
- @property
74
- def request(self) -> Request:
75
- """Return request to change the parameter."""
76
- request: Request = factory(
77
- "frames.requests.SetMixerParameterRequest",
78
- recipient=self.device.parent.address,
79
- data={
80
- ATTR_INDEX: self._index,
81
- ATTR_VALUE: self.values.value,
82
- ATTR_DEVICE_INDEX: self.device.index,
83
- },
84
- )
85
- return request
86
-
87
88
 
88
89
  class MixerBinaryParameter(BinaryParameter, MixerParameter):
89
90
  """Represents a mixer binary parameter."""
@@ -5,13 +5,13 @@ from collections.abc import Sequence
5
5
  from dataclasses import dataclass
6
6
  from functools import reduce
7
7
  from itertools import chain
8
- from typing import Any, Final
8
+ from typing import Any, Final, cast
9
9
 
10
10
  from pyplumio.const import ATTR_PARAMETER, ATTR_SCHEDULE, ATTR_SWITCH, ATTR_TYPE
11
11
  from pyplumio.devices import AddressableDevice, Device
12
12
  from pyplumio.exceptions import FrameDataError
13
13
  from pyplumio.frames import Request
14
- from pyplumio.helpers.factory import factory
14
+ from pyplumio.helpers.factory import create_instance
15
15
  from pyplumio.helpers.parameter import (
16
16
  BinaryParameter,
17
17
  BinaryParameterDescription,
@@ -81,16 +81,17 @@ class ScheduleParameter(Parameter):
81
81
 
82
82
  device: AddressableDevice
83
83
 
84
- @property
85
- def request(self) -> Request:
86
- """Return request to change the parameter."""
84
+ async def create_request(self) -> Request:
85
+ """Create a request to change the parameter."""
87
86
  schedule_name, _ = self.description.name.split("_schedule_", 1)
88
- request: Request = factory(
89
- "frames.requests.SetScheduleRequest",
90
- recipient=self.device.address,
91
- data=collect_schedule_data(schedule_name, self.device),
87
+ return cast(
88
+ Request,
89
+ await create_instance(
90
+ "frames.requests.SetScheduleRequest",
91
+ recipient=self.device.address,
92
+ data=collect_schedule_data(schedule_name, self.device),
93
+ ),
92
94
  )
93
- return request
94
95
 
95
96
 
96
97
  class ScheduleBinaryParameter(ScheduleParameter, BinaryParameter):
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  from collections.abc import Generator
5
5
  from dataclasses import dataclass
6
- from typing import TYPE_CHECKING, Any, Final
6
+ from typing import TYPE_CHECKING, Any, Final, cast
7
7
 
8
8
  from pyplumio.const import (
9
9
  ATTR_INDEX,
@@ -14,7 +14,7 @@ from pyplumio.const import (
14
14
  )
15
15
  from pyplumio.devices import AddressableDevice
16
16
  from pyplumio.frames import Request
17
- from pyplumio.helpers.factory import factory
17
+ from pyplumio.helpers.factory import create_instance
18
18
  from pyplumio.helpers.parameter import (
19
19
  BinaryParameter,
20
20
  BinaryParameterDescription,
@@ -59,6 +59,24 @@ class ThermostatParameter(Parameter):
59
59
  self.offset = offset
60
60
  super().__init__(device, values, description, index)
61
61
 
62
+ async def create_request(self) -> Request:
63
+ """Create a request to change the parameter."""
64
+ return cast(
65
+ Request,
66
+ await create_instance(
67
+ "frames.requests.SetThermostatParameterRequest",
68
+ recipient=self.device.parent.address,
69
+ data={
70
+ # Increase the index by one to account for thermostat
71
+ # profile, which is being set at ecoMAX device level.
72
+ ATTR_INDEX: self._index + 1,
73
+ ATTR_VALUE: self.values.value,
74
+ ATTR_OFFSET: self.offset,
75
+ ATTR_SIZE: self.description.size,
76
+ },
77
+ ),
78
+ )
79
+
62
80
  async def set(self, value: ParameterValueType, retries: int = 5) -> bool:
63
81
  """Set a parameter value."""
64
82
  if isinstance(value, (int, float)):
@@ -81,23 +99,6 @@ class ThermostatParameter(Parameter):
81
99
  """Return the maximum allowed value."""
82
100
  return self.values.max_value * self.description.multiplier
83
101
 
84
- @property
85
- def request(self) -> Request:
86
- """Return request to change the parameter."""
87
- request: Request = factory(
88
- "frames.requests.SetThermostatParameterRequest",
89
- recipient=self.device.parent.address,
90
- data={
91
- # Increase the index by one to account for thermostat
92
- # profile, which is being set at ecoMAX device level.
93
- ATTR_INDEX: self._index + 1,
94
- ATTR_VALUE: self.values.value,
95
- ATTR_OFFSET: self.offset,
96
- ATTR_SIZE: self.description.size,
97
- },
98
- )
99
- return request
100
-
101
102
 
102
103
  class ThermostatBinaryParameter(BinaryParameter, ThermostatParameter):
103
104
  """Represents a thermostat binary parameter."""