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.
@@ -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,6 @@
1
+ #!/usr/bin/env python
2
+
3
+ import setuptools
4
+
5
+ if __name__ == "__main__":
6
+ setuptools.setup() # type: ignore
@@ -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,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()
@@ -0,0 +1,7 @@
1
+ import logging
2
+ _LOGGER = logging.getLogger(__name__)
3
+
4
+ def test_test():
5
+ print("hello print")
6
+ _LOGGER.debug("hello _LOGGER")
7
+ assert True