scietex.hal.vfd 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scietex/hal/vfd/__init__.py +3 -0
- scietex/hal/vfd/base/__init__.py +47 -0
- scietex/hal/vfd/base/capabilities.py +49 -0
- scietex/hal/vfd/base/data.py +122 -0
- scietex/hal/vfd/base/protocol.py +129 -0
- scietex/hal/vfd/base/rs485.py +153 -0
- scietex/hal/vfd/intek/__init__.py +5 -0
- scietex/hal/vfd/intek/spe_b/__init__.py +26 -0
- scietex/hal/vfd/intek/spe_b/client.py +384 -0
- scietex/hal/vfd/intek/spe_b/errors.py +67 -0
- scietex/hal/vfd/intek/spe_b/registers.py +238 -0
- scietex/hal/vfd/intek/spe_b/rs485.py +166 -0
- scietex/hal/vfd/modbus_drivers.py +60 -0
- scietex/hal/vfd/py.typed +0 -0
- scietex/hal/vfd/version.py +8 -0
- scietex/hal/vfd/vesper/__init__.py +11 -0
- scietex/hal/vfd/vesper/e4_8400/__init__.py +35 -0
- scietex/hal/vfd/vesper/e4_8400/client.py +402 -0
- scietex/hal/vfd/vesper/e4_8400/errors.py +99 -0
- scietex/hal/vfd/vesper/e4_8400/registers.py +316 -0
- scietex/hal/vfd/vesper/e4_8400/rs485.py +289 -0
- scietex/hal/vfd/vesper/e5_8200/__init__.py +28 -0
- scietex/hal/vfd/vesper/e5_8200/client.py +338 -0
- scietex/hal/vfd/vesper/e5_8200/errors.py +77 -0
- scietex/hal/vfd/vesper/e5_8200/registers.py +235 -0
- scietex/hal/vfd/vesper/e5_8200/rs485.py +216 -0
- scietex_hal_vfd-2.0.0.dist-info/METADATA +222 -0
- scietex_hal_vfd-2.0.0.dist-info/RECORD +32 -0
- scietex_hal_vfd-2.0.0.dist-info/WHEEL +5 -0
- scietex_hal_vfd-2.0.0.dist-info/entry_points.txt +9 -0
- scietex_hal_vfd-2.0.0.dist-info/licenses/LICENSE +21 -0
- scietex_hal_vfd-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base VFD subpackage.
|
|
3
|
+
|
|
4
|
+
Provides the transport-agnostic driver contract, the capability model, the
|
|
5
|
+
shared data structures, and the RS485 base class the direct-serial drivers
|
|
6
|
+
derive from.
|
|
7
|
+
|
|
8
|
+
Classes:
|
|
9
|
+
VFD: Base class for drivers that own a serial connection.
|
|
10
|
+
VFDCapabilities: The control surface of one drive.
|
|
11
|
+
VFDError: A fault code paired with its message.
|
|
12
|
+
VFDParameters: A snapshot of the drive's operating parameters.
|
|
13
|
+
VFDProtocol: The contract every driver implements.
|
|
14
|
+
VFDState: The drive run state.
|
|
15
|
+
|
|
16
|
+
Modules:
|
|
17
|
+
protocol: Defines the `VFDProtocol` driver contract.
|
|
18
|
+
capabilities: Defines the `VFDCapabilities` model.
|
|
19
|
+
data: Defines the data structures shared by every driver.
|
|
20
|
+
rs485: Defines the `VFD` serial base class.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .capabilities import VFDCapabilities
|
|
24
|
+
from .data import (
|
|
25
|
+
VFDClearErrorCMD,
|
|
26
|
+
VFDError,
|
|
27
|
+
VFDParameters,
|
|
28
|
+
VFDSetFrequencyCMD,
|
|
29
|
+
VFDStartCMD,
|
|
30
|
+
VFDState,
|
|
31
|
+
VFDStopCMD,
|
|
32
|
+
)
|
|
33
|
+
from .protocol import VFDProtocol
|
|
34
|
+
from .rs485 import VFD
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"VFD",
|
|
38
|
+
"VFDCapabilities",
|
|
39
|
+
"VFDClearErrorCMD",
|
|
40
|
+
"VFDError",
|
|
41
|
+
"VFDParameters",
|
|
42
|
+
"VFDProtocol",
|
|
43
|
+
"VFDSetFrequencyCMD",
|
|
44
|
+
"VFDStartCMD",
|
|
45
|
+
"VFDState",
|
|
46
|
+
"VFDStopCMD",
|
|
47
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VFD capability model.
|
|
3
|
+
|
|
4
|
+
A drive advertises what it can do through a single :class:`VFDCapabilities`
|
|
5
|
+
value. The fields are the operations a caller can dispatch without inspecting
|
|
6
|
+
the driver: whether the drive accepts a direction choice, a jog command, a
|
|
7
|
+
coasting stop, a fault reset, and a frequency setpoint.
|
|
8
|
+
|
|
9
|
+
The set is a frozen dataclass so a driver declares it once as a class attribute
|
|
10
|
+
and callers cannot mutate it.
|
|
11
|
+
|
|
12
|
+
Capabilities describe the *control* surface only. Whether a drive reports a
|
|
13
|
+
particular *reading* — a heatsink temperature, a DC bus voltage — is answered by
|
|
14
|
+
the corresponding :class:`scietex.hal.vfd.base.data.VFDParameters` field being
|
|
15
|
+
``None``, not by a flag here. One source of truth cannot drift from the driver.
|
|
16
|
+
|
|
17
|
+
Classes:
|
|
18
|
+
VFDCapabilities: The control surface of one drive.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class VFDCapabilities:
|
|
26
|
+
"""
|
|
27
|
+
The control surface of one drive.
|
|
28
|
+
|
|
29
|
+
Every field defaults to "unsupported", so a driver declares only what its
|
|
30
|
+
hardware exposes. Read-side operations (parameters, state, error code) are
|
|
31
|
+
mandatory for every driver and are therefore not advertised here.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
direction (bool): Whether the drive can be started in either rotation
|
|
35
|
+
direction. ``False`` means forward only.
|
|
36
|
+
jog (bool): Whether the drive accepts a jog command.
|
|
37
|
+
freewheel_stop (bool): Whether the drive can stop by coasting instead of
|
|
38
|
+
ramping down.
|
|
39
|
+
fault_reset (bool): Whether the drive accepts an explicit fault reset.
|
|
40
|
+
frequency_setpoint (bool): Whether the drive accepts a frequency
|
|
41
|
+
setpoint over the communication protocol, independent of the
|
|
42
|
+
keypad or an analog input.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
direction: bool = True
|
|
46
|
+
jog: bool = False
|
|
47
|
+
freewheel_stop: bool = False
|
|
48
|
+
fault_reset: bool = False
|
|
49
|
+
frequency_setpoint: bool = False
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""VFD data structures.
|
|
2
|
+
|
|
3
|
+
:class:`VFDParameters` names the operating frequency ``frequency_hz`` so it
|
|
4
|
+
cannot be confused with ``frequency_percent``, which is the unit some drives
|
|
5
|
+
report natively. The two frequency fields are the only ones guaranteed to be
|
|
6
|
+
populated; every other field is optional and stays ``None`` when the drive's
|
|
7
|
+
protocol does not expose it.
|
|
8
|
+
|
|
9
|
+
``None`` and ``0.0`` mean different things throughout this module: ``None`` is
|
|
10
|
+
"this drive does not report the value", ``0.0`` is "the drive reports zero".
|
|
11
|
+
A caller that plots a temperature must check for ``None`` rather than treating
|
|
12
|
+
a missing reading as a cold drive.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from enum import Enum
|
|
16
|
+
|
|
17
|
+
import msgspec
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VFDState(Enum):
|
|
21
|
+
"""Possible states of a Variable Frequency Drive (VFD)."""
|
|
22
|
+
|
|
23
|
+
UNKNOWN = 0
|
|
24
|
+
STOPPED = 1
|
|
25
|
+
RUNNING_FORWARD = 2
|
|
26
|
+
RUNNING_BACKWARD = 3 # or RUNNING_REVERSE
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
"""Return a human-readable string representation."""
|
|
30
|
+
return self.name.replace("_", " ").title()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VFDError(msgspec.Struct, frozen=True):
|
|
34
|
+
"""Model of a VFD error with code and message."""
|
|
35
|
+
|
|
36
|
+
code: int
|
|
37
|
+
message: str = "Unknown error"
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
return f"VFDError(code={self.code}, message='{self.message}')"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class VFDParameters(msgspec.Struct, frozen=True):
|
|
44
|
+
"""Model for VFD operational parameters.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
frequency_hz: Output frequency in Hz.
|
|
48
|
+
frequency_percent: Output frequency as a percentage of the configured
|
|
49
|
+
maximum, 0-100.
|
|
50
|
+
output_current: Output current in Amps.
|
|
51
|
+
output_voltage: Output voltage in Volts.
|
|
52
|
+
output_power: Output power in kW.
|
|
53
|
+
dc_bus_voltage: DC bus (link) voltage in Volts, or ``None`` when the
|
|
54
|
+
drive does not report it.
|
|
55
|
+
heatsink_temperature_c: Power-module or heatsink temperature in degrees
|
|
56
|
+
Celsius, or ``None`` when the drive does not report it. A drive that
|
|
57
|
+
signals overheat only through a fault code reports ``None`` here:
|
|
58
|
+
a fault is not a temperature.
|
|
59
|
+
motor_temperature_c: Motor winding or PTC temperature in degrees
|
|
60
|
+
Celsius, or ``None`` when the drive does not report it.
|
|
61
|
+
state: Current VFD state.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
frequency_hz: float # Current operating frequency in Hz
|
|
65
|
+
frequency_percent: float # Frequency as a percentage of max (0-100%)
|
|
66
|
+
output_current: float = 0.0 # Output current in Amps
|
|
67
|
+
output_voltage: float = 0.0 # Output voltage in Volts
|
|
68
|
+
output_power: float = 0.0 # Output power in kW
|
|
69
|
+
dc_bus_voltage: float | None = None # DC bus voltage in Volts
|
|
70
|
+
heatsink_temperature_c: float | None = None # Power-module temperature, deg C
|
|
71
|
+
motor_temperature_c: float | None = None # Motor temperature, deg C
|
|
72
|
+
state: VFDState = VFDState.STOPPED # Current VFD state
|
|
73
|
+
|
|
74
|
+
def __str__(self) -> str:
|
|
75
|
+
return (
|
|
76
|
+
f"VFDParameters(frequency={self.frequency_hz} Hz, "
|
|
77
|
+
f"frequency_percent={self.frequency_percent}%, "
|
|
78
|
+
f"current={self.output_current} A, "
|
|
79
|
+
f"voltage={self.output_voltage} V, "
|
|
80
|
+
f"power={self.output_power} kW, "
|
|
81
|
+
f"dc_bus={self.dc_bus_voltage} V, "
|
|
82
|
+
f"heatsink={self.heatsink_temperature_c} C, "
|
|
83
|
+
f"motor={self.motor_temperature_c} C, "
|
|
84
|
+
f"state={self.state})"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class VFDStartCMD(msgspec.Struct, frozen=True):
|
|
89
|
+
"""VFD start command."""
|
|
90
|
+
|
|
91
|
+
forward: bool = True
|
|
92
|
+
slow: bool = False
|
|
93
|
+
|
|
94
|
+
def __str__(self) -> str:
|
|
95
|
+
return f"VFDStartCMD(forward={self.forward}, slow={self.slow})"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class VFDStopCMD(msgspec.Struct, frozen=True):
|
|
99
|
+
"""VFD stop command."""
|
|
100
|
+
|
|
101
|
+
freewheel: bool = False
|
|
102
|
+
|
|
103
|
+
def __str__(self) -> str:
|
|
104
|
+
return f"VFDStopCMD(freewheel={self.freewheel})"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class VFDClearErrorCMD(msgspec.Struct, frozen=True):
|
|
108
|
+
"""VFD clear error command."""
|
|
109
|
+
|
|
110
|
+
communication: bool = False
|
|
111
|
+
|
|
112
|
+
def __str__(self) -> str:
|
|
113
|
+
return f"VFDClearErrorCMD(communication={self.communication})"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class VFDSetFrequencyCMD(msgspec.Struct, frozen=True):
|
|
117
|
+
"""VFD frequency setpoint command."""
|
|
118
|
+
|
|
119
|
+
frequency_hz: float = 0.0
|
|
120
|
+
|
|
121
|
+
def __str__(self) -> str:
|
|
122
|
+
return f"VFDSetFrequencyCMD(frequency_hz={self.frequency_hz})"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VFD protocol.
|
|
3
|
+
|
|
4
|
+
This module defines the transport-agnostic contract every variable frequency
|
|
5
|
+
drive driver implements, plus the capability model that tells a caller which
|
|
6
|
+
optional control operations a given drive supports. The contract is deliberately
|
|
7
|
+
narrow: a driver must be able to connect, disconnect, read the operating
|
|
8
|
+
parameters and state, read and reset the fault code, start and stop the motor,
|
|
9
|
+
and set the frequency. Everything else — a jog command, for example — is
|
|
10
|
+
advertised through :attr:`VFDProtocol.capabilities`, so a caller can reject an
|
|
11
|
+
unsupported command before dispatching it instead of catching an exception after
|
|
12
|
+
the fact.
|
|
13
|
+
|
|
14
|
+
The same contract covers both transports in this package: the direct RS485
|
|
15
|
+
clients derived from :class:`scietex.hal.vfd.base.rs485.VFD` and the Modbus/TCP
|
|
16
|
+
clients that address a drive through the Scietex Modbus gateway.
|
|
17
|
+
|
|
18
|
+
Classes:
|
|
19
|
+
VFDProtocol: The runtime-checkable protocol every drive driver satisfies.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from typing import Any, Protocol, runtime_checkable
|
|
23
|
+
|
|
24
|
+
from .capabilities import VFDCapabilities
|
|
25
|
+
from .data import VFDParameters, VFDState
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@runtime_checkable
|
|
29
|
+
class VFDProtocol(Protocol):
|
|
30
|
+
"""
|
|
31
|
+
Contract implemented by every variable frequency drive driver.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
label (str): Human-readable drive label, unique within a service.
|
|
35
|
+
capabilities (VFDCapabilities): What this drive can do.
|
|
36
|
+
display_name (str): Model name reported to the user interface.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
label: str
|
|
40
|
+
|
|
41
|
+
capabilities: VFDCapabilities = VFDCapabilities()
|
|
42
|
+
display_name: str = "VFD"
|
|
43
|
+
|
|
44
|
+
async def connect(self) -> None:
|
|
45
|
+
"""Open the transport to the drive."""
|
|
46
|
+
|
|
47
|
+
async def close(self) -> None:
|
|
48
|
+
"""Close the transport to the drive."""
|
|
49
|
+
|
|
50
|
+
async def read_data(self) -> dict[str, Any]:
|
|
51
|
+
"""
|
|
52
|
+
Read a snapshot of the drive's current values.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict[str, Any]: Field name to value mapping. ``state`` is always
|
|
56
|
+
present; the remaining keys depend on the driver.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
async def read_parameters(self) -> VFDParameters:
|
|
60
|
+
"""
|
|
61
|
+
Read the drive's operating parameters.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
VFDParameters: Frequency in both Hz and percent of the configured
|
|
65
|
+
maximum, output current, voltage, power and state, plus the
|
|
66
|
+
optional DC bus voltage and temperatures the drive reports.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
async def read_state(self) -> VFDState:
|
|
70
|
+
"""
|
|
71
|
+
Read the drive's run state.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
VFDState: The state, or ``VFDState.UNKNOWN`` when the drive reports
|
|
75
|
+
a value the protocol does not define.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
async def read_error_code(self, comm: bool = False) -> int:
|
|
79
|
+
"""
|
|
80
|
+
Read the latched fault code.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
comm: Read the communication-protocol fault code instead of the
|
|
84
|
+
drive fault code.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
int: The fault code; ``0`` means no fault.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
async def clear_error(self, comm: bool = False) -> int:
|
|
91
|
+
"""
|
|
92
|
+
Reset the latched fault and read the code back.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
comm: Reset the communication-protocol fault instead of the drive
|
|
96
|
+
fault.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
int: The fault code read after the reset; ``0`` means the fault is
|
|
100
|
+
gone.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
async def start(self, forward: bool = True, slow: bool = False) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Start the motor.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
forward: Run in the forward direction; ``False`` runs in reverse.
|
|
109
|
+
slow: Use the jog (slow) command instead of the normal run command.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
async def stop(self, freewheel: bool = False) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Stop the motor.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
freewheel: Coast to a stop instead of ramping down.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
async def set_frequency(self, frequency_hz: float) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Set the frequency setpoint.
|
|
123
|
+
|
|
124
|
+
The value is in Hz, not percent: the caller works in engineering units
|
|
125
|
+
and each driver converts with its own maximum-frequency parameter.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
frequency_hz: The requested output frequency in Hz.
|
|
129
|
+
"""
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Basic VFD device prototype class to be inherited by particular implementations."""
|
|
2
|
+
|
|
3
|
+
from logging import Logger
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from scietex.hal.serial import (
|
|
7
|
+
ModbusSerialConnectionConfig,
|
|
8
|
+
RS485Client,
|
|
9
|
+
SerialConnectionConfig,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from .capabilities import VFDCapabilities
|
|
13
|
+
from .data import (
|
|
14
|
+
VFDClearErrorCMD,
|
|
15
|
+
VFDError,
|
|
16
|
+
VFDParameters,
|
|
17
|
+
VFDSetFrequencyCMD,
|
|
18
|
+
VFDStartCMD,
|
|
19
|
+
VFDState,
|
|
20
|
+
VFDStopCMD,
|
|
21
|
+
)
|
|
22
|
+
from .protocol import VFDProtocol
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VFD(RS485Client, VFDProtocol):
|
|
26
|
+
"""VFD control base class"""
|
|
27
|
+
|
|
28
|
+
capabilities = VFDCapabilities()
|
|
29
|
+
display_name = "VFD"
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
con_params: SerialConnectionConfig | ModbusSerialConnectionConfig,
|
|
34
|
+
address: int = 1,
|
|
35
|
+
label: str = "VFD",
|
|
36
|
+
logger: Logger | None = None,
|
|
37
|
+
**kwargs,
|
|
38
|
+
):
|
|
39
|
+
super().__init__(
|
|
40
|
+
con_params=con_params,
|
|
41
|
+
address=address,
|
|
42
|
+
label=label,
|
|
43
|
+
logger=logger,
|
|
44
|
+
**kwargs,
|
|
45
|
+
)
|
|
46
|
+
self.error_codes: dict[int, VFDError] = {0: VFDError(code=0, message="No error")}
|
|
47
|
+
self.error_codes_com: dict[int, VFDError] = {0: VFDError(code=0, message="No error")}
|
|
48
|
+
|
|
49
|
+
async def connect(self) -> None:
|
|
50
|
+
"""Nothing to do: the serial client opens the port once per operation."""
|
|
51
|
+
self.logger.debug("%s: Connect (per-operation serial client).", self.label)
|
|
52
|
+
|
|
53
|
+
# Errors processing methods
|
|
54
|
+
async def read_error_code(self, comm: bool = False) -> int:
|
|
55
|
+
"""Read error code from the VFD"""
|
|
56
|
+
code = 0
|
|
57
|
+
if comm:
|
|
58
|
+
self.logger.debug("%s: Communication error code %d.", self.label, code)
|
|
59
|
+
else:
|
|
60
|
+
self.logger.debug("%s: Error code %d.", self.label, code)
|
|
61
|
+
return code
|
|
62
|
+
|
|
63
|
+
def parse_error_code(self, code: int, comm: bool = False) -> VFDError:
|
|
64
|
+
"""Parse error code from the VFD"""
|
|
65
|
+
if comm:
|
|
66
|
+
if code in self.error_codes_com:
|
|
67
|
+
return self.error_codes_com[code]
|
|
68
|
+
else:
|
|
69
|
+
if code in self.error_codes:
|
|
70
|
+
return self.error_codes[code]
|
|
71
|
+
return VFDError(code=code, message="Unknown error")
|
|
72
|
+
|
|
73
|
+
async def clear_error(self, comm: bool = False) -> int:
|
|
74
|
+
"""Clear error from the VFD"""
|
|
75
|
+
if comm:
|
|
76
|
+
self.logger.debug("%s: Clear communication error.", self.label)
|
|
77
|
+
else:
|
|
78
|
+
self.logger.debug("%s: Clear error.", self.label)
|
|
79
|
+
return await self.read_error_code(comm)
|
|
80
|
+
|
|
81
|
+
# Parameters monitoring methods
|
|
82
|
+
async def read_parameters(self) -> VFDParameters:
|
|
83
|
+
"""Read the VFD operating parameters"""
|
|
84
|
+
parameters = VFDParameters(frequency_hz=0.0, frequency_percent=0.0)
|
|
85
|
+
self.logger.debug("%s: %s", self.label, parameters)
|
|
86
|
+
return parameters
|
|
87
|
+
|
|
88
|
+
async def read_state(self) -> VFDState:
|
|
89
|
+
"""Read the VFD run state"""
|
|
90
|
+
state = VFDState.UNKNOWN
|
|
91
|
+
self.logger.debug("%s: Current state: %s", self.label, state)
|
|
92
|
+
return state
|
|
93
|
+
|
|
94
|
+
# VFD Control methods
|
|
95
|
+
async def start(self, forward: bool = True, slow: bool = False) -> None:
|
|
96
|
+
"""Start the VFD"""
|
|
97
|
+
self.logger.debug("%s: Start (forward: %s, slow: %s)", self.label, forward, slow)
|
|
98
|
+
|
|
99
|
+
async def stop(self, freewheel: bool = False) -> None:
|
|
100
|
+
"""Stop the VFD"""
|
|
101
|
+
self.logger.debug("%s: Stop (freewheel: %s)", self.label, freewheel)
|
|
102
|
+
|
|
103
|
+
async def set_frequency(self, frequency_hz: float) -> None:
|
|
104
|
+
"""Set the VFD frequency setpoint"""
|
|
105
|
+
self.logger.debug("%s: Set frequency %s Hz.", self.label, frequency_hz)
|
|
106
|
+
|
|
107
|
+
async def read_data(self) -> dict[str, Any]:
|
|
108
|
+
"""
|
|
109
|
+
Read the VFD operating parameters as a plain dictionary.
|
|
110
|
+
|
|
111
|
+
The return type is widened to ``Any`` because `VFDParameters` carries
|
|
112
|
+
optional fields (temperatures, DC bus voltage) that are ``None`` when the
|
|
113
|
+
drive does not report them, which the inherited `RS485Client` signature
|
|
114
|
+
does not allow.
|
|
115
|
+
"""
|
|
116
|
+
parameters = await self.read_parameters()
|
|
117
|
+
return {f: getattr(parameters, f) for f in parameters.__struct_fields__}
|
|
118
|
+
|
|
119
|
+
async def process_message(self, message: dict[str, Any]) -> None:
|
|
120
|
+
await super().process_message(message)
|
|
121
|
+
if "cmd" in message:
|
|
122
|
+
if message["cmd"] == "start":
|
|
123
|
+
start_params = VFDStartCMD()
|
|
124
|
+
if "data" in message:
|
|
125
|
+
try:
|
|
126
|
+
start_params = VFDStartCMD(**message["data"])
|
|
127
|
+
except (TypeError, ValueError):
|
|
128
|
+
pass
|
|
129
|
+
await self.start(forward=start_params.forward, slow=start_params.slow)
|
|
130
|
+
elif message["cmd"] == "stop":
|
|
131
|
+
stop_params = VFDStopCMD()
|
|
132
|
+
if "data" in message:
|
|
133
|
+
try:
|
|
134
|
+
stop_params = VFDStopCMD(**message["data"])
|
|
135
|
+
except (TypeError, ValueError):
|
|
136
|
+
pass
|
|
137
|
+
await self.stop(freewheel=stop_params.freewheel)
|
|
138
|
+
elif message["cmd"] == "clear_error":
|
|
139
|
+
clear_params = VFDClearErrorCMD()
|
|
140
|
+
if "data" in message:
|
|
141
|
+
try:
|
|
142
|
+
clear_params = VFDClearErrorCMD(**message["data"])
|
|
143
|
+
except (TypeError, ValueError):
|
|
144
|
+
pass
|
|
145
|
+
await self.clear_error(comm=clear_params.communication)
|
|
146
|
+
elif message["cmd"] == "set_frequency":
|
|
147
|
+
frequency_params = VFDSetFrequencyCMD()
|
|
148
|
+
if "data" in message:
|
|
149
|
+
try:
|
|
150
|
+
frequency_params = VFDSetFrequencyCMD(**message["data"])
|
|
151
|
+
except (TypeError, ValueError):
|
|
152
|
+
pass
|
|
153
|
+
await self.set_frequency(frequency_hz=frequency_params.frequency_hz)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Intek SPE-B drivers.
|
|
3
|
+
|
|
4
|
+
Two transports are supported for the same register map:
|
|
5
|
+
|
|
6
|
+
- :class:`SPEvB` talks Modbus RTU to the drive over a direct serial connection.
|
|
7
|
+
- :class:`IntekSPEBModbusVFD` talks Modbus/TCP to the Scietex Modbus gateway,
|
|
8
|
+
which owns the serial bus and routes requests to the drive by unit id.
|
|
9
|
+
|
|
10
|
+
Modules:
|
|
11
|
+
registers: Register addresses, control and status codes, scaling helpers.
|
|
12
|
+
errors: Drive and communication fault code tables.
|
|
13
|
+
rs485: The direct-serial client.
|
|
14
|
+
client: The Modbus/TCP gateway client.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .client import IntekSPEBModbusVFD
|
|
18
|
+
from .errors import SPE_B_ERROR_CODES, SPE_B_ERROR_CODES_COM
|
|
19
|
+
from .rs485 import SPEvB
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"IntekSPEBModbusVFD",
|
|
23
|
+
"SPE_B_ERROR_CODES",
|
|
24
|
+
"SPE_B_ERROR_CODES_COM",
|
|
25
|
+
"SPEvB",
|
|
26
|
+
]
|