weider-modbus 0.1.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.
@@ -0,0 +1,36 @@
1
+ """Async Python library for the Modbus TCP interface of Weider WT16 heat pumps."""
2
+
3
+ from .components import DigitalInputs, Measurements, Settings
4
+ from .const import (
5
+ HOT_WATER_TARGET_MAX,
6
+ HOT_WATER_TARGET_MIN,
7
+ MANUFACTURER,
8
+ MAX_PLAUSIBLE_TEMPERATURE,
9
+ MIN_PLAUSIBLE_TEMPERATURE,
10
+ MODEL,
11
+ ROOM_TARGET_MAX,
12
+ ROOM_TARGET_MIN,
13
+ TARGET_STEP,
14
+ )
15
+ from .device import WeiderWT16
16
+ from .helpers import SgReadyMode, clean_error_text, plausible_temperature, sg_ready_mode
17
+
18
+ __all__ = [
19
+ "HOT_WATER_TARGET_MAX",
20
+ "HOT_WATER_TARGET_MIN",
21
+ "MANUFACTURER",
22
+ "MAX_PLAUSIBLE_TEMPERATURE",
23
+ "MIN_PLAUSIBLE_TEMPERATURE",
24
+ "MODEL",
25
+ "ROOM_TARGET_MAX",
26
+ "ROOM_TARGET_MIN",
27
+ "TARGET_STEP",
28
+ "DigitalInputs",
29
+ "Measurements",
30
+ "Settings",
31
+ "SgReadyMode",
32
+ "WeiderWT16",
33
+ "clean_error_text",
34
+ "plausible_temperature",
35
+ "sg_ready_mode",
36
+ ]
@@ -0,0 +1,246 @@
1
+ """Typed Modbus components of the Weider WT16."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Awaitable, Callable
7
+ from typing import ClassVar, Final
8
+
9
+ from modbus_connection.exceptions import IllegalDataAddressError
10
+ from modbus_connection.model import (
11
+ Component,
12
+ NumberField,
13
+ discrete_input,
14
+ gauge,
15
+ integer,
16
+ string,
17
+ uint32,
18
+ )
19
+
20
+ from .const import (
21
+ HOT_WATER_TARGET_MAX,
22
+ HOT_WATER_TARGET_MIN,
23
+ ROOM_TARGET_MAX,
24
+ ROOM_TARGET_MIN,
25
+ )
26
+
27
+ _LOGGER = logging.getLogger(__name__)
28
+
29
+
30
+ def _temperature(address: int) -> NumberField[float]:
31
+ """Return the signed, 0.1-scaled WT16 temperature format.
32
+
33
+ Outdoor, source and refrigerant temperatures go below 0 °C, which the
34
+ device sends as two's complement (65516 is -2.0 °C).
35
+ """
36
+ return gauge(address, 0.1, unit="°C")
37
+
38
+
39
+ def _range_validator(minimum: float, maximum: float) -> Callable[[float], float]:
40
+ """Return a write validator that rejects values outside [minimum, maximum]."""
41
+
42
+ def validate(value: float) -> float:
43
+ if not minimum <= value <= maximum:
44
+ raise ValueError(
45
+ f"{value} is outside the allowed range {minimum}..{maximum}"
46
+ )
47
+ return value
48
+
49
+ return validate
50
+
51
+
52
+ def _target_temperature(
53
+ address: int, minimum: float, maximum: float
54
+ ) -> NumberField[float]:
55
+ """Return a writable, unsigned, 0.1-scaled target temperature.
56
+
57
+ The device only accepts function code 16 for these registers.
58
+ """
59
+ return gauge(
60
+ address,
61
+ 0.1,
62
+ signed=False,
63
+ unit="°C",
64
+ writable=_range_validator(minimum, maximum),
65
+ force_fc16=True,
66
+ )
67
+
68
+
69
+ _READERS: Final = {
70
+ "input": "read_input_registers",
71
+ "holding": "read_holding_registers",
72
+ "discrete": "read_discrete_inputs",
73
+ "coil": "read_coils",
74
+ }
75
+
76
+
77
+ async def _rejected_fields(
78
+ component: Component, space: str, names: set[str]
79
+ ) -> set[str]:
80
+ """Read each field on its own and return those the device rejects."""
81
+ read = getattr(component.modbus_unit, _READERS[space])
82
+ rejected = set()
83
+ for name in sorted(names):
84
+ field = component.resolved_fields[name]
85
+ try:
86
+ await read(field.address, field.count)
87
+ except IllegalDataAddressError:
88
+ rejected.add(name)
89
+ return rejected
90
+
91
+
92
+ async def _update_skipping_unsupported(
93
+ component: Component,
94
+ update: Callable[..., Awaitable[None]],
95
+ space: str,
96
+ optional: frozenset[str] | None,
97
+ *,
98
+ notify: bool,
99
+ ) -> None:
100
+ """Refresh a component, dropping optional fields the device rejects.
101
+
102
+ If the device rejects a block of several fields, each field is read on its
103
+ own to find the unsupported ones. Unsupported fields are skipped only if
104
+ they are all optional (``optional=None`` treats all fields as optional).
105
+ Anything else is a real error and propagates, so it is reported instead of
106
+ hidden.
107
+ """
108
+ while True:
109
+ try:
110
+ await update(notify=notify)
111
+ return
112
+ except IllegalDataAddressError as err:
113
+ block = err.block
114
+ if block is None or block.space != space:
115
+ raise
116
+ unsupported = {
117
+ name
118
+ for name, field in component.resolved_fields.items()
119
+ if field.address < block.address + block.count
120
+ and block.address < field.address + field.count
121
+ }
122
+ if len(unsupported) > 1:
123
+ unsupported = await _rejected_fields(component, space, unsupported)
124
+ if not unsupported or (
125
+ optional is not None and not unsupported <= optional
126
+ ):
127
+ raise
128
+ addresses = sorted(
129
+ component.resolved_fields[name].address for name in unsupported
130
+ )
131
+ component.restrict_fields(set(component.resolved_fields) - unsupported)
132
+ _LOGGER.info(
133
+ "Skipping unsupported %s %s at %s %s",
134
+ "field" if len(unsupported) == 1 else "fields",
135
+ ", ".join(sorted(unsupported)),
136
+ "address" if len(addresses) == 1 else "addresses",
137
+ ", ".join(map(str, addresses)),
138
+ )
139
+
140
+
141
+ class Measurements(Component):
142
+ """Read-only WT16 input registers (function code 04)."""
143
+
144
+ register_space = "input"
145
+ # max_gap = 1 merges directly adjacent registers only, never across a gap.
146
+ max_gap = 1
147
+ max_span = 50
148
+
149
+ hot_water_temperature = _temperature(13)
150
+ flow_target_temperature = _temperature(14)
151
+ outdoor_temperature = _temperature(15)
152
+ buffer_temperature = _temperature(16)
153
+ mixer_temperature = _temperature(17)
154
+ reserve_temperature_1 = _temperature(18)
155
+ reserve_temperature_2 = _temperature(19)
156
+ reserve_temperature_3 = _temperature(20)
157
+ defrost_temperature = _temperature(21)
158
+ flow_temperature = _temperature(25)
159
+ return_temperature = _temperature(26)
160
+ source_inlet_temperature = _temperature(27)
161
+ source_outlet_temperature = _temperature(28)
162
+ superheat = _temperature(29)
163
+ evaporation_temperature = _temperature(31)
164
+ condensation_temperature = _temperature(33)
165
+ evaporator_temperature = _temperature(35)
166
+ suction_gas_temperature = _temperature(36)
167
+ hot_gas_temperature = _temperature(37)
168
+ volume_flow = gauge(44, 0.001, signed=False, unit="m³/h")
169
+ room_temperature = _temperature(724)
170
+ mixer_position = integer(736, signed=False)
171
+ last_pump_runtime = uint32(60164, unit="min")
172
+ last_hot_water_runtime = uint32(60168, unit="min")
173
+ active_error = string(63000, 16)
174
+
175
+ # Registers that some firmware versions do not provide.
176
+ OPTIONAL_FIELDS: ClassVar[frozenset[str]] = frozenset(
177
+ {
178
+ "room_temperature",
179
+ "mixer_position",
180
+ "last_pump_runtime",
181
+ "last_hot_water_runtime",
182
+ "active_error",
183
+ }
184
+ )
185
+
186
+ async def async_update(self, *, notify: bool = True) -> None:
187
+ """Refresh measurements, skipping optional registers this device rejects."""
188
+ await _update_skipping_unsupported(
189
+ self, super().async_update, "input", self.OPTIONAL_FIELDS, notify=notify
190
+ )
191
+
192
+
193
+ class Settings(Component):
194
+ """Writable WT16 holding registers (function codes 03/16)."""
195
+
196
+ max_gap = 0
197
+ max_span = 50
198
+
199
+ hot_water_target_temperature = _target_temperature(
200
+ 1, HOT_WATER_TARGET_MIN, HOT_WATER_TARGET_MAX
201
+ )
202
+ room_target_temperature = _target_temperature(723, ROOM_TARGET_MIN, ROOM_TARGET_MAX)
203
+
204
+ # Installations without a room sensor may not provide the room target.
205
+ OPTIONAL_FIELDS: ClassVar[frozenset[str]] = frozenset({"room_target_temperature"})
206
+
207
+ async def async_update(self, *, notify: bool = True) -> None:
208
+ """Refresh settings, skipping optional registers this device rejects."""
209
+ await _update_skipping_unsupported(
210
+ self, super().async_update, "holding", self.OPTIONAL_FIELDS, notify=notify
211
+ )
212
+
213
+
214
+ class DigitalInputs(Component):
215
+ """WT16 discrete inputs (function code 02)."""
216
+
217
+ # max_gap = 1 merges directly adjacent inputs only, never across a gap.
218
+ max_gap = 1
219
+ max_span = 50
220
+
221
+ flow_switch_1 = discrete_input(45)
222
+ flow_switch_2 = discrete_input(69)
223
+ compressor = discrete_input(679)
224
+ heating_pump = discrete_input(680)
225
+ source_pump = discrete_input(681)
226
+ mixer_pump = discrete_input(682)
227
+ hot_water_pump = discrete_input(685)
228
+ remote_fault = discrete_input(686)
229
+ four_way_valve = discrete_input(690)
230
+ reserve_1 = discrete_input(691)
231
+ reserve_2 = discrete_input(692)
232
+ reserve_3 = discrete_input(693)
233
+ cooling_output = discrete_input(694)
234
+ reserve_5 = discrete_input(695)
235
+ hot_water_block = discrete_input(703)
236
+ heating_block = discrete_input(704)
237
+ utility_block = discrete_input(705)
238
+ sg_ready_1 = discrete_input(706)
239
+ sg_ready_2 = discrete_input(707)
240
+ reserve = discrete_input(714)
241
+
242
+ async def async_update(self, *, notify: bool = True) -> None:
243
+ """Refresh supported inputs, excluding addresses rejected by this device."""
244
+ await _update_skipping_unsupported(
245
+ self, super().async_update, "discrete", None, notify=notify
246
+ )
weider_modbus/const.py ADDED
@@ -0,0 +1,22 @@
1
+ """Device facts for the Weider WT16."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ MANUFACTURER: Final = "Weider"
8
+ MODEL: Final = "WT16"
9
+
10
+ # Allowed write ranges for the target temperatures, in °C (inclusive).
11
+ HOT_WATER_TARGET_MIN: Final = 15.0
12
+ HOT_WATER_TARGET_MAX: Final = 55.0
13
+ ROOM_TARGET_MIN: Final = 15.0
14
+ ROOM_TARGET_MAX: Final = 35.0
15
+ TARGET_STEP: Final = 0.5
16
+
17
+ # The device reports a placeholder value, such as 3276.7, for a temperature
18
+ # sensor that is not connected. Readings outside this range are treated as
19
+ # unknown. The range covers outdoor temperatures down to -45 °C and the hot
20
+ # gas temperature of the refrigerant circuit.
21
+ MIN_PLAUSIBLE_TEMPERATURE: Final = -45.0
22
+ MAX_PLAUSIBLE_TEMPERATURE: Final = 150.0
@@ -0,0 +1,75 @@
1
+ """The top-level WT16 device object."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import TYPE_CHECKING, Final
7
+
8
+ from modbus_connection.model import Device, Raw
9
+
10
+ from .components import DigitalInputs, Measurements, Settings
11
+ from .const import MANUFACTURER, MODEL
12
+ from .helpers import SgReadyMode, sg_ready_mode
13
+
14
+ if TYPE_CHECKING:
15
+ from modbus_connection import ModbusUnit
16
+
17
+ # Attribute names of the components, in polling order.
18
+ COMPONENTS: Final = ("measurements", "settings", "digital_inputs")
19
+
20
+
21
+ class WeiderWT16(Device):
22
+ """One Weider WT16 heat pump, reached through a ``ModbusUnit``.
23
+
24
+ The consumer owns the connection and passes a unit. This library only
25
+ reads and writes registers, so it works over any modbus-connection backend.
26
+ """
27
+
28
+ manufacturer: Final = MANUFACTURER
29
+ model: Final = MODEL
30
+
31
+ def __init__(self, unit: ModbusUnit) -> None:
32
+ """Create the device and its components. No I/O happens here."""
33
+ super().__init__(unit)
34
+ self.measurements = Measurements(unit)
35
+ self.settings = Settings(unit)
36
+ self.digital_inputs = DigitalInputs(unit)
37
+
38
+ async def async_update(self) -> None:
39
+ """Refresh every component.
40
+
41
+ Optional registers the device rejects are dropped on the first poll
42
+ that hits them. Any other ``ModbusError`` propagates, so the caller can
43
+ retry or mark the device unavailable. Listeners fire once every read is
44
+ in, so they see a consistent poll.
45
+ """
46
+ await self.async_ensure_setup()
47
+ for name in COMPONENTS:
48
+ await getattr(self, name).async_update(notify=False)
49
+ for name in COMPONENTS:
50
+ getattr(self, name).notify()
51
+
52
+ async def async_set_hot_water_target(self, temperature: float) -> None:
53
+ """Write the hot water target temperature in °C.
54
+
55
+ Raises ``ValueError`` if the value is outside the allowed range.
56
+ """
57
+ await self.settings.write("hot_water_target_temperature", temperature)
58
+
59
+ async def async_set_room_target(self, temperature: float) -> None:
60
+ """Write the room target temperature in °C.
61
+
62
+ Raises ``ValueError`` if the value is outside the allowed range and
63
+ ``AttributeError`` if this device does not provide the register.
64
+ """
65
+ await self.settings.write("room_target_temperature", temperature)
66
+
67
+ @property
68
+ def sg_ready_mode(self) -> SgReadyMode | None:
69
+ """The SG Ready operating state, derived from the digital inputs."""
70
+ inputs = self.digital_inputs
71
+ return sg_ready_mode(inputs.utility_block, inputs.sg_ready_1, inputs.sg_ready_2)
72
+
73
+ async def async_read_raw(self, names: Iterable[str] = COMPONENTS) -> Raw:
74
+ """Return every register this device reads, undecoded, for diagnostics."""
75
+ return await super().async_read_raw(names)
@@ -0,0 +1,60 @@
1
+ """Interpretation of WT16 values that the raw registers do not carry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import StrEnum
6
+
7
+ from .const import MAX_PLAUSIBLE_TEMPERATURE, MIN_PLAUSIBLE_TEMPERATURE
8
+
9
+
10
+ def plausible_temperature(value: float | None) -> float | None:
11
+ """Return the temperature, or ``None`` if it is a placeholder value.
12
+
13
+ The device reports a placeholder (for example 3276.7) for a temperature
14
+ sensor that is not connected.
15
+ """
16
+ if value is None or MIN_PLAUSIBLE_TEMPERATURE <= value <= MAX_PLAUSIBLE_TEMPERATURE:
17
+ return value
18
+ return None
19
+
20
+
21
+ def clean_error_text(value: str | None) -> str | None:
22
+ """Return the device's error text without padding, or ``None`` if no error.
23
+
24
+ The device pads the 16-register text with null characters or spaces, so
25
+ "no error" arrives as padding only.
26
+ """
27
+ if value is None:
28
+ return None
29
+ text = " ".join(value.replace("\x00", " ").split())
30
+ return text or None
31
+
32
+
33
+ class SgReadyMode(StrEnum):
34
+ """SG Ready operating states (Betriebszustände)."""
35
+
36
+ UTILITY_BLOCK = "bz1_utility_block"
37
+ NORMAL = "bz2_normal"
38
+ INCREASED = "bz3_increased"
39
+ START = "bz4_start"
40
+
41
+
42
+ def sg_ready_mode(
43
+ utility_block: bool | None, sg_ready_1: bool | None, sg_ready_2: bool | None
44
+ ) -> SgReadyMode | None:
45
+ """Return the SG Ready operating state for the given input states.
46
+
47
+ The utility company block always means BZ1. Otherwise SG Ready 1 and 2
48
+ select the state: only SG Ready 2 on is BZ3, only SG Ready 1 on is BZ4, and
49
+ anything else is BZ2. A missing utility block reading counts as no block;
50
+ missing SG Ready readings make the state unknown.
51
+ """
52
+ if utility_block:
53
+ return SgReadyMode.UTILITY_BLOCK
54
+ if sg_ready_1 is None or sg_ready_2 is None:
55
+ return None
56
+ if sg_ready_2 and not sg_ready_1:
57
+ return SgReadyMode.INCREASED
58
+ if sg_ready_1 and not sg_ready_2:
59
+ return SgReadyMode.START
60
+ return SgReadyMode.NORMAL
weider_modbus/py.typed ADDED
File without changes
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.5
2
+ Name: weider-modbus
3
+ Version: 0.1.0
4
+ Summary: Async Python library for the Modbus TCP interface of Weider heat pumps, built on modbus-connection
5
+ Project-URL: Homepage, https://github.com/schadom/weider-modbus
6
+ Project-URL: Source, https://github.com/schadom/weider-modbus
7
+ Project-URL: Issues, https://github.com/schadom/weider-modbus/issues
8
+ Author: schadom
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: heat pump,home-assistant,modbus,weider,wt16
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Home Automation
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.13
22
+ Requires-Dist: modbus-connection>=4.12.1
23
+ Description-Content-Type: text/markdown
24
+
25
+ # weider-modbus
26
+
27
+ Async Python library for the Modbus TCP interface of Weider WT16 heat pumps,
28
+ built on [modbus-connection](https://github.com/home-assistant-libs/modbus-connection).
29
+
30
+ > [!IMPORTANT]
31
+ > This library is not affiliated with Weider Wärmepumpen GmbH and is provided
32
+ > as-is and without warranty.
33
+
34
+ The library only reads and writes registers. It takes a `ModbusUnit` and never
35
+ opens a connection itself, so it works over any modbus-connection backend
36
+ (tmodbus, pymodbus, or the in-memory mock). It is used by the
37
+ [Weider WT16 Home Assistant integration](https://github.com/schadom/hacs_weider).
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install weider-modbus
43
+ ```
44
+
45
+ The library depends only on the backend-neutral `modbus-connection`
46
+ interface. To open a connection yourself, also install a backend:
47
+
48
+ ```bash
49
+ pip install "modbus-connection[tmodbus]"
50
+ ```
51
+
52
+ ## Usage
53
+
54
+ ```python
55
+ import asyncio
56
+
57
+ from modbus_connection import ModbusTcpParams
58
+ from modbus_connection.tmodbus import ModbusConnection
59
+
60
+ from weider_modbus import WeiderWT16, plausible_temperature
61
+
62
+
63
+ async def main() -> None:
64
+ connection = ModbusConnection(ModbusTcpParams(host="192.168.1.50", port=502))
65
+ try:
66
+ device = WeiderWT16(connection.for_unit(1))
67
+ await device.async_update()
68
+
69
+ print("Hot water:", device.measurements.hot_water_temperature)
70
+ outdoor = plausible_temperature(device.measurements.outdoor_temperature)
71
+ print("Outdoor:", outdoor)
72
+ print("Compressor running:", device.digital_inputs.compressor)
73
+ print("SG Ready mode:", device.sg_ready_mode)
74
+
75
+ await device.async_set_hot_water_target(47.5)
76
+ finally:
77
+ await connection.close()
78
+
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ### Components
84
+
85
+ | Attribute | Modbus space | Content |
86
+ | --- | --- | --- |
87
+ | `device.measurements` | Input registers (FC04) | Temperatures, volume flow, runtimes, mixer position, active error text |
88
+ | `device.settings` | Holding registers (FC03/FC16) | Hot water and room target temperatures |
89
+ | `device.digital_inputs` | Discrete inputs (FC02) | Compressor, pumps, valves, blocks, SG Ready inputs |
90
+
91
+ A value reads as `None` until the first successful poll, or if the device
92
+ does not provide the register.
93
+
94
+ ### Optional registers
95
+
96
+ Some firmware versions do not provide every register. The room temperature,
97
+ room target, mixer position, runtimes, active error text and any digital input
98
+ are optional. If the device rejects one of them with an illegal data address,
99
+ the library drops that register on the first poll and stops reading it. Any
100
+ other register error still raises, so real problems are not hidden.
101
+
102
+ ### Errors
103
+
104
+ `async_update()` raises a `modbus_connection.ModbusError` subclass (or
105
+ `TimeoutError` / `OSError` from the transport) when a poll fails. The shared
106
+ connection reopens on the next request, so the caller can simply retry.
107
+
108
+ The target setters raise `ValueError` for a value outside the allowed range
109
+ (hot water 15–55 °C, room 15–35 °C), before anything is written.
110
+
111
+ ### Helpers
112
+
113
+ - `plausible_temperature(value)` returns `None` for the placeholder values the
114
+ device reports for an unconnected sensor (outside −45 to 150 °C).
115
+ - `clean_error_text(value)` strips the padding from the active error text and
116
+ returns `None` when there is no error.
117
+ - `sg_ready_mode(...)` and `device.sg_ready_mode` derive the SG Ready operating
118
+ state (BZ1–BZ4) from the digital inputs.
119
+
120
+ ## Hardware verification
121
+
122
+ The register addresses, the signed 0.1 °C temperature format and the
123
+ multi-register word order come from the existing integration. Confirm them
124
+ against your WT16 documentation, especially for negative temperatures. The
125
+ test simulator checks the software against this map, not the map against a
126
+ real heat pump.
127
+
128
+ ## Development
129
+
130
+ Requires Python 3.13 or newer and [uv](https://docs.astral.sh/uv/).
131
+
132
+ ```bash
133
+ uv sync
134
+ uv run pytest
135
+ uv run ruff check .
136
+ uv run ruff format --check .
137
+ uv run mypy
138
+ ```
139
+
140
+ To run the synthetic WT16 on port 5020 for manual testing:
141
+
142
+ ```bash
143
+ uv run python -m tests.simulator
144
+ ```
145
+
146
+ ## Releasing
147
+
148
+ Releases are published to PyPI from GitHub Actions with
149
+ [trusted publishing](https://docs.pypi.org/trusted-publishers/).
150
+
151
+ 1. Set the new `version` in `pyproject.toml` and merge it to `main`.
152
+ 2. Create a GitHub release with a tag equal to that version (for example
153
+ `0.1.0`).
154
+ 3. The `Publish` workflow builds the package and uploads it to PyPI.
155
+
156
+ A version number can be published only once.
157
+
158
+ ## License
159
+
160
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,10 @@
1
+ weider_modbus/__init__.py,sha256=h4jxlwvvQYMyHVxX2_LMMUNkqutusDDbmZg7vXZscS8,894
2
+ weider_modbus/components.py,sha256=Md_uedNQval8jB8Tb9fo2VPEaU5WbZy0nq49UPoLnMs,8238
3
+ weider_modbus/const.py,sha256=VOdpk4EZbBSDMgIU3h_yNNftL0dpJzxt6jyPPKGS5iE,741
4
+ weider_modbus/device.py,sha256=Rbijwj8ddEtK4hku99iQyRY7pjB7NgTFCO2iuz8UYJQ,2879
5
+ weider_modbus/helpers.py,sha256=evUQ4dGz7hpKIfSXJTAuOynYQAHtm2F0ik1bF54xdsE,1988
6
+ weider_modbus/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ weider_modbus-0.1.0.dist-info/METADATA,sha256=wA4FrdIivRzn5EGPdPy3umB9chHQ0Z3P03ZjO4Dc9yY,5433
8
+ weider_modbus-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
9
+ weider_modbus-0.1.0.dist-info/licenses/LICENSE,sha256=jnP_BOoMzchc9jaCzOEmdce4S2K1mouGtFl5Md0doCc,1064
10
+ weider_modbus-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 schadom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.