modbus-event-connect 0.1.0__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.
- modbus_event_connect-0.1.0/LICENSE +21 -0
- modbus_event_connect-0.1.0/PKG-INFO +87 -0
- modbus_event_connect-0.1.0/README.md +69 -0
- modbus_event_connect-0.1.0/pyproject.toml +9 -0
- modbus_event_connect-0.1.0/setup.cfg +33 -0
- modbus_event_connect-0.1.0/setup.py +6 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect/__init__.py +42 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect/modbus_deviceadapter.py +98 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect/modbus_event_connect.py +87 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect/modbus_models.py +434 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect.egg-info/PKG-INFO +87 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect.egg-info/SOURCES.txt +18 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect.egg-info/dependency_links.txt +1 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect.egg-info/not-zip-safe +1 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect.egg-info/requires.txt +1 -0
- modbus_event_connect-0.1.0/src/modbus_event_connect.egg-info/top_level.txt +1 -0
- modbus_event_connect-0.1.0/tests/test_micro_nabto.py +76 -0
- modbus_event_connect-0.1.0/tests/test_modbus_tcp.py +76 -0
- modbus_event_connect-0.1.0/tests/test_test.py +7 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 HairingX
|
|
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.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: modbus_event_connect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A library to easily interface with modbus devices, be it over TCP, unabto etc.
|
|
5
|
+
Home-page: https://github.com/HairingX/modbus_event_connect
|
|
6
|
+
Author: HairingX
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: modbus,easy,remote,connect,library
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pyModbusTCP==0.3.0
|
|
18
|
+
|
|
19
|
+
# Modbus Event Connect
|
|
20
|
+
|
|
21
|
+
Welcome to **Modbus Event Connect** – a user-friendly Python library designed to simplify the configuration of Modbus devices. Whether you're connecting via TCP or Unabto, Modbus Event Connect has got you covered.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Easy Configuration**: Intuitive setup for Modbus devices.
|
|
26
|
+
- **Flexible Connectivity**: Supports connection through TCP or Unabto.
|
|
27
|
+
- **User-Friendly**: Handles the interaction; you just define the Modbus register items.
|
|
28
|
+
|
|
29
|
+
## Getting Started
|
|
30
|
+
|
|
31
|
+
1. **Installation**: Install the library using pip.
|
|
32
|
+
```bash
|
|
33
|
+
pip install modbus-event-connect
|
|
34
|
+
```
|
|
35
|
+
2. **Basic Usage**: Define your DataPointKeys to a Modbus device with minimal code.
|
|
36
|
+
```python
|
|
37
|
+
from collections.abc import Callable
|
|
38
|
+
from src.modbus_event_connect import *
|
|
39
|
+
|
|
40
|
+
class MyDatapointKey(ModbusDatapointKey):
|
|
41
|
+
MAJOR_VERSION = "major_version"
|
|
42
|
+
|
|
43
|
+
class MySetpointKey(ModbusSetpointKey):
|
|
44
|
+
MY_SETPOINT = "my_setpoint"
|
|
45
|
+
|
|
46
|
+
class MyModbusDevice(ModbusDeviceBase):
|
|
47
|
+
def __init__(self, device_info: ModbusDeviceInfo):
|
|
48
|
+
super().__init__(device_info)
|
|
49
|
+
|
|
50
|
+
self._attr_manufacturer="<manufacturer>"
|
|
51
|
+
self._attr_model_name="<model_name>"
|
|
52
|
+
self._attr_datapoints = [
|
|
53
|
+
ModbusDatapoint(key=MyDatapointKey.MAJOR_VERSION, read_address=1, divider=1, signed=True),
|
|
54
|
+
]
|
|
55
|
+
self._attr_setpoints = [
|
|
56
|
+
ModbusSetpoint(key=MySetpointKey.MY_SETPOINT, read_address=1, write_address=1 ,divider=1, min=1, max=10, signed=True),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
class MyModbusDeviceAdapter(ModbusDeviceAdapter):
|
|
60
|
+
|
|
61
|
+
def _translate_to_model(self, device_info: ModbusDeviceInfo) -> Callable[[ModbusDeviceInfo], ModbusDevice]|None:
|
|
62
|
+
return MyModbusDevice
|
|
63
|
+
|
|
64
|
+
class MyModbusTCPEventConnect(ModbusTCPEventConnect):
|
|
65
|
+
_attr_adapter = MyModbusDeviceAdapter()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
### Client Capabilities
|
|
70
|
+
|
|
71
|
+
The Modbus EventConnect offers a range of methods to facilitate seamless interaction with Modbus devices. Key features include:
|
|
72
|
+
|
|
73
|
+
- **Subscribe**: Easily subscribe to data points and receive updates.
|
|
74
|
+
- **Unsubscribe**: Manage your subscriptions effortlessly.
|
|
75
|
+
- **Comprehensive Methods**: A variety of methods to handle different Modbus operations.
|
|
76
|
+
|
|
77
|
+
Let the EventConnect handle the communication, allowing you to focus on responding to changes and managing your Modbus devices efficiently.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Disclaimer
|
|
81
|
+
|
|
82
|
+
Modbus Event Connect is provided "as is", without warranty of any kind. The authors and contributors are not responsible for any damage or data loss that may occur from using this library. Users are solely responsible for ensuring the proper and safe operation of their Modbus devices.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Modbus Event Connect
|
|
2
|
+
|
|
3
|
+
Welcome to **Modbus Event Connect** – a user-friendly Python library designed to simplify the configuration of Modbus devices. Whether you're connecting via TCP or Unabto, Modbus Event Connect has got you covered.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Easy Configuration**: Intuitive setup for Modbus devices.
|
|
8
|
+
- **Flexible Connectivity**: Supports connection through TCP or Unabto.
|
|
9
|
+
- **User-Friendly**: Handles the interaction; you just define the Modbus register items.
|
|
10
|
+
|
|
11
|
+
## Getting Started
|
|
12
|
+
|
|
13
|
+
1. **Installation**: Install the library using pip.
|
|
14
|
+
```bash
|
|
15
|
+
pip install modbus-event-connect
|
|
16
|
+
```
|
|
17
|
+
2. **Basic Usage**: Define your DataPointKeys to a Modbus device with minimal code.
|
|
18
|
+
```python
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from src.modbus_event_connect import *
|
|
21
|
+
|
|
22
|
+
class MyDatapointKey(ModbusDatapointKey):
|
|
23
|
+
MAJOR_VERSION = "major_version"
|
|
24
|
+
|
|
25
|
+
class MySetpointKey(ModbusSetpointKey):
|
|
26
|
+
MY_SETPOINT = "my_setpoint"
|
|
27
|
+
|
|
28
|
+
class MyModbusDevice(ModbusDeviceBase):
|
|
29
|
+
def __init__(self, device_info: ModbusDeviceInfo):
|
|
30
|
+
super().__init__(device_info)
|
|
31
|
+
|
|
32
|
+
self._attr_manufacturer="<manufacturer>"
|
|
33
|
+
self._attr_model_name="<model_name>"
|
|
34
|
+
self._attr_datapoints = [
|
|
35
|
+
ModbusDatapoint(key=MyDatapointKey.MAJOR_VERSION, read_address=1, divider=1, signed=True),
|
|
36
|
+
]
|
|
37
|
+
self._attr_setpoints = [
|
|
38
|
+
ModbusSetpoint(key=MySetpointKey.MY_SETPOINT, read_address=1, write_address=1 ,divider=1, min=1, max=10, signed=True),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
class MyModbusDeviceAdapter(ModbusDeviceAdapter):
|
|
42
|
+
|
|
43
|
+
def _translate_to_model(self, device_info: ModbusDeviceInfo) -> Callable[[ModbusDeviceInfo], ModbusDevice]|None:
|
|
44
|
+
return MyModbusDevice
|
|
45
|
+
|
|
46
|
+
class MyModbusTCPEventConnect(ModbusTCPEventConnect):
|
|
47
|
+
_attr_adapter = MyModbusDeviceAdapter()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
### Client Capabilities
|
|
52
|
+
|
|
53
|
+
The Modbus EventConnect offers a range of methods to facilitate seamless interaction with Modbus devices. Key features include:
|
|
54
|
+
|
|
55
|
+
- **Subscribe**: Easily subscribe to data points and receive updates.
|
|
56
|
+
- **Unsubscribe**: Manage your subscriptions effortlessly.
|
|
57
|
+
- **Comprehensive Methods**: A variety of methods to handle different Modbus operations.
|
|
58
|
+
|
|
59
|
+
Let the EventConnect handle the communication, allowing you to focus on responding to changes and managing your Modbus devices efficiently.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Disclaimer
|
|
63
|
+
|
|
64
|
+
Modbus Event Connect is provided "as is", without warranty of any kind. The authors and contributors are not responsible for any damage or data loss that may occur from using this library. Users are solely responsible for ensuring the proper and safe operation of their Modbus devices.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[tool.pytest.ini_options]
|
|
6
|
+
log_cli = true
|
|
7
|
+
log_cli_level = "DEBUG"
|
|
8
|
+
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
|
|
9
|
+
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = modbus_event_connect
|
|
3
|
+
version = attr: modbus_event_connect.__version__
|
|
4
|
+
author = HairingX
|
|
5
|
+
license = MIT
|
|
6
|
+
description = A library to easily interface with modbus devices, be it over TCP, unabto etc.
|
|
7
|
+
keywords = modbus, easy, remote, connect, library
|
|
8
|
+
url = https://github.com/HairingX/modbus_event_connect
|
|
9
|
+
long_description = file: README.md
|
|
10
|
+
long_description_content_type = text/markdown
|
|
11
|
+
classifiers =
|
|
12
|
+
Development Status :: 3 - Alpha
|
|
13
|
+
Programming Language :: Python :: 3.12
|
|
14
|
+
Intended Audience :: Developers
|
|
15
|
+
Topic :: Software Development :: Libraries
|
|
16
|
+
License :: OSI Approved :: MIT License
|
|
17
|
+
|
|
18
|
+
[options]
|
|
19
|
+
python_requires = >=3.12
|
|
20
|
+
packages = find:
|
|
21
|
+
package_dir =
|
|
22
|
+
=src
|
|
23
|
+
zip_safe = False
|
|
24
|
+
install_requires =
|
|
25
|
+
pyModbusTCP ==0.3.0
|
|
26
|
+
|
|
27
|
+
[options.packages.find]
|
|
28
|
+
where = src
|
|
29
|
+
|
|
30
|
+
[egg_info]
|
|
31
|
+
tag_build =
|
|
32
|
+
tag_date = 0
|
|
33
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from .modbus_event_connect import ( ModbusEventConnect )
|
|
2
|
+
from .modbus_deviceadapter import ( ModbusDeviceAdapter )
|
|
3
|
+
from .modbus_models import (
|
|
4
|
+
ModbusDevice,
|
|
5
|
+
ModbusDeviceBase,
|
|
6
|
+
ModbusDeviceInfo,
|
|
7
|
+
ModbusDatapoint,
|
|
8
|
+
ModbusDatapointKey,
|
|
9
|
+
ModbusParser,
|
|
10
|
+
ModbusPointKey,
|
|
11
|
+
ModbusSetpoint,
|
|
12
|
+
ModbusSetpointKey,
|
|
13
|
+
MODBUS_VALUE_TYPES,
|
|
14
|
+
MODIFIER,
|
|
15
|
+
UOM,
|
|
16
|
+
VersionInfo,
|
|
17
|
+
)
|
|
18
|
+
from .micro_nabto.micro_nabto_connection import ( MicroNabtoModbusDeviceInfo )
|
|
19
|
+
from .micro_nabto.micro_nabto_event_connect import ( MicroNabtoEventConnect )
|
|
20
|
+
from .modbus_tcp.modbus_tcp_event_connect import ( ModbusTCPEventConnect )
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ModbusDevice",
|
|
25
|
+
"ModbusDeviceAdapter",
|
|
26
|
+
"ModbusDeviceBase",
|
|
27
|
+
"ModbusDeviceInfo",
|
|
28
|
+
"ModbusDatapoint",
|
|
29
|
+
"ModbusDatapointKey",
|
|
30
|
+
"ModbusParser",
|
|
31
|
+
"ModbusPointKey",
|
|
32
|
+
"ModbusEventConnect",
|
|
33
|
+
"ModbusSetpoint",
|
|
34
|
+
"ModbusSetpointKey",
|
|
35
|
+
"MODBUS_VALUE_TYPES",
|
|
36
|
+
"MODIFIER",
|
|
37
|
+
"UOM",
|
|
38
|
+
"MicroNabtoEventConnect",
|
|
39
|
+
"MicroNabtoModbusDeviceInfo",
|
|
40
|
+
"ModbusTCPEventConnect",
|
|
41
|
+
"VersionInfo",
|
|
42
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, List, Tuple
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from .modbus_models import ( MODBUS_VALUE_TYPES, ModbusDeviceInfo, ModbusPointKey, ModbusDevice, ModbusDatapoint, ModbusDatapointKey, ModbusSetpoint, ModbusSetpointKey )
|
|
6
|
+
|
|
7
|
+
_LOGGER = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class ModbusDeviceAdapter(ModbusDevice):
|
|
10
|
+
_device_info: ModbusDeviceInfo
|
|
11
|
+
_loaded_model: ModbusDevice|None
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def instantiate(self, device_info: ModbusDeviceInfo) -> None:
|
|
17
|
+
self._device_info = device_info
|
|
18
|
+
model_to_load = self._translate_to_model(device_info)
|
|
19
|
+
if model_to_load == None:
|
|
20
|
+
raise Exception("Invalid model")
|
|
21
|
+
loaded_model = model_to_load(device_info)
|
|
22
|
+
loaded_model.instantiate(device_info)
|
|
23
|
+
self._loaded_model = loaded_model
|
|
24
|
+
self.ready = True
|
|
25
|
+
|
|
26
|
+
def _translate_to_model(self, device_info: ModbusDeviceInfo) -> Callable[[ModbusDeviceInfo], ModbusDevice]|None:
|
|
27
|
+
"""Translate the model to the correct device model, must be implemented in the subclass"""
|
|
28
|
+
raise NotImplementedError("Method not implemented")
|
|
29
|
+
|
|
30
|
+
def provides_model(self, device_info: ModbusDeviceInfo) -> bool:
|
|
31
|
+
return self._translate_to_model(device_info) is not None
|
|
32
|
+
|
|
33
|
+
#region ModbusDevice
|
|
34
|
+
|
|
35
|
+
def _get_loaded_model(self) -> ModbusDevice:
|
|
36
|
+
if self._loaded_model is None:
|
|
37
|
+
raise Exception("No model loaded")
|
|
38
|
+
return self._loaded_model
|
|
39
|
+
|
|
40
|
+
def get_datapoint(self, key: ModbusDatapointKey) -> ModbusDatapoint|None:
|
|
41
|
+
return self._get_loaded_model().get_datapoint(key)
|
|
42
|
+
|
|
43
|
+
def get_datapoints_for_read(self) -> List[ModbusDatapoint]:
|
|
44
|
+
return self._get_loaded_model().get_datapoints_for_read()
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def device_id(self): return self._get_loaded_model().device_id
|
|
48
|
+
@property
|
|
49
|
+
def device_host(self): return self._get_loaded_model().device_host
|
|
50
|
+
@property
|
|
51
|
+
def device_port(self): return self._get_loaded_model().device_port
|
|
52
|
+
@property
|
|
53
|
+
def identification(self): return self._get_loaded_model().identification
|
|
54
|
+
@property
|
|
55
|
+
def manufacturer(self): return self._get_loaded_model().manufacturer
|
|
56
|
+
@property
|
|
57
|
+
def model_name(self): return self._get_loaded_model().model_name
|
|
58
|
+
@property
|
|
59
|
+
def version(self): return self._get_loaded_model().version
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_max_value(self, key: ModbusSetpointKey) -> float|int|None:
|
|
63
|
+
return self._get_loaded_model().get_max_value(key)
|
|
64
|
+
|
|
65
|
+
def get_min_value(self, key: ModbusSetpointKey) -> float|int|None:
|
|
66
|
+
return self._get_loaded_model().get_min_value(key)
|
|
67
|
+
|
|
68
|
+
def get_setpoint(self, key: ModbusSetpointKey) -> ModbusSetpoint|None:
|
|
69
|
+
return self._get_loaded_model().get_setpoint(key)
|
|
70
|
+
|
|
71
|
+
def get_setpoint_step(self, key: ModbusSetpointKey) -> float|int:
|
|
72
|
+
return self._get_loaded_model().get_setpoint_step(key)
|
|
73
|
+
|
|
74
|
+
def get_setpoints_for_read(self) -> List[ModbusSetpoint]:
|
|
75
|
+
return self._get_loaded_model().get_setpoints_for_read()
|
|
76
|
+
|
|
77
|
+
def get_unit_of_measure(self, key: ModbusPointKey) -> str|None:
|
|
78
|
+
return self._get_loaded_model().get_unit_of_measure(key)
|
|
79
|
+
|
|
80
|
+
def get_value(self, key: ModbusPointKey) -> MODBUS_VALUE_TYPES|None:
|
|
81
|
+
return self._get_loaded_model().get_value(key)
|
|
82
|
+
|
|
83
|
+
def get_values(self) -> Dict[ModbusPointKey, MODBUS_VALUE_TYPES|None]:
|
|
84
|
+
return self._get_loaded_model().get_values()
|
|
85
|
+
|
|
86
|
+
def has_value(self, key: ModbusPointKey) -> bool:
|
|
87
|
+
return self._get_loaded_model().has_value(key)
|
|
88
|
+
|
|
89
|
+
def provides(self, key: ModbusPointKey) -> bool:
|
|
90
|
+
return self._get_loaded_model().provides(key)
|
|
91
|
+
|
|
92
|
+
def set_read(self, key: ModbusPointKey, read: bool) -> bool:
|
|
93
|
+
return self._get_loaded_model().set_read(key, read)
|
|
94
|
+
|
|
95
|
+
def set_value(self, key: ModbusPointKey, value: MODBUS_VALUE_TYPES) -> Tuple[MODBUS_VALUE_TYPES|None, MODBUS_VALUE_TYPES|None]:
|
|
96
|
+
return self._get_loaded_model().set_value(key, value)
|
|
97
|
+
|
|
98
|
+
#endregion ModbusDevice
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import AsyncGenerator, Callable, Dict, List, Tuple
|
|
4
|
+
|
|
5
|
+
from .modbus_models import MODBUS_VALUE_TYPES, ModbusDatapoint, ModbusDatapointKey, ModbusSetpoint, ModbusSetpointKey
|
|
6
|
+
from .modbus_deviceadapter import ModbusDeviceAdapter
|
|
7
|
+
|
|
8
|
+
class ModbusEventConnect(ABC):
|
|
9
|
+
_attr_adapter: ModbusDeviceAdapter
|
|
10
|
+
_subscribers: Dict[ModbusDatapointKey|ModbusSetpointKey, List[Callable[[MODBUS_VALUE_TYPES|None, MODBUS_VALUE_TYPES|None], None]]] = {}
|
|
11
|
+
"""Callable[old_value, new_value]"""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def is_connected(self) -> bool:
|
|
15
|
+
raise NotImplementedError("Method not implemented")
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def stop(self) -> None:
|
|
18
|
+
raise NotImplementedError("Method not implemented")
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def _request_datapoint_data(self, points: List[ModbusDatapoint]) -> AsyncGenerator[Tuple[ModbusDatapoint, MODBUS_VALUE_TYPES], None]:
|
|
21
|
+
raise NotImplementedError("Method not implemented")
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def _request_setpoint_data(self, points: List[ModbusSetpoint]) -> AsyncGenerator[Tuple[ModbusSetpoint, MODBUS_VALUE_TYPES], None]:
|
|
24
|
+
raise NotImplementedError("Method not implemented")
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def version(self): return self._attr_adapter.version
|
|
28
|
+
@property
|
|
29
|
+
def device_host(self): return self._attr_adapter.device_host
|
|
30
|
+
@property
|
|
31
|
+
def device_id(self): return self._attr_adapter.device_id
|
|
32
|
+
@property
|
|
33
|
+
def device_port(self): return self._attr_adapter.device_port
|
|
34
|
+
@property
|
|
35
|
+
def identification(self): return self._attr_adapter.identification
|
|
36
|
+
@property
|
|
37
|
+
def manufacturer(self): return self._attr_adapter.manufacturer
|
|
38
|
+
@property
|
|
39
|
+
def model_name(self) -> str: return self._attr_adapter.model_name
|
|
40
|
+
|
|
41
|
+
async def request_datapoint_data(self) -> None:
|
|
42
|
+
"""Request the current value of all subscribed datapoints. All subscribers will be notified of the new value if changed."""
|
|
43
|
+
points = self._attr_adapter.get_datapoints_for_read()
|
|
44
|
+
async for point, value in self._request_datapoint_data(points):
|
|
45
|
+
self._set_value(point.key, value)
|
|
46
|
+
|
|
47
|
+
async def request_setpoint_data(self) -> None:
|
|
48
|
+
"""Request the current value of all subscribed setpoints. All subscribers will be notified of the new value if changed."""
|
|
49
|
+
points = self._attr_adapter.get_setpoints_for_read()
|
|
50
|
+
async for point, value in self._request_setpoint_data(points):
|
|
51
|
+
self._set_value(point.key, value)
|
|
52
|
+
|
|
53
|
+
def subscribe(self, key: ModbusDatapointKey|ModbusSetpointKey, update_method: Callable[[MODBUS_VALUE_TYPES|None, MODBUS_VALUE_TYPES|None], None]):
|
|
54
|
+
"""
|
|
55
|
+
Subscribe to a datapoint or setpoint value change.
|
|
56
|
+
|
|
57
|
+
:param key: The key of the datapoint or setpoint to subscribe to.
|
|
58
|
+
:param update_method: The method to call when the value changes. The Callable will receive old_value and new_value as the two inputs.
|
|
59
|
+
"""
|
|
60
|
+
if key not in self._subscribers:
|
|
61
|
+
self._subscribers[key] = []
|
|
62
|
+
self._attr_adapter.set_read(key, True)
|
|
63
|
+
self._subscribers[key].append(update_method)
|
|
64
|
+
value = self._attr_adapter.get_value(key)
|
|
65
|
+
if value is not None:
|
|
66
|
+
update_method(None, value)
|
|
67
|
+
|
|
68
|
+
def unsubscribe(self, key: ModbusDatapointKey|ModbusSetpointKey, update_method: Callable[[MODBUS_VALUE_TYPES|None, MODBUS_VALUE_TYPES|None], None]):
|
|
69
|
+
"""Remove a subscription to a datapoint or setpoint value change."""
|
|
70
|
+
subscribers = self._subscribers.get(key)
|
|
71
|
+
if subscribers is None: return
|
|
72
|
+
if update_method in subscribers:
|
|
73
|
+
if len(subscribers) == 1:
|
|
74
|
+
del self._subscribers[key]
|
|
75
|
+
self._attr_adapter.set_read(key, False)
|
|
76
|
+
else:
|
|
77
|
+
subscribers.remove(update_method)
|
|
78
|
+
|
|
79
|
+
def _set_value(self, key: ModbusDatapointKey|ModbusSetpointKey, value: MODBUS_VALUE_TYPES):
|
|
80
|
+
oldval, newval = self._attr_adapter.set_value(key, value)
|
|
81
|
+
self._notify_subscribers(key, oldval, newval)
|
|
82
|
+
|
|
83
|
+
def _notify_subscribers(self, key: ModbusDatapointKey|ModbusSetpointKey, old_value: MODBUS_VALUE_TYPES|None, new_value: MODBUS_VALUE_TYPES|None):
|
|
84
|
+
subscribers = self._subscribers.get(key)
|
|
85
|
+
if subscribers is None: return
|
|
86
|
+
for subscriber in subscribers:
|
|
87
|
+
subscriber(old_value, new_value)
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Dict, List, NotRequired, Optional, Tuple, TypedDict
|
|
6
|
+
|
|
7
|
+
MODBUS_VALUE_TYPES = float|int|str
|
|
8
|
+
|
|
9
|
+
class ModbusPointKey(StrEnum):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class ModbusDatapointKey(ModbusPointKey):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class ModbusSetpointKey(ModbusPointKey):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class UOM:
|
|
19
|
+
SECONDS = "seconds"
|
|
20
|
+
MINUTES = "minutes"
|
|
21
|
+
HOURS = "hours"
|
|
22
|
+
DAYS = "days"
|
|
23
|
+
MONTHS = "months"
|
|
24
|
+
YEARS = "years"
|
|
25
|
+
CELSIUS = "celsius"
|
|
26
|
+
BOOL = "bool"
|
|
27
|
+
BITMASK = "bitmask"
|
|
28
|
+
PPM = "ppm"
|
|
29
|
+
"""CONCENTRATION PARTS PER MILLION"""
|
|
30
|
+
RPM = "rpm"
|
|
31
|
+
"""REVOLUTIONS PER MINUTE"""
|
|
32
|
+
# INT = "int"
|
|
33
|
+
# FLOAT = "float"
|
|
34
|
+
PCT = "percent"
|
|
35
|
+
TEXT = "text"
|
|
36
|
+
UNKNOWN = None
|
|
37
|
+
|
|
38
|
+
class ModbusPointExtras(TypedDict):
|
|
39
|
+
unit_of_measurement: NotRequired[str|None]
|
|
40
|
+
"""Unit of measurement for the value, UOM class contains the standard units, defaults to None"""
|
|
41
|
+
always_read: NotRequired[bool|None]
|
|
42
|
+
"""Indication if the point should always be read from the device. If not True, the point will only be read if explicitly requested"""
|
|
43
|
+
|
|
44
|
+
@dataclass(kw_only=True)
|
|
45
|
+
class ModbusDatapoint:
|
|
46
|
+
key: ModbusDatapointKey
|
|
47
|
+
extra: Optional[ModbusPointExtras|None] = None
|
|
48
|
+
#read
|
|
49
|
+
read_address: int
|
|
50
|
+
signed: bool
|
|
51
|
+
"""indication of the data being signed or unsigned (default=True)"""
|
|
52
|
+
divider: Optional[int|None] = None
|
|
53
|
+
"""Applied to the register value in the order: 1: divider, 2: offset, 3: modifier"""
|
|
54
|
+
offset: Optional[int|None] = None
|
|
55
|
+
"""Applied to the register value in the order: 1: divider, 2: offset, 3: modifier"""
|
|
56
|
+
read_modifier: Optional[Callable[[float|int], float|int]|None] = None
|
|
57
|
+
"""Modifier applied to value after it has been parsed by the system. can be used to alter hours to days etc. or round floating values
|
|
58
|
+
Applied to the register value in the order: 1: divider, 2: offset, 3: modifier"""
|
|
59
|
+
read_obj: Optional[int|None] = None
|
|
60
|
+
"""default is 0"""
|
|
61
|
+
|
|
62
|
+
@dataclass(kw_only=True)
|
|
63
|
+
class ModbusSetpoint:
|
|
64
|
+
key: ModbusSetpointKey
|
|
65
|
+
extra: Optional[ModbusPointExtras|None] = None
|
|
66
|
+
#read
|
|
67
|
+
read_address: Optional[int|None] = None
|
|
68
|
+
signed: bool
|
|
69
|
+
"""indication of the data being signed or unsigned (default=True)"""
|
|
70
|
+
divider: Optional[int|None] = None
|
|
71
|
+
"""Applied to the register value in the order: 1: divider, 2: offset, 3: modifier"""
|
|
72
|
+
offset: Optional[int|None] = None
|
|
73
|
+
"""Applied to the register value in the order: 1: divider, 2: offset, 3: modifier"""
|
|
74
|
+
read_modifier: Optional[Callable[[float|int], float|int]|None] = None
|
|
75
|
+
"""Modifier applied to value after it has been parsed by the system. can be used to alter hours to days etc. or round floating values
|
|
76
|
+
Applied to the register value in the order: 1: divider, 2: offset, 3: modifier"""
|
|
77
|
+
read_obj: Optional[int|None] = None
|
|
78
|
+
"""default is 0"""
|
|
79
|
+
#write
|
|
80
|
+
max: int
|
|
81
|
+
"""max value in the register"""
|
|
82
|
+
min: int
|
|
83
|
+
"""min value in the register"""
|
|
84
|
+
write_address: int|None = None
|
|
85
|
+
step: Optional[int|None] = None
|
|
86
|
+
"""step size in register value, if unset will default to the divider"""
|
|
87
|
+
write_modifier: Optional[Callable[[float|int], float|int]|None] = None
|
|
88
|
+
"""Modifier applied to value before it has been parsed back to register type. can be used to alter hours to days etc. or round floating values"""
|
|
89
|
+
write_obj: Optional[int|None] = None
|
|
90
|
+
"""default is 0"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ModbusDatapointData:
|
|
94
|
+
point: ModbusDatapoint
|
|
95
|
+
read: bool
|
|
96
|
+
value: MODBUS_VALUE_TYPES|None = None
|
|
97
|
+
unit_of_measurement: str|None = None
|
|
98
|
+
def __init__(self, point: ModbusDatapoint):
|
|
99
|
+
self.point = point
|
|
100
|
+
self.read = point.extra is not None and "always_read" in point.extra and point.extra["always_read"] == True
|
|
101
|
+
self.unit_of_measurement = point.extra["unit_of_measurement"] if point.extra is not None and "unit_of_measurement" in point.extra else None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ModbusSetpointData:
|
|
105
|
+
point: ModbusSetpoint
|
|
106
|
+
read: bool
|
|
107
|
+
value: MODBUS_VALUE_TYPES|None = None
|
|
108
|
+
unit_of_measurement: str|None = None
|
|
109
|
+
def __init__(self, point: ModbusSetpoint):
|
|
110
|
+
self.point = point
|
|
111
|
+
self.read = point.extra is not None and "always_read" in point.extra and point.extra["always_read"] == True
|
|
112
|
+
self.unit_of_measurement = point.extra["unit_of_measurement"] if point.extra is not None and "unit_of_measurement" in point.extra else None
|
|
113
|
+
|
|
114
|
+
@dataclass()
|
|
115
|
+
class VersionInfo:
|
|
116
|
+
major: int
|
|
117
|
+
minor: int
|
|
118
|
+
patch: int
|
|
119
|
+
|
|
120
|
+
@dataclass(kw_only=True)
|
|
121
|
+
class ModbusDeviceIdenfication:
|
|
122
|
+
vendor_name: str|None
|
|
123
|
+
product_code: str|None
|
|
124
|
+
major_minor_revision: str|None
|
|
125
|
+
vendor_url: str|None
|
|
126
|
+
product_name: str|None
|
|
127
|
+
model_name: str|None
|
|
128
|
+
user_application_name: str|None
|
|
129
|
+
|
|
130
|
+
@dataclass(kw_only=True)
|
|
131
|
+
class ModbusDeviceInfo:
|
|
132
|
+
device_id: str
|
|
133
|
+
device_host: str
|
|
134
|
+
device_port: int
|
|
135
|
+
version: VersionInfo
|
|
136
|
+
identification: ModbusDeviceIdenfication|None
|
|
137
|
+
|
|
138
|
+
class ModbusDevice(ABC):
|
|
139
|
+
ready: bool = False
|
|
140
|
+
_device_info: ModbusDeviceInfo
|
|
141
|
+
|
|
142
|
+
def __init__(self, device_info: ModbusDeviceInfo) -> None:
|
|
143
|
+
self._device_info = device_info
|
|
144
|
+
|
|
145
|
+
def instantiate(self, device_info: ModbusDeviceInfo) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Sets up the device for usage and raises errors if device has issues.
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: If the an attribute is not set.
|
|
150
|
+
"""
|
|
151
|
+
self.ready = True
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def device_id(self) -> str: return self._device_info.device_id
|
|
155
|
+
@property
|
|
156
|
+
def device_host(self) -> str: return self._device_info.device_host
|
|
157
|
+
@property
|
|
158
|
+
def device_port(self) -> int: return self._device_info.device_port
|
|
159
|
+
@property
|
|
160
|
+
def identification(self) -> ModbusDeviceIdenfication|None: return self._device_info.identification
|
|
161
|
+
@property
|
|
162
|
+
def version(self) -> VersionInfo: return self._device_info.version
|
|
163
|
+
@property
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def manufacturer(self) -> str:
|
|
166
|
+
raise NotImplementedError("Method not implemented")
|
|
167
|
+
@property
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def model_name(self) -> str:
|
|
170
|
+
raise NotImplementedError("Method not implemented")
|
|
171
|
+
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def get_datapoint(self, key: ModbusDatapointKey) -> ModbusDatapoint|None:
|
|
174
|
+
raise NotImplementedError("Method not implemented")
|
|
175
|
+
@abstractmethod
|
|
176
|
+
def get_datapoints_for_read(self) -> List[ModbusDatapoint]:
|
|
177
|
+
raise NotImplementedError("Method not implemented")
|
|
178
|
+
@abstractmethod
|
|
179
|
+
def get_max_value(self, key: ModbusSetpointKey) -> float|int|None:
|
|
180
|
+
raise NotImplementedError("Method not implemented")
|
|
181
|
+
@abstractmethod
|
|
182
|
+
def get_min_value(self, key: ModbusSetpointKey) -> float|int|None:
|
|
183
|
+
raise NotImplementedError("Method not implemented")
|
|
184
|
+
@abstractmethod
|
|
185
|
+
def get_setpoint(self, key: ModbusSetpointKey) -> ModbusSetpoint|None:
|
|
186
|
+
raise NotImplementedError("Method not implemented")
|
|
187
|
+
@abstractmethod
|
|
188
|
+
def get_setpoint_step(self, key: ModbusSetpointKey) -> float|int:
|
|
189
|
+
raise NotImplementedError("Method not implemented")
|
|
190
|
+
@abstractmethod
|
|
191
|
+
def get_setpoints_for_read(self) -> List[ModbusSetpoint]:
|
|
192
|
+
raise NotImplementedError("Method not implemented")
|
|
193
|
+
@abstractmethod
|
|
194
|
+
def get_unit_of_measure(self, key: ModbusPointKey) -> str|None:
|
|
195
|
+
raise NotImplementedError("Method not implemented")
|
|
196
|
+
@abstractmethod
|
|
197
|
+
def get_value(self, key: ModbusPointKey) -> MODBUS_VALUE_TYPES|None:
|
|
198
|
+
raise NotImplementedError("Method not implemented")
|
|
199
|
+
@abstractmethod
|
|
200
|
+
def get_values(self) -> Dict[ModbusPointKey, MODBUS_VALUE_TYPES|None]:
|
|
201
|
+
raise NotImplementedError("Method not implemented")
|
|
202
|
+
@abstractmethod
|
|
203
|
+
def has_value(self, key: ModbusPointKey) -> bool:
|
|
204
|
+
raise NotImplementedError("Method not implemented")
|
|
205
|
+
@abstractmethod
|
|
206
|
+
def provides(self, key: ModbusPointKey) -> bool:
|
|
207
|
+
raise NotImplementedError("Method not implemented")
|
|
208
|
+
@abstractmethod
|
|
209
|
+
def set_read(self, key: ModbusPointKey, read: bool) -> bool:
|
|
210
|
+
raise NotImplementedError("Method not implemented")
|
|
211
|
+
@abstractmethod
|
|
212
|
+
def set_value(self, key: ModbusPointKey, value: MODBUS_VALUE_TYPES) -> Tuple[MODBUS_VALUE_TYPES|None, MODBUS_VALUE_TYPES|None]:
|
|
213
|
+
"""Sets the value for the key and returns the old and new value"""
|
|
214
|
+
raise NotImplementedError("Method not implemented")
|
|
215
|
+
|
|
216
|
+
class ModbusDeviceBase(ModbusDevice):
|
|
217
|
+
_attr_manufacturer:str = ""
|
|
218
|
+
"""Manufacturer of the device. Must be assigned in the __init__ method"""
|
|
219
|
+
_attr_model_name:str = ""
|
|
220
|
+
"""Model name of the device. Must be assigned in the __init__ method"""
|
|
221
|
+
_attr_datapoints: List[ModbusDatapoint]
|
|
222
|
+
"""Datapoints for the device. Must be assigned in the __init__ method"""
|
|
223
|
+
_attr_setpoints: List[ModbusSetpoint]
|
|
224
|
+
"""Setpoints for the device. Must be assigned in the __init__ method"""
|
|
225
|
+
_attr_default_extras = dict[ModbusPointKey, ModbusPointExtras]()
|
|
226
|
+
"""Default extras for the device. Can be assigned in the __init__ method"""
|
|
227
|
+
|
|
228
|
+
_datapoints = dict[ModbusDatapointKey, ModbusDatapointData]()
|
|
229
|
+
_setpoints = dict[ModbusSetpointKey, ModbusSetpointData]()
|
|
230
|
+
|
|
231
|
+
def __init__(self, device_info: ModbusDeviceInfo) -> None:
|
|
232
|
+
super().__init__(device_info)
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def manufacturer(self) -> str:
|
|
236
|
+
return self._attr_manufacturer
|
|
237
|
+
@property
|
|
238
|
+
def model_name(self) -> str:
|
|
239
|
+
return self._attr_model_name
|
|
240
|
+
|
|
241
|
+
def instantiate(self, device_info: ModbusDeviceInfo) -> None:
|
|
242
|
+
if not self._attr_manufacturer:
|
|
243
|
+
raise ValueError("Manufacturer not set")
|
|
244
|
+
if not self._attr_model_name:
|
|
245
|
+
raise ValueError("Model name not set")
|
|
246
|
+
if not hasattr(self, '_attr_datapoints'):
|
|
247
|
+
raise ValueError("Datapoints not set")
|
|
248
|
+
if not hasattr(self, '_attr_setpoints'):
|
|
249
|
+
raise ValueError("Setpoints not set")
|
|
250
|
+
|
|
251
|
+
for point in self._attr_datapoints:
|
|
252
|
+
point.extra = point.extra or self._attr_default_extras.get(point.key)
|
|
253
|
+
self._datapoints = {point.key: ModbusDatapointData(point) for point in self._attr_datapoints}
|
|
254
|
+
|
|
255
|
+
for point in self._attr_setpoints:
|
|
256
|
+
point.extra = point.extra or self._attr_default_extras.get(point.key)
|
|
257
|
+
self._setpoints = {point.key: ModbusSetpointData(point) for point in self._attr_setpoints}
|
|
258
|
+
|
|
259
|
+
def get_datapoint(self, key: ModbusDatapointKey) -> ModbusDatapoint | None:
|
|
260
|
+
data = self._datapoints.get(key)
|
|
261
|
+
return data.point if data is not None else None
|
|
262
|
+
|
|
263
|
+
def get_datapoints_for_read(self) -> List[ModbusDatapoint]:
|
|
264
|
+
return [value.point for value in self._datapoints.values() if value.read]
|
|
265
|
+
|
|
266
|
+
def get_max_value(self, key: ModbusSetpointKey) -> float | int | None:
|
|
267
|
+
if self.provides(key):
|
|
268
|
+
point = self._setpoints[key].point
|
|
269
|
+
return ModbusParser.parse_from_modbus_value(point=point, value=point.max)
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
def get_min_value(self, key: ModbusSetpointKey) -> float | int | None:
|
|
273
|
+
if self.provides(key):
|
|
274
|
+
point = self._setpoints[key].point
|
|
275
|
+
return ModbusParser.parse_from_modbus_value(point=point, value=point.min)
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
def get_setpoint(self, key: ModbusSetpointKey) -> ModbusSetpoint | None:
|
|
279
|
+
data = self._setpoints.get(key)
|
|
280
|
+
return data.point if data is not None else None
|
|
281
|
+
|
|
282
|
+
def get_setpoint_step(self, key: ModbusSetpointKey) -> float|int:
|
|
283
|
+
data = self._setpoints.get(key)
|
|
284
|
+
if data is not None:
|
|
285
|
+
divider = ModbusParser.get_point_divider(data.point)
|
|
286
|
+
step = ModbusParser.get_point_step(data.point)
|
|
287
|
+
if divider > 1: return step / divider
|
|
288
|
+
return step
|
|
289
|
+
return 1
|
|
290
|
+
|
|
291
|
+
def get_setpoints_for_read(self) -> List[ModbusSetpoint]:
|
|
292
|
+
return [value.point for value in self._setpoints.values() if value.read]
|
|
293
|
+
|
|
294
|
+
def get_unit_of_measure(self, key: ModbusPointKey) -> str | None:
|
|
295
|
+
if isinstance(key, ModbusDatapointKey):
|
|
296
|
+
data = self._datapoints.get(key)
|
|
297
|
+
if data is not None:
|
|
298
|
+
return data.unit_of_measurement
|
|
299
|
+
elif isinstance(key, ModbusSetpointKey):
|
|
300
|
+
data = self._setpoints.get(key)
|
|
301
|
+
if data is not None:
|
|
302
|
+
return data.unit_of_measurement
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
def get_value(self, key: ModbusPointKey) -> MODBUS_VALUE_TYPES|None:
|
|
306
|
+
if isinstance(key, ModbusDatapointKey):
|
|
307
|
+
point = self._datapoints.get(key)
|
|
308
|
+
if point is not None:
|
|
309
|
+
return point.value
|
|
310
|
+
elif isinstance(key, ModbusSetpointKey):
|
|
311
|
+
point = self._setpoints.get(key)
|
|
312
|
+
if point is not None:
|
|
313
|
+
return point.value
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
def get_values(self) -> Dict[ModbusPointKey, MODBUS_VALUE_TYPES|None]:
|
|
317
|
+
datapoints = {key: value.value for key, value in self._datapoints.items() if value.read}
|
|
318
|
+
setpoints = {key: value.value for key, value in self._setpoints.items() if value.read}
|
|
319
|
+
return {**datapoints, **setpoints}
|
|
320
|
+
|
|
321
|
+
def has_value(self, key: ModbusPointKey) -> bool:
|
|
322
|
+
if isinstance(key, ModbusDatapointKey):
|
|
323
|
+
return key in self._datapoints
|
|
324
|
+
elif isinstance(key, ModbusSetpointKey):
|
|
325
|
+
return key in self._setpoints
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
def provides(self, key: ModbusPointKey) -> bool:
|
|
329
|
+
return key in self._datapoints or key in self._setpoints
|
|
330
|
+
|
|
331
|
+
def set_read(self, key: ModbusPointKey, read: bool) -> bool:
|
|
332
|
+
"""
|
|
333
|
+
Sets the read state for the point. Returns the new read state.
|
|
334
|
+
If the device sets the always_read to True in the configuration for the key, the read state can never be set to False.
|
|
335
|
+
"""
|
|
336
|
+
if key in self._datapoints:
|
|
337
|
+
self._datapoints[key].read = read
|
|
338
|
+
return True
|
|
339
|
+
elif key in self._setpoints:
|
|
340
|
+
data = self._setpoints[key]
|
|
341
|
+
if data.point.read_address is not None:
|
|
342
|
+
data.read = read
|
|
343
|
+
return True
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
def set_value(self, key: ModbusPointKey, value: MODBUS_VALUE_TYPES) -> Tuple[MODBUS_VALUE_TYPES|None, MODBUS_VALUE_TYPES|None]:
|
|
347
|
+
old_value:MODBUS_VALUE_TYPES|None = None
|
|
348
|
+
assigned_value:MODBUS_VALUE_TYPES|None = None
|
|
349
|
+
if key in self._datapoints:
|
|
350
|
+
data = self._datapoints.get(key)
|
|
351
|
+
if data is not None:
|
|
352
|
+
old_value = data.value
|
|
353
|
+
assigned_value = data.value = value
|
|
354
|
+
elif key in self._setpoints:
|
|
355
|
+
data = self._setpoints.get(key)
|
|
356
|
+
if data is not None:
|
|
357
|
+
assigned_value = data.value = value
|
|
358
|
+
return (old_value, assigned_value)
|
|
359
|
+
|
|
360
|
+
class MODIFIER:
|
|
361
|
+
@staticmethod
|
|
362
|
+
def flip_bool(value:float|int) -> float|int:
|
|
363
|
+
"""Flips the true/false state
|
|
364
|
+
- 1 -> 0
|
|
365
|
+
- 0 -> 1"""
|
|
366
|
+
return 1-value
|
|
367
|
+
|
|
368
|
+
@staticmethod
|
|
369
|
+
def seconds_to_minutes(value:float|int) -> float|int:
|
|
370
|
+
return round(value/60)
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def hours_to_days(value:float|int) -> float|int:
|
|
374
|
+
return round(value/24)
|
|
375
|
+
|
|
376
|
+
class ModbusParser:
|
|
377
|
+
@staticmethod
|
|
378
|
+
def parse_to_modbus_value(point:ModbusSetpoint, value: float|int) -> int:
|
|
379
|
+
divider = ModbusParser.get_point_divider(point)
|
|
380
|
+
offset = ModbusParser.get_point_offset(point)
|
|
381
|
+
modifier = ModbusParser.get_point_write_modifier(point)
|
|
382
|
+
new_value:float|int = value
|
|
383
|
+
if modifier is not None: new_value = modifier(new_value)
|
|
384
|
+
if divider > 1: new_value *= divider
|
|
385
|
+
if offset != 0: new_value -= offset
|
|
386
|
+
return int(new_value) #cast to int, modbus writes only accept an int
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def parse_from_modbus_value(point:ModbusDatapoint|ModbusSetpoint, value: int) -> float|int:
|
|
390
|
+
divider = ModbusParser.get_point_divider(point)
|
|
391
|
+
offset = ModbusParser.get_point_offset(point)
|
|
392
|
+
modifier = ModbusParser.get_point_read_modifier(point)
|
|
393
|
+
new_value:float|int = value
|
|
394
|
+
if offset != 0: new_value += offset
|
|
395
|
+
if divider > 1: new_value /= divider
|
|
396
|
+
if modifier is not None: new_value = modifier(new_value)
|
|
397
|
+
return new_value
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def get_point_divider(point:ModbusDatapoint|ModbusSetpoint) -> int:
|
|
401
|
+
return 1 if point.divider is None else point.divider
|
|
402
|
+
@staticmethod
|
|
403
|
+
def get_point_offset(point:ModbusDatapoint|ModbusSetpoint) -> int:
|
|
404
|
+
return 0 if point.offset is None else point.offset
|
|
405
|
+
@staticmethod
|
|
406
|
+
def get_point_read_address(point:ModbusDatapoint|ModbusSetpoint) -> int|None:
|
|
407
|
+
return point.read_address
|
|
408
|
+
@staticmethod
|
|
409
|
+
def get_point_read_obj(point:ModbusDatapoint|ModbusSetpoint) -> int:
|
|
410
|
+
return 0 if point.read_obj is None else point.read_obj
|
|
411
|
+
@staticmethod
|
|
412
|
+
def get_point_write_address(point:ModbusSetpoint) -> int|None:
|
|
413
|
+
return point.write_address
|
|
414
|
+
@staticmethod
|
|
415
|
+
def get_point_write_obj(point:ModbusSetpoint) -> int:
|
|
416
|
+
return 0 if point.write_obj is None else point.write_obj
|
|
417
|
+
@staticmethod
|
|
418
|
+
def get_point_signed(point:ModbusDatapoint|ModbusSetpoint) -> bool:
|
|
419
|
+
return point.signed
|
|
420
|
+
@staticmethod
|
|
421
|
+
def get_point_step(point:ModbusSetpoint) -> int:
|
|
422
|
+
return 1 if point.step is None else point.step
|
|
423
|
+
@staticmethod
|
|
424
|
+
def get_point_max(point:ModbusSetpoint) -> int:
|
|
425
|
+
return point.max
|
|
426
|
+
@staticmethod
|
|
427
|
+
def get_point_min(point:ModbusSetpoint) -> int:
|
|
428
|
+
return point.min
|
|
429
|
+
@staticmethod
|
|
430
|
+
def get_point_read_modifier(point:ModbusDatapoint|ModbusSetpoint) -> Callable[[float|int], float|int]|None:
|
|
431
|
+
return None if point.read_modifier is None else point.read_modifier
|
|
432
|
+
@staticmethod
|
|
433
|
+
def get_point_write_modifier(point: ModbusSetpoint) -> Callable[[float|int], float|int]|None:
|
|
434
|
+
return None if point.write_modifier is None else point.write_modifier
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: modbus_event_connect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A library to easily interface with modbus devices, be it over TCP, unabto etc.
|
|
5
|
+
Home-page: https://github.com/HairingX/modbus_event_connect
|
|
6
|
+
Author: HairingX
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: modbus,easy,remote,connect,library
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: pyModbusTCP==0.3.0
|
|
18
|
+
|
|
19
|
+
# Modbus Event Connect
|
|
20
|
+
|
|
21
|
+
Welcome to **Modbus Event Connect** – a user-friendly Python library designed to simplify the configuration of Modbus devices. Whether you're connecting via TCP or Unabto, Modbus Event Connect has got you covered.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Easy Configuration**: Intuitive setup for Modbus devices.
|
|
26
|
+
- **Flexible Connectivity**: Supports connection through TCP or Unabto.
|
|
27
|
+
- **User-Friendly**: Handles the interaction; you just define the Modbus register items.
|
|
28
|
+
|
|
29
|
+
## Getting Started
|
|
30
|
+
|
|
31
|
+
1. **Installation**: Install the library using pip.
|
|
32
|
+
```bash
|
|
33
|
+
pip install modbus-event-connect
|
|
34
|
+
```
|
|
35
|
+
2. **Basic Usage**: Define your DataPointKeys to a Modbus device with minimal code.
|
|
36
|
+
```python
|
|
37
|
+
from collections.abc import Callable
|
|
38
|
+
from src.modbus_event_connect import *
|
|
39
|
+
|
|
40
|
+
class MyDatapointKey(ModbusDatapointKey):
|
|
41
|
+
MAJOR_VERSION = "major_version"
|
|
42
|
+
|
|
43
|
+
class MySetpointKey(ModbusSetpointKey):
|
|
44
|
+
MY_SETPOINT = "my_setpoint"
|
|
45
|
+
|
|
46
|
+
class MyModbusDevice(ModbusDeviceBase):
|
|
47
|
+
def __init__(self, device_info: ModbusDeviceInfo):
|
|
48
|
+
super().__init__(device_info)
|
|
49
|
+
|
|
50
|
+
self._attr_manufacturer="<manufacturer>"
|
|
51
|
+
self._attr_model_name="<model_name>"
|
|
52
|
+
self._attr_datapoints = [
|
|
53
|
+
ModbusDatapoint(key=MyDatapointKey.MAJOR_VERSION, read_address=1, divider=1, signed=True),
|
|
54
|
+
]
|
|
55
|
+
self._attr_setpoints = [
|
|
56
|
+
ModbusSetpoint(key=MySetpointKey.MY_SETPOINT, read_address=1, write_address=1 ,divider=1, min=1, max=10, signed=True),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
class MyModbusDeviceAdapter(ModbusDeviceAdapter):
|
|
60
|
+
|
|
61
|
+
def _translate_to_model(self, device_info: ModbusDeviceInfo) -> Callable[[ModbusDeviceInfo], ModbusDevice]|None:
|
|
62
|
+
return MyModbusDevice
|
|
63
|
+
|
|
64
|
+
class MyModbusTCPEventConnect(ModbusTCPEventConnect):
|
|
65
|
+
_attr_adapter = MyModbusDeviceAdapter()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
### Client Capabilities
|
|
70
|
+
|
|
71
|
+
The Modbus EventConnect offers a range of methods to facilitate seamless interaction with Modbus devices. Key features include:
|
|
72
|
+
|
|
73
|
+
- **Subscribe**: Easily subscribe to data points and receive updates.
|
|
74
|
+
- **Unsubscribe**: Manage your subscriptions effortlessly.
|
|
75
|
+
- **Comprehensive Methods**: A variety of methods to handle different Modbus operations.
|
|
76
|
+
|
|
77
|
+
Let the EventConnect handle the communication, allowing you to focus on responding to changes and managing your Modbus devices efficiently.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Disclaimer
|
|
81
|
+
|
|
82
|
+
Modbus Event Connect is provided "as is", without warranty of any kind. The authors and contributors are not responsible for any damage or data loss that may occur from using this library. Users are solely responsible for ensuring the proper and safe operation of their Modbus devices.
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
setup.py
|
|
6
|
+
src/modbus_event_connect/__init__.py
|
|
7
|
+
src/modbus_event_connect/modbus_deviceadapter.py
|
|
8
|
+
src/modbus_event_connect/modbus_event_connect.py
|
|
9
|
+
src/modbus_event_connect/modbus_models.py
|
|
10
|
+
src/modbus_event_connect.egg-info/PKG-INFO
|
|
11
|
+
src/modbus_event_connect.egg-info/SOURCES.txt
|
|
12
|
+
src/modbus_event_connect.egg-info/dependency_links.txt
|
|
13
|
+
src/modbus_event_connect.egg-info/not-zip-safe
|
|
14
|
+
src/modbus_event_connect.egg-info/requires.txt
|
|
15
|
+
src/modbus_event_connect.egg-info/top_level.txt
|
|
16
|
+
tests/test_micro_nabto.py
|
|
17
|
+
tests/test_modbus_tcp.py
|
|
18
|
+
tests/test_test.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyModbusTCP==0.3.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
modbus_event_connect
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
# import json
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Any
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from credentials import Credentials
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from src.modbus_event_connect.micro_nabto.micro_nabto_connection import Request
|
|
12
|
+
from src.modbus_event_connect import MODBUS_VALUE_TYPES
|
|
13
|
+
from models.micro_nabto_test_models import ModbusTestDatapointKey, ModbusTestMicroNabto
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
_LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class TestData:
|
|
20
|
+
client: ModbusTestMicroNabto
|
|
21
|
+
data = dict[str, Any]()
|
|
22
|
+
credentials: Credentials
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def testdata():
|
|
26
|
+
#setup
|
|
27
|
+
credentials = Credentials(["username", "hostname"])
|
|
28
|
+
client = ModbusTestMicroNabto()
|
|
29
|
+
#
|
|
30
|
+
yield TestData(client=client, credentials=credentials)
|
|
31
|
+
#teardown
|
|
32
|
+
client.stop()
|
|
33
|
+
|
|
34
|
+
async def test_connection_request():
|
|
35
|
+
request = Request(1)
|
|
36
|
+
responsetask = request.wait_for_response()
|
|
37
|
+
request.notify_waiters()
|
|
38
|
+
await responsetask
|
|
39
|
+
|
|
40
|
+
async def test_connection_request_thread():
|
|
41
|
+
request = Request( 1)
|
|
42
|
+
def thread_method():
|
|
43
|
+
asyncio.run(asyncio.sleep(1))
|
|
44
|
+
request.notify_waiters()
|
|
45
|
+
_listen_thread = threading.Thread(target=thread_method)
|
|
46
|
+
_listen_thread.start()
|
|
47
|
+
await request.wait_for_response()
|
|
48
|
+
|
|
49
|
+
async def test_connection_request_asyncio_loop():
|
|
50
|
+
request = Request(1)
|
|
51
|
+
def thread_method():
|
|
52
|
+
asyncio.run(asyncio.sleep(1))
|
|
53
|
+
request.notify_waiters()
|
|
54
|
+
_loop = asyncio.get_event_loop()
|
|
55
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
56
|
+
_loop.run_in_executor(pool, thread_method)
|
|
57
|
+
await request.wait_for_response()
|
|
58
|
+
|
|
59
|
+
async def test_connect(testdata: TestData):
|
|
60
|
+
credentials = testdata.credentials
|
|
61
|
+
client = testdata.client
|
|
62
|
+
success = await client.connect(credentials.username, "DEVICE_ID", credentials.hostname)
|
|
63
|
+
assert success
|
|
64
|
+
|
|
65
|
+
async def test_datapoint(testdata: TestData):
|
|
66
|
+
credentials = testdata.credentials
|
|
67
|
+
client = testdata.client
|
|
68
|
+
success = await client.connect(credentials.username, "DEVICE_ID", credentials.hostname)
|
|
69
|
+
assert success
|
|
70
|
+
await client.request_datapoint_data()
|
|
71
|
+
event = asyncio.Event()
|
|
72
|
+
def callback(oldval:MODBUS_VALUE_TYPES|None, newval:MODBUS_VALUE_TYPES|None):
|
|
73
|
+
_LOGGER.debug(f"{ModbusTestDatapointKey.MAJOR_VERSION}: {oldval if oldval is not None else 'None'} -> {newval if newval is not None else 'None'}")
|
|
74
|
+
event.set()
|
|
75
|
+
client.subscribe(ModbusTestDatapointKey.MAJOR_VERSION, callback)
|
|
76
|
+
assert await event.wait()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import concurrent.futures
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
# import json
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Any
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from credentials import Credentials
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from src.modbus_event_connect.micro_nabto.micro_nabto_connection import Request
|
|
12
|
+
from src.modbus_event_connect import MODBUS_VALUE_TYPES
|
|
13
|
+
from models.modbus_tcp_test_models import ModbusTestDatapointKey, ModbusTestTCP
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
_LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class TestData:
|
|
20
|
+
client: ModbusTestTCP
|
|
21
|
+
data = dict[str, Any]()
|
|
22
|
+
credentials: Credentials
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def testdata():
|
|
26
|
+
#setup
|
|
27
|
+
credentials = Credentials(["username", "hostname"])
|
|
28
|
+
client = ModbusTestTCP()
|
|
29
|
+
#
|
|
30
|
+
yield TestData(client=client, credentials=credentials)
|
|
31
|
+
#teardown
|
|
32
|
+
client.stop()
|
|
33
|
+
|
|
34
|
+
async def test_connection_request():
|
|
35
|
+
request = Request(1)
|
|
36
|
+
responsetask = request.wait_for_response()
|
|
37
|
+
request.notify_waiters()
|
|
38
|
+
await responsetask
|
|
39
|
+
|
|
40
|
+
async def test_connection_request_thread():
|
|
41
|
+
request = Request( 1)
|
|
42
|
+
def thread_method():
|
|
43
|
+
asyncio.run(asyncio.sleep(1))
|
|
44
|
+
request.notify_waiters()
|
|
45
|
+
_listen_thread = threading.Thread(target=thread_method)
|
|
46
|
+
_listen_thread.start()
|
|
47
|
+
await request.wait_for_response()
|
|
48
|
+
|
|
49
|
+
async def test_connection_request_asyncio_loop():
|
|
50
|
+
request = Request(1)
|
|
51
|
+
def thread_method():
|
|
52
|
+
asyncio.run(asyncio.sleep(1))
|
|
53
|
+
request.notify_waiters()
|
|
54
|
+
_loop = asyncio.get_event_loop()
|
|
55
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
56
|
+
_loop.run_in_executor(pool, thread_method)
|
|
57
|
+
await request.wait_for_response()
|
|
58
|
+
|
|
59
|
+
async def test_connect(testdata: TestData):
|
|
60
|
+
credentials = testdata.credentials
|
|
61
|
+
client = testdata.client
|
|
62
|
+
success = await client.connect("DEVICE_ID", credentials.hostname)
|
|
63
|
+
assert success
|
|
64
|
+
|
|
65
|
+
async def test_datapoint(testdata: TestData):
|
|
66
|
+
credentials = testdata.credentials
|
|
67
|
+
client = testdata.client
|
|
68
|
+
success = await client.connect("DEVICE_ID", credentials.hostname)
|
|
69
|
+
assert success
|
|
70
|
+
event = asyncio.Event()
|
|
71
|
+
def callback(oldval:MODBUS_VALUE_TYPES|None, newval:MODBUS_VALUE_TYPES|None):
|
|
72
|
+
_LOGGER.debug(f"{ModbusTestDatapointKey.MAJOR_VERSION}: {oldval if oldval is not None else 'None'} -> {newval if newval is not None else 'None'}")
|
|
73
|
+
event.set()
|
|
74
|
+
client.subscribe(ModbusTestDatapointKey.MAJOR_VERSION, callback)
|
|
75
|
+
await client.request_datapoint_data()
|
|
76
|
+
assert await event.wait()
|