PyPlumIO 0.5.33__tar.gz → 0.5.35__tar.gz

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 (142) hide show
  1. {pyplumio-0.5.33 → pyplumio-0.5.35}/PKG-INFO +1 -1
  2. {pyplumio-0.5.33 → pyplumio-0.5.35}/PyPlumIO.egg-info/PKG-INFO +1 -1
  3. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/_version.py +2 -2
  4. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/devices/__init__.py +9 -5
  5. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/devices/ecomax.py +5 -2
  6. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/exceptions.py +4 -0
  7. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/helpers/factory.py +5 -2
  8. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/helpers/parameter.py +5 -3
  9. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/helpers/schedule.py +6 -3
  10. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/stream.py +14 -9
  11. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/helpers/test_factory.py +1 -1
  12. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/helpers/test_parameter.py +8 -2
  13. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/test_devices.py +5 -5
  14. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/test_stream.py +7 -4
  15. {pyplumio-0.5.33 → pyplumio-0.5.35}/.gitattributes +0 -0
  16. {pyplumio-0.5.33 → pyplumio-0.5.35}/.github/CODE_OF_CONDUCT.md +0 -0
  17. {pyplumio-0.5.33 → pyplumio-0.5.35}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  18. {pyplumio-0.5.33 → pyplumio-0.5.35}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  19. {pyplumio-0.5.33 → pyplumio-0.5.35}/.github/dependabot.yml +0 -0
  20. {pyplumio-0.5.33 → pyplumio-0.5.35}/.github/workflows/ci.yml +0 -0
  21. {pyplumio-0.5.33 → pyplumio-0.5.35}/.github/workflows/codeql-analysis.yml +0 -0
  22. {pyplumio-0.5.33 → pyplumio-0.5.35}/.github/workflows/deploy.yml +0 -0
  23. {pyplumio-0.5.33 → pyplumio-0.5.35}/.github/workflows/documentation.yml +0 -0
  24. {pyplumio-0.5.33 → pyplumio-0.5.35}/.gitignore +0 -0
  25. {pyplumio-0.5.33 → pyplumio-0.5.35}/.pre-commit-config.yaml +0 -0
  26. {pyplumio-0.5.33 → pyplumio-0.5.35}/.vscode/settings.json +0 -0
  27. {pyplumio-0.5.33 → pyplumio-0.5.35}/LICENSE +0 -0
  28. {pyplumio-0.5.33 → pyplumio-0.5.35}/MANIFEST.in +0 -0
  29. {pyplumio-0.5.33 → pyplumio-0.5.35}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  30. {pyplumio-0.5.33 → pyplumio-0.5.35}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  31. {pyplumio-0.5.33 → pyplumio-0.5.35}/PyPlumIO.egg-info/requires.txt +0 -0
  32. {pyplumio-0.5.33 → pyplumio-0.5.35}/PyPlumIO.egg-info/top_level.txt +0 -0
  33. {pyplumio-0.5.33 → pyplumio-0.5.35}/README.md +0 -0
  34. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/Makefile +0 -0
  35. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/make.bat +0 -0
  36. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/callbacks.rst +0 -0
  37. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/conf.py +0 -0
  38. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/connecting.rst +0 -0
  39. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/frames.rst +0 -0
  40. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/index.rst +0 -0
  41. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/mixers_thermostats.rst +0 -0
  42. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/protocol.rst +0 -0
  43. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/reading.rst +0 -0
  44. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/schedules.rst +0 -0
  45. {pyplumio-0.5.33 → pyplumio-0.5.35}/docs/source/writing.rst +0 -0
  46. {pyplumio-0.5.33 → pyplumio-0.5.35}/images/ecomax.png +0 -0
  47. {pyplumio-0.5.33 → pyplumio-0.5.35}/images/rs485.png +0 -0
  48. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/__init__.py +0 -0
  49. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/__main__.py +0 -0
  50. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/connection.py +0 -0
  51. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/const.py +0 -0
  52. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/devices/ecoster.py +0 -0
  53. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/devices/mixer.py +0 -0
  54. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/devices/thermostat.py +0 -0
  55. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/filters.py +0 -0
  56. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/frames/__init__.py +0 -0
  57. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/frames/messages.py +0 -0
  58. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/frames/requests.py +0 -0
  59. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/frames/responses.py +0 -0
  60. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/helpers/__init__.py +0 -0
  61. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/helpers/data_types.py +0 -0
  62. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/helpers/event_manager.py +0 -0
  63. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/helpers/task_manager.py +0 -0
  64. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/helpers/timeout.py +0 -0
  65. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/helpers/uid.py +0 -0
  66. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/protocol.py +0 -0
  67. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/py.typed +0 -0
  68. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/__init__.py +0 -0
  69. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/alerts.py +0 -0
  70. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/boiler_load.py +0 -0
  71. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/boiler_power.py +0 -0
  72. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/ecomax_parameters.py +0 -0
  73. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/fan_power.py +0 -0
  74. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/frame_versions.py +0 -0
  75. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/fuel_consumption.py +0 -0
  76. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/fuel_level.py +0 -0
  77. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/lambda_sensor.py +0 -0
  78. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/mixer_parameters.py +0 -0
  79. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/mixer_sensors.py +0 -0
  80. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/modules.py +0 -0
  81. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/network_info.py +0 -0
  82. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/output_flags.py +0 -0
  83. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/outputs.py +0 -0
  84. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/pending_alerts.py +0 -0
  85. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/product_info.py +0 -0
  86. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/program_version.py +0 -0
  87. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/regulator_data.py +0 -0
  88. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/regulator_data_schema.py +0 -0
  89. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/schedules.py +0 -0
  90. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/statuses.py +0 -0
  91. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/temperatures.py +0 -0
  92. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/thermostat_parameters.py +0 -0
  93. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/structures/thermostat_sensors.py +0 -0
  94. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyplumio/utils.py +0 -0
  95. {pyplumio-0.5.33 → pyplumio-0.5.35}/pyproject.toml +0 -0
  96. {pyplumio-0.5.33 → pyplumio-0.5.35}/requirements.txt +0 -0
  97. {pyplumio-0.5.33 → pyplumio-0.5.35}/requirements_docs.txt +0 -0
  98. {pyplumio-0.5.33 → pyplumio-0.5.35}/requirements_test.txt +0 -0
  99. {pyplumio-0.5.33 → pyplumio-0.5.35}/setup.cfg +0 -0
  100. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/__init__.py +0 -0
  101. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/conftest.py +0 -0
  102. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/frames/test_init.py +0 -0
  103. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/frames/test_messages.py +0 -0
  104. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/frames/test_requests.py +0 -0
  105. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/frames/test_responses.py +0 -0
  106. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/helpers/__init__.py +0 -0
  107. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/helpers/test_data_types.py +0 -0
  108. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/helpers/test_event_manager.py +0 -0
  109. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/helpers/test_schedule.py +0 -0
  110. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/helpers/test_task_manager.py +0 -0
  111. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/helpers/test_timeout.py +0 -0
  112. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/helpers/test_uid.py +0 -0
  113. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/ruff.toml +0 -0
  114. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/test_connection.py +0 -0
  115. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/test_filters.py +0 -0
  116. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/test_init.py +0 -0
  117. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/test_main.py +0 -0
  118. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/test_protocol.py +0 -0
  119. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/test_utils.py +0 -0
  120. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/messages/regulator_data.json +0 -0
  121. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/messages/sensor_data.json +0 -0
  122. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/requests/alerts.json +0 -0
  123. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/requests/ecomax_control.json +0 -0
  124. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/requests/ecomax_parameters.json +0 -0
  125. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/requests/mixer_parameters.json +0 -0
  126. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  127. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  128. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/requests/set_schedule.json +0 -0
  129. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  130. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/requests/thermostat_parameters.json +0 -0
  131. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/alerts.json +0 -0
  132. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/device_available.json +0 -0
  133. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/ecomax_parameters.json +0 -0
  134. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/mixer_parameters.json +0 -0
  135. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/password.json +0 -0
  136. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/program_version.json +0 -0
  137. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/regulator_data_schema.json +0 -0
  138. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/schedules.json +0 -0
  139. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/thermostat_parameters.json +0 -0
  140. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/responses/uid.json +0 -0
  141. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  142. {pyplumio-0.5.33 → pyplumio-0.5.35}/tests/testdata/unknown/unknown_mixer_parameter.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyPlumIO
3
- Version: 0.5.33
3
+ Version: 0.5.35
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyPlumIO
3
- Version: 0.5.33
3
+ Version: 0.5.35
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
@@ -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.33'
16
- __version_tuple__ = version_tuple = (0, 5, 33)
15
+ __version__ = version = '0.5.35'
16
+ __version_tuple__ = version_tuple = (0, 5, 35)
@@ -9,7 +9,7 @@ import logging
9
9
  from typing import Any, ClassVar
10
10
 
11
11
  from pyplumio.const import ATTR_FRAME_ERRORS, ATTR_LOADED, DeviceType, FrameType
12
- from pyplumio.exceptions import UnknownDeviceError
12
+ from pyplumio.exceptions import RequestError, UnknownDeviceError
13
13
  from pyplumio.frames import DataFrameDescription, Frame, Request, is_known_frame_type
14
14
  from pyplumio.helpers.event_manager import EventManager
15
15
  from pyplumio.helpers.factory import create_instance
@@ -82,7 +82,7 @@ class Device(ABC, EventManager):
82
82
  """
83
83
  parameter = await self.get(name, timeout)
84
84
  if not isinstance(parameter, Parameter):
85
- raise TypeError(f"{name} is not valid parameter")
85
+ raise TypeError(f"The parameter '{name}' is not valid or does not exist.")
86
86
 
87
87
  return await parameter.set(value, retries=retries)
88
88
 
@@ -143,7 +143,7 @@ class PhysicalDevice(Device, ABC):
143
143
  and not self.has_frame_version(frame_type, version)
144
144
  ):
145
145
  _LOGGER.debug(
146
- "Updating frame %s to version %i", repr(frame_type), version
146
+ "Updating frame %s to version %i", frame_type, version
147
147
  )
148
148
  request = await Request.create(frame_type, recipient=self.address)
149
149
  self.queue.put_nowait(request)
@@ -180,7 +180,7 @@ class PhysicalDevice(Device, ABC):
180
180
  )
181
181
 
182
182
  errors = [
183
- result.args[1] for result in results if isinstance(result, BaseException)
183
+ result.args[1] for result in results if isinstance(result, RequestError)
184
184
  ]
185
185
 
186
186
  await asyncio.gather(
@@ -203,7 +203,11 @@ class PhysicalDevice(Device, ABC):
203
203
  except asyncio.TimeoutError:
204
204
  retries -= 1
205
205
 
206
- raise ValueError(f'could not request "{name}"', frame_type)
206
+ raise RequestError(
207
+ f"Failed to request parameter '{name}' with frame type '{frame_type}' "
208
+ f"after {retries} retries.",
209
+ frame_type,
210
+ )
207
211
 
208
212
  @classmethod
209
213
  async def create(cls, device_type: int, **kwargs: Any) -> PhysicalDevice:
@@ -98,6 +98,8 @@ SETUP_FRAME_TYPES: tuple[DataFrameDescription, ...] = (
98
98
 
99
99
  _LOGGER = logging.getLogger(__name__)
100
100
 
101
+ ecomax_control_error = "ecoMAX control is not available. Please try again later."
102
+
101
103
 
102
104
  class EcoMAX(PhysicalDevice):
103
105
  """Represents an ecoMAX controller."""
@@ -192,6 +194,7 @@ class EcoMAX(PhysicalDevice):
192
194
  values,
193
195
  product.model,
194
196
  )
197
+ return
195
198
 
196
199
  handler = (
197
200
  EcomaxSwitch
@@ -393,7 +396,7 @@ class EcoMAX(PhysicalDevice):
393
396
  ecomax_control: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
394
397
  return await ecomax_control.turn_on()
395
398
  except KeyError:
396
- _LOGGER.error("ecoMAX control isn't available, please try later")
399
+ _LOGGER.error(ecomax_control_error)
397
400
  return False
398
401
 
399
402
  async def turn_off(self) -> bool:
@@ -402,7 +405,7 @@ class EcoMAX(PhysicalDevice):
402
405
  ecomax_control: EcomaxSwitch = self.data[ATTR_ECOMAX_CONTROL]
403
406
  return await ecomax_control.turn_off()
404
407
  except KeyError:
405
- _LOGGER.error("ecoMAX control isn't available, please try later")
408
+ _LOGGER.error(ecomax_control_error)
406
409
  return False
407
410
 
408
411
  def turn_on_nowait(self) -> None:
@@ -15,6 +15,10 @@ class ProtocolError(PyPlumIOError):
15
15
  """Base class for protocol-related errors."""
16
16
 
17
17
 
18
+ class RequestError(PyPlumIOError):
19
+ """Raised on request error."""
20
+
21
+
18
22
  class ReadError(ProtocolError):
19
23
  """Raised on read error."""
20
24
 
@@ -26,9 +26,12 @@ async def create_instance(class_path: str, cls: type[T], **kwargs: Any) -> T:
26
26
  module = await _import_module(module_name)
27
27
  instance = getattr(module, class_name)(**kwargs)
28
28
  if not isinstance(instance, cls):
29
- raise TypeError(f"class '{class_name}' should be derived from {cls}")
29
+ raise TypeError(
30
+ f"Expected instance of '{cls.__name__}', but got "
31
+ f"'{type(instance).__name__}' from '{class_name}'"
32
+ )
30
33
 
31
34
  return instance
32
35
  except Exception:
33
- _LOGGER.error("Failed to load module (%s)", class_path)
36
+ _LOGGER.exception("Failed to create instance for class path '%s'", class_path)
34
37
  raise
@@ -189,7 +189,8 @@ class Parameter(ABC):
189
189
  value = _normalize_parameter_value(value)
190
190
  if value < self.values.min_value or value > self.values.max_value:
191
191
  raise ValueError(
192
- f"Value must be between '{self.min_value}' and '{self.max_value}'"
192
+ f"Invalid value: {value}. Must be between "
193
+ f"{self.min_value} and {self.max_value}."
193
194
  )
194
195
 
195
196
  return value
@@ -215,9 +216,10 @@ class Parameter(ABC):
215
216
  self._pending_update = True
216
217
  while self.pending_update:
217
218
  if retries <= 0:
218
- _LOGGER.error(
219
- "Timed out while trying to set '%s' parameter",
219
+ _LOGGER.warning(
220
+ "Unable to confirm that parameter '%s' was set after %d retries",
220
221
  self.description.name,
222
+ retries,
221
223
  )
222
224
  return False
223
225
 
@@ -50,8 +50,8 @@ def _get_time_range(
50
50
 
51
51
  if end_dt <= start_dt:
52
52
  raise ValueError(
53
- f"Invalid interval ({start}, {end}). "
54
- "Lower boundary must be less than upper."
53
+ f"Invalid time range: start time ({start}) must be earlier "
54
+ f"than end time ({end})."
55
55
  )
56
56
 
57
57
  def _dt_to_index(dt: dt.datetime) -> int:
@@ -107,7 +107,10 @@ class ScheduleDay(MutableMapping):
107
107
  ) -> None:
108
108
  """Set a schedule interval state."""
109
109
  if state not in get_args(ScheduleState):
110
- raise ValueError(f'state "{state}" is not allowed')
110
+ raise ValueError(
111
+ f"Invalid state '{state}'. Allowed states are: "
112
+ f"{', '.join(get_args(ScheduleState))}"
113
+ )
111
114
 
112
115
  for index in _get_time_range(start, end):
113
116
  self._intervals[index] = True if state in ON_STATES else False
@@ -45,7 +45,7 @@ class FrameWriter:
45
45
  """Send the frame and wait until send buffer is empty."""
46
46
  self._writer.write(frame.bytes)
47
47
  await self._writer.drain()
48
- _LOGGER.debug("Sent frame: %s", frame)
48
+ _LOGGER.debug("Sent frame: %s, bytes: %s", frame, frame.bytes)
49
49
 
50
50
  async def close(self) -> None:
51
51
  """Close the frame writer."""
@@ -53,7 +53,9 @@ class FrameWriter:
53
53
  self._writer.close()
54
54
  await self.wait_closed()
55
55
  except (OSError, asyncio.TimeoutError):
56
- _LOGGER.exception("Unexpected error, while closing the writer")
56
+ _LOGGER.exception(
57
+ "Failed to close the frame writer due to an unexpected error"
58
+ )
57
59
 
58
60
  @timeout(WRITER_TIMEOUT)
59
61
  async def wait_closed(self) -> None:
@@ -96,7 +98,7 @@ class FrameReader:
96
98
  buffer += await self._reader.readexactly(HEADER_SIZE - DELIMITER_SIZE)
97
99
  except IncompleteReadError as e:
98
100
  raise ReadError(
99
- f"Got incomplete header, while trying to read {e.expected} bytes"
101
+ f"Incomplete header, expected {e.expected} bytes"
100
102
  ) from e
101
103
 
102
104
  return Header(*struct_header.unpack_from(buffer)[DELIMITER_SIZE:]), buffer
@@ -123,18 +125,21 @@ class FrameReader:
123
125
  raise UnknownDeviceError(f"Unknown sender type ({sender})")
124
126
 
125
127
  if frame_length > MAX_FRAME_LENGTH or frame_length < MIN_FRAME_LENGTH:
126
- raise ReadError(f"Unexpected frame length ({frame_length})")
128
+ raise ReadError(
129
+ f"Unexpected frame length ({frame_length}), expected between "
130
+ f"{MIN_FRAME_LENGTH} and {MAX_FRAME_LENGTH}"
131
+ )
127
132
 
128
133
  try:
129
134
  buffer += await self._reader.readexactly(frame_length - HEADER_SIZE)
130
135
  except IncompleteReadError as e:
131
- raise ReadError(
132
- f"Got incomplete frame, while trying to read {e.expected} bytes"
133
- ) from e
136
+ raise ReadError(f"Incomplete frame, expected {e.expected} bytes") from e
134
137
 
135
138
  if (checksum := bcc(buffer[:-2])) and checksum != buffer[-2]:
136
139
  raise ChecksumError(
137
- f"Incorrect frame checksum ({checksum} != {buffer[-2]})"
140
+ f"Incorrect frame checksum: calculated {checksum}, "
141
+ f"expected {buffer[-2]}. "
142
+ f"Frame data: {buffer.hex()}"
138
143
  )
139
144
 
140
145
  frame = await Frame.create(
@@ -145,6 +150,6 @@ class FrameReader:
145
150
  econet_version=econet_version,
146
151
  message=buffer[HEADER_SIZE + 1 : -2],
147
152
  )
148
- _LOGGER.debug("Received frame: %s", frame)
153
+ _LOGGER.debug("Received frame: %s, bytes: %s", frame, buffer.hex())
149
154
 
150
155
  return frame
@@ -19,7 +19,7 @@ async def test_get_object_with_incorrect_base_class() -> None:
19
19
  await create_instance("frames.responses.UIDResponse", cls=Request)
20
20
 
21
21
  assert str(exc_info.value) == (
22
- "class 'UIDResponse' should be derived from <class 'pyplumio.frames.Request'>"
22
+ "Expected instance of 'Request', but got 'UIDResponse' from 'UIDResponse'"
23
23
  )
24
24
 
25
25
 
@@ -268,7 +268,10 @@ async def test_number_request_with_unchanged_value(
268
268
  assert number.pending_update
269
269
  assert mock_put.await_count == 3 # type: ignore [unreachable]
270
270
  mock_put.reset_mock()
271
- assert "Timed out while trying to set 'test_number' parameter" in caplog.text
271
+ assert (
272
+ "Unable to confirm that parameter 'test_number' was set after 0 retries"
273
+ in caplog.text
274
+ )
272
275
  await number.set(5)
273
276
  mock_put.assert_not_awaited()
274
277
 
@@ -283,7 +286,10 @@ async def test_switch_request_with_unchanged_value(
283
286
  assert switch.pending_update
284
287
  assert mock_put.await_count == 3 # type: ignore [unreachable]
285
288
  mock_put.reset_mock()
286
- assert "Timed out while trying to set 'test_switch' parameter" in caplog.text
289
+ assert (
290
+ "Unable to confirm that parameter 'test_switch' was set after 0 retries"
291
+ in caplog.text
292
+ )
287
293
  await switch.set(True)
288
294
  mock_put.assert_not_awaited()
289
295
 
@@ -36,7 +36,7 @@ from pyplumio.devices.ecomax import (
36
36
  from pyplumio.devices.ecoster import EcoSTER
37
37
  from pyplumio.devices.mixer import Mixer
38
38
  from pyplumio.devices.thermostat import Thermostat
39
- from pyplumio.exceptions import UnknownDeviceError
39
+ from pyplumio.exceptions import RequestError, UnknownDeviceError
40
40
  from pyplumio.frames import Response
41
41
  from pyplumio.frames.messages import RegulatorDataMessage, SensorDataMessage
42
42
  from pyplumio.frames.requests import (
@@ -144,7 +144,7 @@ async def test_async_setup_error() -> None:
144
144
  patch(
145
145
  "pyplumio.devices.ecomax.EcoMAX.request",
146
146
  side_effect=(
147
- ValueError("test", FrameType.REQUEST_ALERTS),
147
+ RequestError("test", FrameType.REQUEST_ALERTS),
148
148
  True,
149
149
  True,
150
150
  True,
@@ -597,7 +597,7 @@ async def test_request_error(ecomax: EcoMAX) -> None:
597
597
  "pyplumio.devices.ecomax.EcoMAX.get",
598
598
  side_effect=(asyncio.TimeoutError, asyncio.TimeoutError),
599
599
  ),
600
- pytest.raises(ValueError),
600
+ pytest.raises(RequestError),
601
601
  ):
602
602
  await ecomax.request("foo", FrameType.REQUEST_ALERTS, retries=1)
603
603
 
@@ -656,7 +656,7 @@ async def test_set(ecomax: EcoMAX) -> None:
656
656
  async def test_turn_on(ecomax: EcoMAX, caplog) -> None:
657
657
  """Test turning the controller on."""
658
658
  assert not await ecomax.turn_on()
659
- assert "ecoMAX control isn't available" in caplog.text
659
+ assert "ecoMAX control is not available. Please try again later." in caplog.text
660
660
  ecomax.data[ATTR_ECOMAX_CONTROL] = AsyncMock()
661
661
  assert await ecomax.turn_on()
662
662
  ecomax.data[ATTR_ECOMAX_CONTROL].turn_on.assert_awaited_once()
@@ -665,7 +665,7 @@ async def test_turn_on(ecomax: EcoMAX, caplog) -> None:
665
665
  async def test_turn_off(ecomax: EcoMAX, caplog) -> None:
666
666
  """Test turning the controller off."""
667
667
  await ecomax.turn_off()
668
- assert "ecoMAX control isn't available" in caplog.text
668
+ assert "ecoMAX control is not available. Please try again later." in caplog.text
669
669
  ecomax.data[ATTR_ECOMAX_CONTROL] = AsyncMock()
670
670
  await ecomax.turn_off()
671
671
  ecomax.data[ATTR_ECOMAX_CONTROL].turn_off.assert_awaited_once()
@@ -53,7 +53,7 @@ async def test_frame_writer_with_close_error(
53
53
  writer = FrameWriter(mock_stream_writer)
54
54
  getattr(mock_stream_writer, method).side_effect = exception
55
55
  await writer.close()
56
- assert "Unexpected error, while closing the writer" in caplog.text
56
+ assert "Failed to close the frame writer due to an unexpected error" in caplog.text
57
57
 
58
58
 
59
59
  @patch(
@@ -104,7 +104,7 @@ async def test_frame_reader_with_short_header(
104
104
  with pytest.raises(ReadError) as exc_info:
105
105
  await frame_reader.read()
106
106
 
107
- assert "Got incomplete header, while trying to read 7 bytes" in str(exc_info.value)
107
+ assert "Incomplete header, expected 7 bytes" in str(exc_info.value)
108
108
 
109
109
 
110
110
  @patch(
@@ -137,7 +137,7 @@ async def test_frame_reader_with_incomplete_read(
137
137
  with pytest.raises(ReadError) as exc_info:
138
138
  await frame_reader.read()
139
139
 
140
- assert "Got incomplete frame, while trying to read 10 bytes" in str(exc_info.value)
140
+ assert "Incomplete frame, expected 10 bytes" in str(exc_info.value)
141
141
 
142
142
 
143
143
  @patch(
@@ -152,7 +152,10 @@ async def test_frame_reader_with_incorrect_bcc(
152
152
  with pytest.raises(ChecksumError) as exc_info:
153
153
  await frame_reader.read()
154
154
 
155
- assert "Incorrect frame checksum (200 != 201)" in str(exc_info.value)
155
+ assert (
156
+ "Incorrect frame checksum: calculated 200, expected 201. "
157
+ "Frame data: 680c000056300531fe00c916"
158
+ ) in str(exc_info.value)
156
159
 
157
160
 
158
161
  @patch(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes