PyPlumIO 0.5.32__tar.gz → 0.5.34__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.32 → pyplumio-0.5.34}/PKG-INFO +1 -1
  2. {pyplumio-0.5.32 → pyplumio-0.5.34}/PyPlumIO.egg-info/PKG-INFO +1 -1
  3. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/_version.py +2 -2
  4. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/devices/__init__.py +11 -11
  5. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/devices/ecomax.py +5 -2
  6. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/helpers/factory.py +5 -2
  7. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/helpers/parameter.py +5 -3
  8. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/helpers/schedule.py +6 -3
  9. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/stream.py +14 -9
  10. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/helpers/test_factory.py +1 -1
  11. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/helpers/test_parameter.py +2 -2
  12. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/test_devices.py +2 -6
  13. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/test_stream.py +7 -4
  14. {pyplumio-0.5.32 → pyplumio-0.5.34}/.gitattributes +0 -0
  15. {pyplumio-0.5.32 → pyplumio-0.5.34}/.github/CODE_OF_CONDUCT.md +0 -0
  16. {pyplumio-0.5.32 → pyplumio-0.5.34}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  17. {pyplumio-0.5.32 → pyplumio-0.5.34}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  18. {pyplumio-0.5.32 → pyplumio-0.5.34}/.github/dependabot.yml +0 -0
  19. {pyplumio-0.5.32 → pyplumio-0.5.34}/.github/workflows/ci.yml +0 -0
  20. {pyplumio-0.5.32 → pyplumio-0.5.34}/.github/workflows/codeql-analysis.yml +0 -0
  21. {pyplumio-0.5.32 → pyplumio-0.5.34}/.github/workflows/deploy.yml +0 -0
  22. {pyplumio-0.5.32 → pyplumio-0.5.34}/.github/workflows/documentation.yml +0 -0
  23. {pyplumio-0.5.32 → pyplumio-0.5.34}/.gitignore +0 -0
  24. {pyplumio-0.5.32 → pyplumio-0.5.34}/.pre-commit-config.yaml +0 -0
  25. {pyplumio-0.5.32 → pyplumio-0.5.34}/.vscode/settings.json +0 -0
  26. {pyplumio-0.5.32 → pyplumio-0.5.34}/LICENSE +0 -0
  27. {pyplumio-0.5.32 → pyplumio-0.5.34}/MANIFEST.in +0 -0
  28. {pyplumio-0.5.32 → pyplumio-0.5.34}/PyPlumIO.egg-info/SOURCES.txt +0 -0
  29. {pyplumio-0.5.32 → pyplumio-0.5.34}/PyPlumIO.egg-info/dependency_links.txt +0 -0
  30. {pyplumio-0.5.32 → pyplumio-0.5.34}/PyPlumIO.egg-info/requires.txt +0 -0
  31. {pyplumio-0.5.32 → pyplumio-0.5.34}/PyPlumIO.egg-info/top_level.txt +0 -0
  32. {pyplumio-0.5.32 → pyplumio-0.5.34}/README.md +0 -0
  33. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/Makefile +0 -0
  34. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/make.bat +0 -0
  35. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/callbacks.rst +0 -0
  36. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/conf.py +0 -0
  37. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/connecting.rst +0 -0
  38. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/frames.rst +0 -0
  39. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/index.rst +0 -0
  40. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/mixers_thermostats.rst +0 -0
  41. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/protocol.rst +0 -0
  42. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/reading.rst +0 -0
  43. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/schedules.rst +0 -0
  44. {pyplumio-0.5.32 → pyplumio-0.5.34}/docs/source/writing.rst +0 -0
  45. {pyplumio-0.5.32 → pyplumio-0.5.34}/images/ecomax.png +0 -0
  46. {pyplumio-0.5.32 → pyplumio-0.5.34}/images/rs485.png +0 -0
  47. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/__init__.py +0 -0
  48. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/__main__.py +0 -0
  49. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/connection.py +0 -0
  50. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/const.py +0 -0
  51. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/devices/ecoster.py +0 -0
  52. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/devices/mixer.py +0 -0
  53. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/devices/thermostat.py +0 -0
  54. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/exceptions.py +0 -0
  55. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/filters.py +0 -0
  56. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/frames/__init__.py +0 -0
  57. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/frames/messages.py +0 -0
  58. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/frames/requests.py +0 -0
  59. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/frames/responses.py +0 -0
  60. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/helpers/__init__.py +0 -0
  61. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/helpers/data_types.py +0 -0
  62. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/helpers/event_manager.py +0 -0
  63. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/helpers/task_manager.py +0 -0
  64. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/helpers/timeout.py +0 -0
  65. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/helpers/uid.py +0 -0
  66. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/protocol.py +0 -0
  67. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/py.typed +0 -0
  68. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/__init__.py +0 -0
  69. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/alerts.py +0 -0
  70. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/boiler_load.py +0 -0
  71. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/boiler_power.py +0 -0
  72. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/ecomax_parameters.py +0 -0
  73. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/fan_power.py +0 -0
  74. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/frame_versions.py +0 -0
  75. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/fuel_consumption.py +0 -0
  76. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/fuel_level.py +0 -0
  77. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/lambda_sensor.py +0 -0
  78. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/mixer_parameters.py +0 -0
  79. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/mixer_sensors.py +0 -0
  80. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/modules.py +0 -0
  81. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/network_info.py +0 -0
  82. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/output_flags.py +0 -0
  83. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/outputs.py +0 -0
  84. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/pending_alerts.py +0 -0
  85. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/product_info.py +0 -0
  86. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/program_version.py +0 -0
  87. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/regulator_data.py +0 -0
  88. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/regulator_data_schema.py +0 -0
  89. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/schedules.py +0 -0
  90. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/statuses.py +0 -0
  91. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/temperatures.py +0 -0
  92. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/thermostat_parameters.py +0 -0
  93. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/structures/thermostat_sensors.py +0 -0
  94. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyplumio/utils.py +0 -0
  95. {pyplumio-0.5.32 → pyplumio-0.5.34}/pyproject.toml +0 -0
  96. {pyplumio-0.5.32 → pyplumio-0.5.34}/requirements.txt +0 -0
  97. {pyplumio-0.5.32 → pyplumio-0.5.34}/requirements_docs.txt +0 -0
  98. {pyplumio-0.5.32 → pyplumio-0.5.34}/requirements_test.txt +0 -0
  99. {pyplumio-0.5.32 → pyplumio-0.5.34}/setup.cfg +0 -0
  100. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/__init__.py +0 -0
  101. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/conftest.py +0 -0
  102. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/frames/test_init.py +0 -0
  103. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/frames/test_messages.py +0 -0
  104. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/frames/test_requests.py +0 -0
  105. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/frames/test_responses.py +0 -0
  106. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/helpers/__init__.py +0 -0
  107. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/helpers/test_data_types.py +0 -0
  108. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/helpers/test_event_manager.py +0 -0
  109. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/helpers/test_schedule.py +0 -0
  110. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/helpers/test_task_manager.py +0 -0
  111. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/helpers/test_timeout.py +0 -0
  112. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/helpers/test_uid.py +0 -0
  113. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/ruff.toml +0 -0
  114. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/test_connection.py +0 -0
  115. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/test_filters.py +0 -0
  116. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/test_init.py +0 -0
  117. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/test_main.py +0 -0
  118. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/test_protocol.py +0 -0
  119. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/test_utils.py +0 -0
  120. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/messages/regulator_data.json +0 -0
  121. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/messages/sensor_data.json +0 -0
  122. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/requests/alerts.json +0 -0
  123. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/requests/ecomax_control.json +0 -0
  124. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/requests/ecomax_parameters.json +0 -0
  125. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/requests/mixer_parameters.json +0 -0
  126. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/requests/set_ecomax_parameter.json +0 -0
  127. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/requests/set_mixer_parameter.json +0 -0
  128. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/requests/set_schedule.json +0 -0
  129. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/requests/set_thermostat_parameter.json +0 -0
  130. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/requests/thermostat_parameters.json +0 -0
  131. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/alerts.json +0 -0
  132. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/device_available.json +0 -0
  133. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/ecomax_parameters.json +0 -0
  134. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/mixer_parameters.json +0 -0
  135. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/password.json +0 -0
  136. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/program_version.json +0 -0
  137. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/regulator_data_schema.json +0 -0
  138. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/schedules.json +0 -0
  139. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/thermostat_parameters.json +0 -0
  140. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/responses/uid.json +0 -0
  141. {pyplumio-0.5.32 → pyplumio-0.5.34}/tests/testdata/unknown/unknown_ecomax_parameter.json +0 -0
  142. {pyplumio-0.5.32 → pyplumio-0.5.34}/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.32
3
+ Version: 0.5.34
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.32
3
+ Version: 0.5.34
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.32'
16
- __version_tuple__ = version_tuple = (0, 5, 32)
15
+ __version__ = version = '0.5.34'
16
+ __version_tuple__ = version_tuple = (0, 5, 34)
@@ -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)
@@ -151,15 +151,12 @@ class PhysicalDevice(Device, ABC):
151
151
 
152
152
  self.subscribe(ATTR_FRAME_VERSIONS, update_frame_versions)
153
153
 
154
- def has_frame_version(self, frame_type: int, version: int | None = None) -> bool:
154
+ def has_frame_version(self, frame_type: FrameType | int, version: int) -> bool:
155
155
  """Return True if frame data is up to date, False otherwise."""
156
- if frame_type not in self._frame_versions:
157
- return False
158
-
159
- if version is None or self._frame_versions[frame_type] == version:
160
- return True
161
-
162
- return False
156
+ return (
157
+ frame_type in self._frame_versions
158
+ and self._frame_versions[frame_type] == version
159
+ )
163
160
 
164
161
  def supports_frame_type(self, frame_type: int) -> bool:
165
162
  """Check if frame type is supported by the device."""
@@ -206,7 +203,10 @@ class PhysicalDevice(Device, ABC):
206
203
  except asyncio.TimeoutError:
207
204
  retries -= 1
208
205
 
209
- raise ValueError(f'could not request "{name}"', frame_type)
206
+ raise ValueError(
207
+ f"Failed to request parameter '{name}' with frame type '{frame_type}' "
208
+ f"after {retries} retries."
209
+ )
210
210
 
211
211
  @classmethod
212
212
  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:
@@ -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
+ "Failed to set parameter '%s' 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,7 @@ 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 "Failed to set parameter 'test_number' after 0 retries" in caplog.text
272
272
  await number.set(5)
273
273
  mock_put.assert_not_awaited()
274
274
 
@@ -283,7 +283,7 @@ async def test_switch_request_with_unchanged_value(
283
283
  assert switch.pending_update
284
284
  assert mock_put.await_count == 3 # type: ignore [unreachable]
285
285
  mock_put.reset_mock()
286
- assert "Timed out while trying to set 'test_switch' parameter" in caplog.text
286
+ assert "Failed to set parameter 'test_switch' after 0 retries" in caplog.text
287
287
  await switch.set(True)
288
288
  mock_put.assert_not_awaited()
289
289
 
@@ -165,15 +165,11 @@ async def test_async_setup_error() -> None:
165
165
 
166
166
  async def test_frame_versions_update(ecomax: EcoMAX) -> None:
167
167
  """Test requesting updated frames."""
168
- assert not ecomax.has_frame_version(RegulatorDataSchemaRequest.frame_type)
169
168
  test_data = load_json_test_data("messages/sensor_data.json")[0]
170
169
  with patch("asyncio.Queue.put_nowait") as mock_put_nowait:
171
170
  ecomax.handle_frame(SensorDataMessage(message=test_data["message"]))
172
171
  await ecomax.wait_until_done()
173
172
 
174
- assert not ecomax.has_frame_version(RegulatorDataSchemaRequest.frame_type, 0)
175
- assert ecomax.has_frame_version(RegulatorDataSchemaRequest.frame_type, 45559)
176
-
177
173
  mock_put_nowait.assert_has_calls(
178
174
  [
179
175
  call(RegulatorDataSchemaRequest(recipient=DeviceType.ECOMAX)),
@@ -660,7 +656,7 @@ async def test_set(ecomax: EcoMAX) -> None:
660
656
  async def test_turn_on(ecomax: EcoMAX, caplog) -> None:
661
657
  """Test turning the controller on."""
662
658
  assert not await ecomax.turn_on()
663
- assert "ecoMAX control isn't available" in caplog.text
659
+ assert "ecoMAX control is not available. Please try again later." in caplog.text
664
660
  ecomax.data[ATTR_ECOMAX_CONTROL] = AsyncMock()
665
661
  assert await ecomax.turn_on()
666
662
  ecomax.data[ATTR_ECOMAX_CONTROL].turn_on.assert_awaited_once()
@@ -669,7 +665,7 @@ async def test_turn_on(ecomax: EcoMAX, caplog) -> None:
669
665
  async def test_turn_off(ecomax: EcoMAX, caplog) -> None:
670
666
  """Test turning the controller off."""
671
667
  await ecomax.turn_off()
672
- assert "ecoMAX control isn't available" in caplog.text
668
+ assert "ecoMAX control is not available. Please try again later." in caplog.text
673
669
  ecomax.data[ATTR_ECOMAX_CONTROL] = AsyncMock()
674
670
  await ecomax.turn_off()
675
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