scietex.hal.vfd 2.0.1__tar.gz → 2.0.2__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 (44) hide show
  1. {scietex_hal_vfd-2.0.1/src/scietex.hal.vfd.egg-info → scietex_hal_vfd-2.0.2}/PKG-INFO +8 -2
  2. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/README.md +7 -1
  3. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/base/data.py +5 -2
  4. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/intek/spe_b/client.py +29 -15
  5. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/intek/spe_b/rs485.py +14 -4
  6. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/version.py +1 -1
  7. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2/src/scietex.hal.vfd.egg-info}/PKG-INFO +8 -2
  8. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/tests/test_modbus_client.py +39 -3
  9. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/LICENSE +0 -0
  10. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/pyproject.toml +0 -0
  11. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/setup.cfg +0 -0
  12. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/__init__.py +0 -0
  13. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/base/__init__.py +0 -0
  14. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/base/capabilities.py +0 -0
  15. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/base/protocol.py +0 -0
  16. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/base/rs485.py +0 -0
  17. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/intek/__init__.py +0 -0
  18. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/intek/spe_b/__init__.py +0 -0
  19. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/intek/spe_b/errors.py +0 -0
  20. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/intek/spe_b/registers.py +0 -0
  21. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/modbus_drivers.py +0 -0
  22. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/py.typed +0 -0
  23. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/__init__.py +0 -0
  24. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e4_8400/__init__.py +0 -0
  25. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e4_8400/client.py +0 -0
  26. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e4_8400/errors.py +0 -0
  27. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e4_8400/registers.py +0 -0
  28. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e4_8400/rs485.py +0 -0
  29. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e5_8200/__init__.py +0 -0
  30. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e5_8200/client.py +0 -0
  31. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e5_8200/errors.py +0 -0
  32. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e5_8200/registers.py +0 -0
  33. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex/hal/vfd/vesper/e5_8200/rs485.py +0 -0
  34. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex.hal.vfd.egg-info/SOURCES.txt +0 -0
  35. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex.hal.vfd.egg-info/dependency_links.txt +0 -0
  36. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex.hal.vfd.egg-info/entry_points.txt +0 -0
  37. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex.hal.vfd.egg-info/requires.txt +0 -0
  38. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/src/scietex.hal.vfd.egg-info/top_level.txt +0 -0
  39. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/tests/test_modbus_drivers.py +0 -0
  40. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/tests/test_registers.py +0 -0
  41. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/tests/test_version.py +0 -0
  42. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/tests/test_vesper_e4_8400_registers.py +0 -0
  43. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/tests/test_vesper_e5_8200_registers.py +0 -0
  44. {scietex_hal_vfd-2.0.1 → scietex_hal_vfd-2.0.2}/tests/test_vesper_optional_reads.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scietex.hal.vfd
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Scietex VFD HAL package
5
5
  Author-email: Anton Bondarenko <bond.anton@gmail.com>
6
6
  License: MIT License
@@ -86,7 +86,7 @@ A driver reaches a drive over one of two transports:
86
86
 
87
87
  | Field | Intek SPE-B | Vesper E5-8200 | Vesper E4-8400 |
88
88
  | --- | --- | --- | --- |
89
- | `frequency_hz` | yes | yes | yes |
89
+ | `frequency_hz` | yes¹ | yes | yes |
90
90
  | `frequency_percent` | yes | yes | yes |
91
91
  | `output_current` | yes | yes | yes |
92
92
  | `output_voltage` | yes | yes | yes |
@@ -98,6 +98,12 @@ A driver reaches a drive over one of two transports:
98
98
  Only the E4-8400 exposes a live temperature reading (`004Dh`, U1-46). The other
99
99
  two report overheat only as a fault code, and a fault is not a temperature.
100
100
 
101
+ ¹ The SPE-B reports the operating frequency only as a percentage of `P0.10`, so
102
+ `frequency_hz` is `None` until `P0.10` has been read. A drive that refuses
103
+ `P0.10` still reports `frequency_percent`, current, voltage and power; the
104
+ connection stays usable and only the Hz field degrades. `set_frequency` is
105
+ refused in that state, because no percentage can be computed.
106
+
101
107
  ## Installation
102
108
 
103
109
  Install the package via pip (assuming it's published to PyPI):
@@ -38,7 +38,7 @@ A driver reaches a drive over one of two transports:
38
38
 
39
39
  | Field | Intek SPE-B | Vesper E5-8200 | Vesper E4-8400 |
40
40
  | --- | --- | --- | --- |
41
- | `frequency_hz` | yes | yes | yes |
41
+ | `frequency_hz` | yes¹ | yes | yes |
42
42
  | `frequency_percent` | yes | yes | yes |
43
43
  | `output_current` | yes | yes | yes |
44
44
  | `output_voltage` | yes | yes | yes |
@@ -50,6 +50,12 @@ A driver reaches a drive over one of two transports:
50
50
  Only the E4-8400 exposes a live temperature reading (`004Dh`, U1-46). The other
51
51
  two report overheat only as a fault code, and a fault is not a temperature.
52
52
 
53
+ ¹ The SPE-B reports the operating frequency only as a percentage of `P0.10`, so
54
+ `frequency_hz` is `None` until `P0.10` has been read. A drive that refuses
55
+ `P0.10` still reports `frequency_percent`, current, voltage and power; the
56
+ connection stays usable and only the Hz field degrades. `set_frequency` is
57
+ refused in that state, because no percentage can be computed.
58
+
53
59
  ## Installation
54
60
 
55
61
  Install the package via pip (assuming it's published to PyPI):
@@ -44,7 +44,10 @@ class VFDParameters(msgspec.Struct, frozen=True):
44
44
  """Model for VFD operational parameters.
45
45
 
46
46
  Attributes:
47
- frequency_hz: Output frequency in Hz.
47
+ frequency_hz: Output frequency in Hz, or ``None`` when the drive
48
+ reports the frequency only as a percentage and the maximum it is a
49
+ percentage of is unknown. ``None`` rather than 0.0 because the drive
50
+ has not reported a frequency of zero — it has reported nothing.
48
51
  frequency_percent: Output frequency as a percentage of the configured
49
52
  maximum, 0-100.
50
53
  output_current: Output current in Amps.
@@ -61,7 +64,7 @@ class VFDParameters(msgspec.Struct, frozen=True):
61
64
  state: Current VFD state.
62
65
  """
63
66
 
64
- frequency_hz: float # Current operating frequency in Hz
67
+ frequency_hz: float | None # Current operating frequency in Hz
65
68
  frequency_percent: float # Frequency as a percentage of max (0-100%)
66
69
  output_current: float = 0.0 # Output current in Amps
67
70
  output_voltage: float = 0.0 # Output voltage in Volts
@@ -103,11 +103,13 @@ class IntekSPEBModbusVFD(VFDProtocol):
103
103
  Open the TCP connection to the gateway and read the drive's scale factors.
104
104
 
105
105
  Raises:
106
- ModbusOperationError: If the gateway is unreachable, or if P0.10
107
- cannot be read. pymodbus reports a failed connect by returning
108
- ``False`` rather than raising, so the result is checked here;
109
- otherwise an unreachable drive would look connected and only
110
- fail on the first read.
106
+ ModbusOperationError: If the gateway is unreachable. pymodbus
107
+ reports a failed connect by returning ``False`` rather than
108
+ raising, so the result is checked here; otherwise an
109
+ unreachable drive would look connected and only fail on the
110
+ first read. An unreadable P0.10 is not an error: the drive
111
+ still reports its measurements, only ``frequency_hz`` degrades
112
+ to ``None``.
111
113
  """
112
114
  if self._client is None:
113
115
  self._client = AsyncModbusTcpClient(self._host, port=self._port, timeout=self._timeout)
@@ -205,16 +207,25 @@ class IntekSPEBModbusVFD(VFDProtocol):
205
207
  """
206
208
  Read and cache the P0.10 maximum frequency and the Pd.06 resolution.
207
209
 
208
- Raises:
209
- ModbusOperationError: If P0.10 cannot be read, because every
210
- frequency this client reports would then be a guess.
210
+ P0.10 is optional: a drive that refuses it still reports its operating
211
+ percentage, current, voltage and power, so the connection stays usable
212
+ and only ``frequency_hz`` degrades to ``None``. Raising here would make
213
+ the poll loop log a connect failure every cycle and publish nothing at
214
+ all, which is what an SPE-B that has not been configured for Modbus
215
+ (Pd.01/Pd.05) does — it answers the gateway with exception 11.
211
216
  """
212
- raw = await self._read_registers(registers.PARAM_MAX_FREQUENCY_EEPROM, 1)
213
- self._max_frequency_hz = raw[0] / registers.PARAMETER_FREQUENCY_RESOLUTION
214
- if self._max_frequency_hz <= 0:
217
+ try:
218
+ raw = await self._read_registers(registers.PARAM_MAX_FREQUENCY_EEPROM, 1)
219
+ except ModbusOperationError as exc:
215
220
  self.logger.warning(
216
- "%s: P0.10 reads %d, frequency is reported as 0 Hz.", self.label, raw[0]
221
+ "%s: P0.10 is unreadable (%s), frequency is reported as None.", self.label, exc
217
222
  )
223
+ else:
224
+ self._max_frequency_hz = raw[0] / registers.PARAMETER_FREQUENCY_RESOLUTION
225
+ if self._max_frequency_hz <= 0:
226
+ self.logger.warning(
227
+ "%s: P0.10 reads %d, frequency is reported as None.", self.label, raw[0]
228
+ )
218
229
  # Pd.06 only refines the current reading, so a drive that refuses the
219
230
  # read keeps working with the factory resolution instead of failing
220
231
  # the connection: stop and fault reset must stay available.
@@ -229,7 +240,7 @@ class IntekSPEBModbusVFD(VFDProtocol):
229
240
  else registers.CURRENT_RESOLUTION_FINE
230
241
  )
231
242
 
232
- def _percent_to_hz(self, percent: float) -> float:
243
+ def _percent_to_hz(self, percent: float) -> float | None:
233
244
  """
234
245
  Convert an operating frequency percentage into Hz.
235
246
 
@@ -237,10 +248,13 @@ class IntekSPEBModbusVFD(VFDProtocol):
237
248
  percent: Frequency as reported by the drive.
238
249
 
239
250
  Returns:
240
- float: The frequency in Hz, or 0.0 while P0.10 is unknown.
251
+ float | None: The frequency in Hz, or ``None`` while P0.10 is
252
+ unknown. ``None`` rather than 0.0 because a drive that refuses
253
+ P0.10 has not reported a frequency of zero — it has reported
254
+ nothing, and a caller must be able to tell the two apart.
241
255
  """
242
256
  if self._max_frequency_hz <= 0:
243
- return 0.0
257
+ return None
244
258
  return registers.percent_to_hz(percent, self._max_frequency_hz)
245
259
 
246
260
  async def read_parameters(self) -> VFDParameters:
@@ -53,12 +53,22 @@ class SPEvB(VFD):
53
53
  self._current_resolution = registers.CURRENT_RESOLUTION_FINE
54
54
 
55
55
  async def _read_scale_factors(self) -> None:
56
- """Read P0.10 and Pd.06 from the drive once and cache them."""
57
- raw = await self.read_register(registers.PARAM_MAX_FREQUENCY_EEPROM, holding=True)
56
+ """
57
+ Read P0.10 and Pd.06 from the drive once and cache them.
58
+
59
+ P0.10 is optional: a drive that refuses it still reports its operating
60
+ percentage, current, voltage and power, so only ``frequency_hz``
61
+ degrades to ``None``. Raising here would fail the whole snapshot.
62
+ """
63
+ raw = await self.read_register(
64
+ registers.PARAM_MAX_FREQUENCY_EEPROM,
65
+ holding=True,
66
+ raise_on_error=False,
67
+ )
58
68
  self._max_frequency_hz = (raw or 0) / registers.PARAMETER_FREQUENCY_RESOLUTION
59
69
  if self._max_frequency_hz <= 0:
60
70
  self.logger.warning(
61
- "%s: P0.10 reads %s, frequency is reported as 0 Hz.", self.label, raw
71
+ "%s: P0.10 reads %s, frequency is reported as None.", self.label, raw
62
72
  )
63
73
  # Pd.06 only refines the current reading, so a drive that refuses the
64
74
  # read keeps working with the factory resolution.
@@ -114,7 +124,7 @@ class SPEvB(VFD):
114
124
  frequency_hz = (
115
125
  registers.percent_to_hz(percent, self._max_frequency_hz)
116
126
  if self._max_frequency_hz
117
- else 0.0
127
+ else None
118
128
  )
119
129
  state = await self.read_state()
120
130
  return VFDParameters(
@@ -5,4 +5,4 @@ Attributes:
5
5
  __version__ (str): The current version of the package.
6
6
  """
7
7
 
8
- __version__ = "2.0.1"
8
+ __version__ = "2.0.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scietex.hal.vfd
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Scietex VFD HAL package
5
5
  Author-email: Anton Bondarenko <bond.anton@gmail.com>
6
6
  License: MIT License
@@ -86,7 +86,7 @@ A driver reaches a drive over one of two transports:
86
86
 
87
87
  | Field | Intek SPE-B | Vesper E5-8200 | Vesper E4-8400 |
88
88
  | --- | --- | --- | --- |
89
- | `frequency_hz` | yes | yes | yes |
89
+ | `frequency_hz` | yes¹ | yes | yes |
90
90
  | `frequency_percent` | yes | yes | yes |
91
91
  | `output_current` | yes | yes | yes |
92
92
  | `output_voltage` | yes | yes | yes |
@@ -98,6 +98,12 @@ A driver reaches a drive over one of two transports:
98
98
  Only the E4-8400 exposes a live temperature reading (`004Dh`, U1-46). The other
99
99
  two report overheat only as a fault code, and a fault is not a temperature.
100
100
 
101
+ ¹ The SPE-B reports the operating frequency only as a percentage of `P0.10`, so
102
+ `frequency_hz` is `None` until `P0.10` has been read. A drive that refuses
103
+ `P0.10` still reports `frequency_percent`, current, voltage and power; the
104
+ connection stays usable and only the Hz field degrades. `set_frequency` is
105
+ refused in that state, because no percentage can be computed.
106
+
101
107
  ## Installation
102
108
 
103
109
  Install the package via pip (assuming it's published to PyPI):
@@ -159,12 +159,48 @@ def test_read_parameters_uses_the_coarse_current_resolution():
159
159
  assert asyncio.run(vfd.read_parameters()).output_current == pytest.approx(12.3)
160
160
 
161
161
 
162
- def test_read_parameters_reports_zero_hz_when_p010_is_zero():
163
- """An unreadable maximum frequency keeps the percentage but cannot scale it."""
162
+ def test_read_parameters_reports_no_hz_when_p010_is_zero():
163
+ """An unusable maximum frequency keeps the percentage but cannot scale it."""
164
164
  vfd, _ = _connected([_read([5000, 0, 0, 0, 0]), _read([3])], max_frequency_raw=0)
165
165
  parameters = asyncio.run(vfd.read_parameters())
166
166
  assert parameters.frequency_percent == pytest.approx(50.0)
167
- assert parameters.frequency_hz == pytest.approx(0.0)
167
+ assert parameters.frequency_hz is None
168
+
169
+
170
+ def test_connect_survives_a_refused_p010_and_still_reports_measurements():
171
+ """A drive that refuses P0.10 must not blank the whole snapshot.
172
+
173
+ An SPE-B that has not been configured for Modbus (Pd.01/Pd.05) answers the
174
+ gateway with exception 11 for every register. Raising from `connect()` made
175
+ the poll loop log a connect failure every cycle and publish nothing; the
176
+ percentage, current, voltage and power are still readable, so only
177
+ `frequency_hz` degrades to None.
178
+ """
179
+ stub = _StubClient(
180
+ [
181
+ ExceptionResponse(function_code=3), # P0.10
182
+ _read([1]), # Pd.06 = coarse, 0.1 A
183
+ _read([5000, 540, 380, 123, 45]),
184
+ _read([1]), # status: running forward
185
+ ]
186
+ )
187
+ vfd = IntekSPEBModbusVFD("127.0.0.1")
188
+ vfd._client = stub
189
+ asyncio.run(vfd.connect())
190
+ parameters = asyncio.run(vfd.read_parameters())
191
+ assert parameters.frequency_percent == pytest.approx(50.0)
192
+ assert parameters.frequency_hz is None
193
+ assert parameters.output_current == pytest.approx(12.3)
194
+ assert parameters.output_voltage == pytest.approx(380.0)
195
+ assert parameters.dc_bus_voltage == pytest.approx(540.0)
196
+ assert parameters.state is VFDState.RUNNING_FORWARD
197
+
198
+
199
+ def test_set_frequency_still_refuses_when_p010_is_unknown():
200
+ """Without P0.10 no percentage can be computed, so the write is rejected."""
201
+ vfd, _ = _connected([], max_frequency_raw=0)
202
+ with pytest.raises(ValueError, match="P0.10"):
203
+ asyncio.run(vfd.set_frequency(25.0))
168
204
 
169
205
 
170
206
  def test_read_parameters_survives_an_unreadable_status_register():
File without changes