gravity-controller-operator 2.0.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.
Files changed (27) hide show
  1. gravity_controller_operator-2.0.0/LICENSE +19 -0
  2. gravity_controller_operator-2.0.0/PKG-INFO +115 -0
  3. gravity_controller_operator-2.0.0/README.md +101 -0
  4. gravity_controller_operator-2.0.0/gravity_controller_operator/__init__.py +0 -0
  5. gravity_controller_operator-2.0.0/gravity_controller_operator/controller_factory.py +26 -0
  6. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/__init__.py +0 -0
  7. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/arm_k210.py +47 -0
  8. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/emulator_contr.py +37 -0
  9. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/moxa.py +80 -0
  10. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/netping_relay.py +76 -0
  11. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/sigur.py +75 -0
  12. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/tests/__init__.py +0 -0
  13. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/tests/test_moxa_controller.py +31 -0
  14. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers/wb_mr6lv.py +63 -0
  15. gravity_controller_operator-2.0.0/gravity_controller_operator/controllers_super.py +105 -0
  16. gravity_controller_operator-2.0.0/gravity_controller_operator/docs/__init__.py +0 -0
  17. gravity_controller_operator-2.0.0/gravity_controller_operator/exceptions.py +8 -0
  18. gravity_controller_operator-2.0.0/gravity_controller_operator/main.py +47 -0
  19. gravity_controller_operator-2.0.0/gravity_controller_operator/tests/__init__.py +0 -0
  20. gravity_controller_operator-2.0.0/gravity_controller_operator/tests/main_tests.py +19 -0
  21. gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/PKG-INFO +115 -0
  22. gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/SOURCES.txt +25 -0
  23. gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/dependency_links.txt +1 -0
  24. gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/requires.txt +2 -0
  25. gravity_controller_operator-2.0.0/gravity_controller_operator.egg-info/top_level.txt +1 -0
  26. gravity_controller_operator-2.0.0/setup.cfg +4 -0
  27. gravity_controller_operator-2.0.0/setup.py +21 -0
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.1
2
+ Name: gravity_controller_operator
3
+ Version: 2.0.0
4
+ Summary: SDK предоставляющий универсальный интерфейс для работы со многими промышленными контроллерами ввода-вывода (количество расширяемо)
5
+ Author: PunchyArchy
6
+ Author-email: ksmdrmvscthny@gmail.com
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pyModbusTCP
13
+ Requires-Dist: pymodbus==3.6.9
14
+
15
+ # Gravity Controller Operator
16
+
17
+ Модуль для унифицированной работы с промышленными контроллерами (реле, дискретные входы и т.д.). Позволяет взаимодействовать с различными устройствами через единый интерфейс.
18
+
19
+ ---
20
+
21
+ ## 🚀 Поддерживаемые контроллеры
22
+
23
+ - **EmulatorController** — заглушка для тестов
24
+ - **ARMK210Controller** — Modbus TCP
25
+ - **WBMR6LV** — Modbus RTU
26
+ - **NetPing2Controller** — HTTP API
27
+ - **Sigur** — доступ через TCP (СКУД)
28
+ - **MoxaE1214** — REST API (vdn.dac.v1)
29
+
30
+ ---
31
+
32
+ ## 🧩 Архитектура
33
+
34
+ ### 🧠 Интерфейсы:
35
+
36
+ - `DIInterface`: чтение дискретных входов
37
+ - `RelayInterface`: чтение/управление релейными выходами
38
+ - `ControllerInterface`: объединяет оба
39
+
40
+ Все интерфейсы наследуют `SoftStateMixin`, в котором реализовано хранение состояния точек (`state`, `changed`, `addr`).
41
+
42
+ ### 🛠 Контроллер
43
+ Каждый контроллер реализует:
44
+ ```python
45
+ self.interface = ControllerInterface(di_interface=..., relay_interface=...)
46
+ ```
47
+
48
+ ### 🧪 Обёртки:
49
+
50
+ - `ControllerOperator` — отвечает за автоматическое обновление, mutex-безопасность и удобный API
51
+ - `ControllerCreator` — фабрика создания контроллеров по имени модели (см. `controller_factory.py`)
52
+
53
+ ---
54
+
55
+ ## 🧪 Как использовать
56
+
57
+ ```python
58
+ from gravity_controller_operator.controllers.moxa import MoxaE1214
59
+ from gravity_controller_operator.controller_operator import ControllerOperator
60
+
61
+ moxa = MoxaE1214("192.168.60.103")
62
+ operator = ControllerOperator(moxa)
63
+
64
+ print(operator.get_points())
65
+ operator.change_relay_state(0, 1) # включить реле 0
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 🔬 Тестирование
71
+
72
+ Установка:
73
+ ```bash
74
+ pip install pytest
75
+ ```
76
+
77
+ Запуск:
78
+ ```bash
79
+ pytest gravity_controller_operator/tests/
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 📦 Добавление нового контроллера
85
+
86
+ 1. Реализуй `YourControllerDI` и `YourControllerRelay`, наследуя `DIInterface`, `RelayInterface`
87
+ 2. Создай `YourController`, в котором:
88
+ ```python
89
+ self.interface = ControllerInterface(di_interface=..., relay_interface=...)
90
+ ```
91
+ 3. Добавь контроллер в `AVAILABLE_CONTROLLERS` в `controller_factory.py`
92
+
93
+ ---
94
+
95
+ ## 📁 Структура
96
+
97
+ ```
98
+ controllers_super.py # Базовые интерфейсы
99
+ controllers/ # Реализации контроллеров
100
+ controller_operator.py # Обёртка ControllerOperator ✅
101
+ controller_factory.py # AVAILABLE_CONTROLLERS + фабрика ✅
102
+ exceptions.py # Кастомные исключения ✅
103
+ tests/ # Тесты
104
+ README.md
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 🤝 Контакты
110
+
111
+ - 📧 Email: ksmdrmvstchny@gmail.com
112
+ - 💬 Telegram: [@PunchyArchy](https://t.me/PunchyArchy)
113
+ - 📞 Phone: +7 996 294-75-95
114
+
115
+ Проект поддерживается Артуром (PunchyArchy, Qodex)
@@ -0,0 +1,101 @@
1
+ # Gravity Controller Operator
2
+
3
+ Модуль для унифицированной работы с промышленными контроллерами (реле, дискретные входы и т.д.). Позволяет взаимодействовать с различными устройствами через единый интерфейс.
4
+
5
+ ---
6
+
7
+ ## 🚀 Поддерживаемые контроллеры
8
+
9
+ - **EmulatorController** — заглушка для тестов
10
+ - **ARMK210Controller** — Modbus TCP
11
+ - **WBMR6LV** — Modbus RTU
12
+ - **NetPing2Controller** — HTTP API
13
+ - **Sigur** — доступ через TCP (СКУД)
14
+ - **MoxaE1214** — REST API (vdn.dac.v1)
15
+
16
+ ---
17
+
18
+ ## 🧩 Архитектура
19
+
20
+ ### 🧠 Интерфейсы:
21
+
22
+ - `DIInterface`: чтение дискретных входов
23
+ - `RelayInterface`: чтение/управление релейными выходами
24
+ - `ControllerInterface`: объединяет оба
25
+
26
+ Все интерфейсы наследуют `SoftStateMixin`, в котором реализовано хранение состояния точек (`state`, `changed`, `addr`).
27
+
28
+ ### 🛠 Контроллер
29
+ Каждый контроллер реализует:
30
+ ```python
31
+ self.interface = ControllerInterface(di_interface=..., relay_interface=...)
32
+ ```
33
+
34
+ ### 🧪 Обёртки:
35
+
36
+ - `ControllerOperator` — отвечает за автоматическое обновление, mutex-безопасность и удобный API
37
+ - `ControllerCreator` — фабрика создания контроллеров по имени модели (см. `controller_factory.py`)
38
+
39
+ ---
40
+
41
+ ## 🧪 Как использовать
42
+
43
+ ```python
44
+ from gravity_controller_operator.controllers.moxa import MoxaE1214
45
+ from gravity_controller_operator.controller_operator import ControllerOperator
46
+
47
+ moxa = MoxaE1214("192.168.60.103")
48
+ operator = ControllerOperator(moxa)
49
+
50
+ print(operator.get_points())
51
+ operator.change_relay_state(0, 1) # включить реле 0
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 🔬 Тестирование
57
+
58
+ Установка:
59
+ ```bash
60
+ pip install pytest
61
+ ```
62
+
63
+ Запуск:
64
+ ```bash
65
+ pytest gravity_controller_operator/tests/
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 📦 Добавление нового контроллера
71
+
72
+ 1. Реализуй `YourControllerDI` и `YourControllerRelay`, наследуя `DIInterface`, `RelayInterface`
73
+ 2. Создай `YourController`, в котором:
74
+ ```python
75
+ self.interface = ControllerInterface(di_interface=..., relay_interface=...)
76
+ ```
77
+ 3. Добавь контроллер в `AVAILABLE_CONTROLLERS` в `controller_factory.py`
78
+
79
+ ---
80
+
81
+ ## 📁 Структура
82
+
83
+ ```
84
+ controllers_super.py # Базовые интерфейсы
85
+ controllers/ # Реализации контроллеров
86
+ controller_operator.py # Обёртка ControllerOperator ✅
87
+ controller_factory.py # AVAILABLE_CONTROLLERS + фабрика ✅
88
+ exceptions.py # Кастомные исключения ✅
89
+ tests/ # Тесты
90
+ README.md
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 🤝 Контакты
96
+
97
+ - 📧 Email: ksmdrmvstchny@gmail.com
98
+ - 💬 Telegram: [@PunchyArchy](https://t.me/PunchyArchy)
99
+ - 📞 Phone: +7 996 294-75-95
100
+
101
+ Проект поддерживается Артуром (PunchyArchy, Qodex)
@@ -0,0 +1,26 @@
1
+ from gravity_controller_operator.controllers.emulator_contr import EmulatorController
2
+ from gravity_controller_operator.controllers.netping_relay import NetPing2Controller
3
+ from gravity_controller_operator.controllers.arm_k210 import ARMK210Controller
4
+ from gravity_controller_operator.controllers.wb_mr6lv import WBMR6LV
5
+ from gravity_controller_operator.exceptions import UnknownController
6
+ from gravity_controller_operator.controllers.sigur import Sigur
7
+
8
+
9
+ AVAILABLE_CONTROLLERS = [
10
+ ARMK210Controller,
11
+ WBMR6LV,
12
+ NetPing2Controller,
13
+ EmulatorController,
14
+ Sigur
15
+ ]
16
+
17
+
18
+ class ControllerCreator:
19
+ @staticmethod
20
+ def get_controller(model, emulator=False, *args, **kwargs):
21
+ for contr in AVAILABLE_CONTROLLERS:
22
+ if emulator:
23
+ return EmulatorController(*args, **kwargs)
24
+ if contr.model.lower() == model.lower():
25
+ return contr(*args, **kwargs)
26
+ raise UnknownController(model, [contr.model for contr in AVAILABLE_CONTROLLERS])
@@ -0,0 +1,47 @@
1
+ from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
2
+ from pyModbusTCP.client import ModbusClient
3
+
4
+
5
+ class ARMK210ControllerDI(DIInterface):
6
+ map_keys_amount = 8
7
+ starts_with = 0
8
+
9
+ def __init__(self, client):
10
+ self.client = client
11
+ super().__init__()
12
+
13
+ def get_phys_dict(self):
14
+ response = self.client.read_input_registers(self.starts_with, self.map_keys_amount)
15
+ while not response:
16
+ response = self.client.read_input_registers(self.starts_with, self.map_keys_amount)
17
+ return {i: val for i, val in enumerate(response)}
18
+
19
+
20
+ class ARMK210ControllerRelay(RelayInterface):
21
+ map_keys_amount = 8
22
+ starts_with = 0
23
+
24
+ def __init__(self, client):
25
+ self.client = client
26
+ super().__init__()
27
+
28
+ def get_phys_dict(self):
29
+ response = self.client.read_holding_registers(self.starts_with, self.map_keys_amount)
30
+ while not response:
31
+ response = self.client.read_holding_registers(self.starts_with, self.map_keys_amount)
32
+ return {i: val for i, val in enumerate(response)}
33
+
34
+ def change_phys_relay_state(self, addr, state: bool):
35
+ result = self.client.write_single_coil(addr, state)
36
+ while not result:
37
+ result = self.client.write_single_coil(addr, state)
38
+
39
+
40
+ class ARMK210Controller:
41
+ model = "arm_k210"
42
+
43
+ def __init__(self, ip: str, port: int = 8234, name="ARM_K210_Controller", *args, **kwargs):
44
+ client = ModbusClient(host=ip, port=port)
45
+ di = ARMK210ControllerDI(client)
46
+ relay = ARMK210ControllerRelay(client)
47
+ self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
@@ -0,0 +1,37 @@
1
+ from gravity_controller_operator.controllers_super import DIInterface, \
2
+ RelayInterface, ControllerInterface
3
+
4
+
5
+ class EmulatorDI(DIInterface):
6
+ map_keys_amount = 4
7
+ starts_with = 1
8
+
9
+ def __init__(self):
10
+ super().__init__()
11
+
12
+ def get_phys_dict(self):
13
+ return {1: 0, 2: 0, 3: 0, 4: 0}
14
+
15
+
16
+ class EmulatorRelay(RelayInterface):
17
+ map_keys_amount = 4
18
+ starts_with = 1
19
+
20
+ def __init__(self):
21
+ super().__init__()
22
+
23
+ def get_phys_dict(self):
24
+ return {1: 0, 2: 0, 3: 0, 4: 0}
25
+
26
+ def change_phys_relay_state(self, addr, state: bool):
27
+ # Ничего не делает, просто имитация
28
+ pass
29
+
30
+
31
+ class EmulatorController:
32
+ model = "emulator_controller"
33
+
34
+ def __init__(self):
35
+ di = EmulatorDI()
36
+ relay = EmulatorRelay()
37
+ self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
@@ -0,0 +1,80 @@
1
+ import requests
2
+ from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
3
+
4
+
5
+ class MoxaClient:
6
+ def __init__(self, ip: str):
7
+ self.base_url = f"http://{ip}/api/slot/0/io"
8
+ self.headers = {
9
+ "Content-Type": "application/json",
10
+ "Accept": "vdn.dac.v1"
11
+ }
12
+
13
+ def get_di(self):
14
+ url = f"{self.base_url}/di"
15
+ r = requests.get(url, headers=self.headers, timeout=2)
16
+ r.raise_for_status()
17
+ return r.json()["io"]["di"]
18
+
19
+ def get_relays(self):
20
+ url = f"{self.base_url}/relay"
21
+ r = requests.get(url, headers=self.headers, timeout=2)
22
+ r.raise_for_status()
23
+ return r.json()["io"]["relay"]
24
+
25
+ def set_relay(self, channel: int, state: int):
26
+ url = f"{self.base_url}/relay/{channel}/relayStatus"
27
+ payload = {
28
+ "slot": "0",
29
+ "io": {
30
+ "relay": {
31
+ str(channel): {
32
+ "relayStatus": str(state)
33
+ }
34
+ }
35
+ }
36
+ }
37
+ headers = self.headers.copy()
38
+ headers["Content-Length"] = str(len(str(payload)))
39
+ r = requests.put(url, headers=headers, json=payload, timeout=2)
40
+ r.raise_for_status()
41
+ return r.status_code == 200
42
+
43
+
44
+ class MoxaDI(DIInterface):
45
+ map_keys_amount = 16
46
+ starts_with = 0
47
+
48
+ def __init__(self, client):
49
+ self.client = client
50
+ super().__init__()
51
+
52
+ def get_phys_dict(self):
53
+ data = self.client.get_di()
54
+ return {int(item["diIndex"]): item["diStatus"] for item in data}
55
+
56
+
57
+ class MoxaRelay(RelayInterface):
58
+ map_keys_amount = 4
59
+ starts_with = 0
60
+
61
+ def __init__(self, client):
62
+ self.client = client
63
+ super().__init__()
64
+
65
+ def get_phys_dict(self):
66
+ data = self.client.get_relays()
67
+ return {int(item["relayIndex"]): int(item["relayStatus"]) for item in data}
68
+
69
+ def change_phys_relay_state(self, addr, state: bool):
70
+ return self.client.set_relay(addr, int(state))
71
+
72
+
73
+ class MoxaE1214:
74
+ model = "moxa_e1214"
75
+
76
+ def __init__(self, ip):
77
+ client = MoxaClient(ip)
78
+ di = MoxaDI(client)
79
+ relay = MoxaRelay(client)
80
+ self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
@@ -0,0 +1,76 @@
1
+ from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
2
+ from netping_contr import mixins
3
+ import requests
4
+ from requests.auth import HTTPBasicAuth
5
+
6
+
7
+ class NetPingDevice(mixins.NetPingResponseParser):
8
+ def __init__(self, ip, port=80, username="visor", password="ping"):
9
+ self.ip = ip
10
+ self.port = port
11
+ self.username = username
12
+ self.password = password
13
+ self.schema = "http://"
14
+
15
+ def get_full_url(self):
16
+ return f"{self.schema}{self.ip}:{self.port}"
17
+
18
+ def get_all_di_status(self):
19
+ return requests.get(
20
+ url=f"{self.get_full_url()}/io.cgi?io",
21
+ auth=HTTPBasicAuth(self.username, self.password))
22
+
23
+ def get_all_relay_states(self):
24
+ states = {}
25
+ for i in range(1, 5):
26
+ r = requests.get(
27
+ url=f"{self.get_full_url()}/relay.cgi?r{i}",
28
+ auth=HTTPBasicAuth(self.username, self.password))
29
+ states[i] = self.parse_relay_state(r)
30
+ return states
31
+
32
+ def change_relay_status(self, relay_num, state):
33
+ return requests.get(
34
+ url=f"{self.get_full_url()}/relay.cgi?r{relay_num}={state}",
35
+ auth=HTTPBasicAuth(self.username, self.password))
36
+
37
+
38
+ class NetPingDI(DIInterface):
39
+ map_keys_amount = 4
40
+ starts_with = 1
41
+
42
+ def __init__(self, controller):
43
+ self.controller = controller
44
+ super().__init__()
45
+
46
+ def get_phys_dict(self):
47
+ raw = self.controller.get_all_di_status()
48
+ return self.controller.parse_all_lines_request(raw)
49
+
50
+
51
+ class NetPingRelay(RelayInterface):
52
+ map_keys_amount = 4
53
+ starts_with = 1
54
+
55
+ def __init__(self, controller):
56
+ self.controller = controller
57
+ super().__init__()
58
+
59
+ def get_phys_dict(self):
60
+ return self.controller.get_all_relay_states()
61
+
62
+ def change_phys_relay_state(self, addr, state: bool):
63
+ result = self.controller.change_relay_status(addr, state)
64
+ while "error" in result:
65
+ result = self.controller.change_relay_status(addr, state)
66
+
67
+
68
+ class NetPing2Controller:
69
+ model = "netping_relay"
70
+
71
+ def __init__(self, ip, port=80, username="visor", password="ping",
72
+ name="netping_relay2", *args, **kwargs):
73
+ device = NetPingDevice(ip=ip, port=port, username=username, password=password)
74
+ di = NetPingDI(device)
75
+ relay = NetPingRelay(device)
76
+ self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
@@ -0,0 +1,75 @@
1
+ from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
2
+ from _thread import allocate_lock
3
+ from socket import socket
4
+
5
+
6
+ class SigurBase:
7
+ def __init__(self, sock):
8
+ self.sock = sock
9
+ self.mutex = allocate_lock()
10
+ self.login()
11
+
12
+ def login(self, login="Administrator", password=""):
13
+ return self._send_command(f'"LOGIN" 1.8 "{login}" "{password}"')
14
+
15
+ def _send_command(self, command, get_response=True):
16
+ self.mutex.acquire()
17
+ command = f'{command}\r\n'
18
+ self.sock.send(command.encode("utf-8"))
19
+ if get_response:
20
+ response = self.sock.recv(1024)
21
+ self.mutex.release()
22
+ return response
23
+ self.mutex.release()
24
+
25
+
26
+ class SigurDI(DIInterface, SigurBase):
27
+ map_keys_amount = 5
28
+ starts_with = 3
29
+
30
+ def __init__(self, sock):
31
+ SigurBase.__init__(self, sock)
32
+ DIInterface.__init__(self)
33
+
34
+ def get_phys_dict(self):
35
+ result = {}
36
+ for point in range(self.starts_with, self.starts_with + self.map_keys_amount):
37
+ raw = self._send_command(f'"GETAPINFO" {point}')
38
+ response = raw.decode()
39
+ status = response.split(" ")[-2]
40
+ result[point] = 0 if status in ("ONLINE_UNLOCKED", "ONLINE_NORMAL") else 1
41
+ return result
42
+
43
+
44
+ class SigurRelay(RelayInterface, SigurBase):
45
+ map_keys_amount = 3
46
+ starts_with = 1
47
+
48
+ def __init__(self, sock):
49
+ SigurBase.__init__(self, sock)
50
+ RelayInterface.__init__(self)
51
+
52
+ def get_phys_dict(self):
53
+ result = {}
54
+ for point in range(self.starts_with, self.starts_with + self.map_keys_amount):
55
+ raw = self._send_command(f'"GETAPINFO" {point}')
56
+ response = raw.decode()
57
+ status = response.split(" ")[-2]
58
+ result[point] = 0 if status in ("ONLINE_UNLOCKED", "ONLINE_NORMAL") else 1
59
+ return result
60
+
61
+ def change_phys_relay_state(self, addr, state: bool):
62
+ # Имитация действия, при необходимости можно добавить настоящую команду
63
+ pass
64
+
65
+
66
+ class Sigur:
67
+ model = "sigur"
68
+
69
+ def __init__(self, ip, port=3312, login="Administrator", password="",
70
+ name="Sigur", *args, **kwargs):
71
+ sock = socket()
72
+ sock.connect((ip, port))
73
+ di = SigurDI(sock)
74
+ relay = SigurRelay(sock)
75
+ self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
@@ -0,0 +1,31 @@
1
+ import pytest
2
+ from gravity_controller_operator.main import ControllerOperator
3
+ from gravity_controller_operator.controllers.moxa import MoxaE1214
4
+
5
+
6
+ @pytest.fixture(scope="module")
7
+ def moxa_operator():
8
+ controller = MoxaE1214("192.168.60.103")
9
+ return ControllerOperator(controller, auto_update_points=False)
10
+
11
+
12
+ def test_di_states(moxa_operator):
13
+ di = moxa_operator.get_points()["di"]
14
+ assert isinstance(di, dict)
15
+ assert all(isinstance(v["state"], (int, type(None))) for v in di.values())
16
+
17
+
18
+ def test_relay_states(moxa_operator):
19
+ relays = moxa_operator.get_points()["relays"]
20
+ assert isinstance(relays, dict)
21
+ assert all(isinstance(v["state"], (int, type(None))) for v in relays.values())
22
+
23
+
24
+ def test_relay_toggle(moxa_operator):
25
+ moxa_operator.change_relay_state(0, 0)
26
+ moxa_operator.update_points()
27
+ assert moxa_operator.get_points()["relays"][0]["state"] == 0
28
+
29
+ moxa_operator.change_relay_state(0, 1)
30
+ moxa_operator.update_points()
31
+ assert moxa_operator.get_points()["relays"][0]["state"] == 1
@@ -0,0 +1,63 @@
1
+ from gravity_controller_operator.controllers_super import DIInterface, RelayInterface, ControllerInterface
2
+ from pymodbus.client import ModbusSerialClient
3
+ from pymodbus import Framer
4
+
5
+
6
+ class WBMR6LVDI(DIInterface):
7
+ map_keys_amount = 8
8
+ starts_with = 0
9
+ spec_addr = {0: 7, 1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5}
10
+
11
+ def __init__(self, client, slave_id):
12
+ self.client = client
13
+ self.slave_id = slave_id
14
+ super().__init__()
15
+
16
+ def get_phys_dict(self):
17
+ response = self.client.read_discrete_inputs(
18
+ self.starts_with, self.map_keys_amount, slave=self.slave_id)
19
+ while not response or response.isError():
20
+ response = self.client.read_discrete_inputs(
21
+ self.starts_with, self.map_keys_amount, slave=self.slave_id)
22
+ return {i: bit for i, bit in enumerate(response.bits)}
23
+
24
+
25
+ class WBMR6LVRelay(RelayInterface):
26
+ map_keys_amount = 6
27
+ starts_with = 0
28
+ spec_addr = {1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5}
29
+
30
+ def __init__(self, client, slave_id):
31
+ self.client = client
32
+ self.slave_id = slave_id
33
+ super().__init__()
34
+
35
+ def get_phys_dict(self):
36
+ response = self.client.read_coils(
37
+ self.starts_with, self.map_keys_amount, slave=self.slave_id)
38
+ while not response or response.isError():
39
+ response = self.client.read_coils(
40
+ self.starts_with, self.map_keys_amount, slave=self.slave_id)
41
+ return {i: bit for i, bit in enumerate(response.bits)}
42
+
43
+ def change_phys_relay_state(self, addr, state: bool):
44
+ result = self.client.write_coil(addr, state, slave=self.slave_id)
45
+ while not result or result.isError():
46
+ result = self.client.write_coil(addr, state, slave=self.slave_id)
47
+
48
+
49
+ class WBMR6LV:
50
+ model = "wb_mr6lv"
51
+
52
+ def __init__(self, device, slave_id, baudrate=9600, stopbits=2, bytesize=8,
53
+ name="WBMR6LV", *args, **kwargs):
54
+ client = ModbusSerialClient(
55
+ device,
56
+ framer=Framer.RTU,
57
+ baudrate=baudrate,
58
+ stopbits=stopbits,
59
+ bytesize=bytesize,
60
+ )
61
+ di = WBMR6LVDI(client, slave_id)
62
+ relay = WBMR6LVRelay(client, slave_id)
63
+ self.interface = ControllerInterface(di_interface=di, relay_interface=relay)
@@ -0,0 +1,105 @@
1
+ from abc import ABC, abstractmethod
2
+ import datetime
3
+
4
+
5
+ class SoftStateMixin:
6
+ """
7
+ Хранит программное состояние входов/выходов.
8
+ Позволяет обновлять состояния и получать их по ключу.
9
+ """
10
+ spec_addr = {} # переназначение адресов (если физический != логическому)
11
+ map_keys_amount = 0
12
+ starts_with = 0
13
+
14
+ def __init__(self):
15
+ self.state = self._init_state()
16
+
17
+ def _init_state(self):
18
+ points = {}
19
+ for i in range(self.map_keys_amount):
20
+ logical_ch = self.starts_with + i
21
+ addr = self.spec_addr.get(logical_ch, logical_ch)
22
+ points[logical_ch] = {"state": None, "changed": None, "addr": addr}
23
+ return points
24
+
25
+ def update_state(self, addr, value, mark_time=True):
26
+ now = datetime.datetime.now() if mark_time else None
27
+ for logical_ch, info in self.state.items():
28
+ if info["addr"] == addr:
29
+ info["state"] = value
30
+ info["changed"] = now
31
+
32
+ def get_state(self):
33
+ return self.state
34
+
35
+ def get_point(self, num):
36
+ return self.state.get(num, {"error": f"channel {num} not found"})
37
+
38
+
39
+ class BasePhysInterface(ABC):
40
+ """
41
+ Абстрактный интерфейс работы с железом.
42
+ Потомки реализуют конкретную работу с DI или Relay.
43
+ """
44
+ @abstractmethod
45
+ def get_phys_dict(self) -> dict:
46
+ """
47
+ Вернуть словарь {канал: значение} с физического устройства.
48
+ """
49
+ pass
50
+
51
+
52
+ class RelayPhysInterface(BasePhysInterface):
53
+ @abstractmethod
54
+ def change_phys_relay_state(self, addr: int, state: bool):
55
+ """
56
+ Изменить состояние реле на физическом устройстве.
57
+ """
58
+ pass
59
+
60
+ class DIInterface(SoftStateMixin, BasePhysInterface):
61
+ def __init__(self):
62
+ super().__init__()
63
+ self.update_from_device()
64
+
65
+ def update_from_device(self):
66
+ values = self.get_phys_dict()
67
+ for addr, value in values.items():
68
+ self.update_state(addr, value, mark_time=False)
69
+
70
+
71
+ class RelayInterface(SoftStateMixin, RelayPhysInterface):
72
+ def __init__(self):
73
+ super().__init__()
74
+ self.update_from_device()
75
+
76
+ def update_from_device(self):
77
+ values = self.get_phys_dict()
78
+ for addr, value in values.items():
79
+ self.update_state(addr, value, mark_time=False)
80
+
81
+ def change_relay_state(self, logical_ch: int, state: bool):
82
+ self.update_state(logical_ch, state)
83
+ addr = self.state[logical_ch]["addr"]
84
+ return self.change_phys_relay_state(addr, state)
85
+
86
+
87
+ class ControllerInterface:
88
+ """
89
+ Комбинирует доступ к DI и Relay интерфейсам.
90
+ """
91
+ def __init__(self, di_interface=None, relay_interface=None):
92
+ self.di_interface = di_interface
93
+ self.relay_interface = relay_interface
94
+
95
+ def update_all(self):
96
+ if self.di_interface:
97
+ self.di_interface.update_from_device()
98
+ if self.relay_interface:
99
+ self.relay_interface.update_from_device()
100
+
101
+ def get_all_states(self):
102
+ return {
103
+ "di": self.di_interface.get_state() if self.di_interface else {},
104
+ "relays": self.relay_interface.get_state() if self.relay_interface else {}
105
+ }
@@ -0,0 +1,8 @@
1
+ class UnknownController(Exception):
2
+ # Исключение, возникающее при неизвестном имени терминала
3
+ def __init__(self, contr_name=None, contr_list=[]):
4
+ text = f'Контроллер {contr_name} не обнаружен! Создайте класс с контроллером ' \
5
+ 'в директории controllers, укажите его модель через атрибут ' \
6
+ 'model, затем добавьте этот класс в список ' \
7
+ f'AVAILABLE_CONTROLLERS {tuple(contr_list)}'
8
+ super().__init__(text)
@@ -0,0 +1,47 @@
1
+ import threading
2
+ import time
3
+ from threading import Lock
4
+
5
+
6
+ class ControllerOperator:
7
+ def __init__(self, controller, auto_update_points=True, update_cooldown=0.25):
8
+ self.controller = controller
9
+ self.interface = controller.interface
10
+ self.mutex = Lock()
11
+ self.update_cooldown = update_cooldown
12
+ self.auto_update_points_enabled = auto_update_points
13
+
14
+ if auto_update_points:
15
+ threading.Thread(target=self._auto_update_loop, daemon=True).start()
16
+
17
+ def _auto_update_loop(self):
18
+ while self.auto_update_points_enabled:
19
+ self.update_points()
20
+ time.sleep(self.update_cooldown)
21
+
22
+ def update_points(self):
23
+ with self.mutex:
24
+ self.interface.update_all()
25
+
26
+ def get_points(self):
27
+ with self.mutex:
28
+ return self.interface.get_all_states()
29
+
30
+ def change_relay_state(self, ch: int, value: int):
31
+ with self.mutex:
32
+ return self.interface.relay_interface.change_relay_state(ch, value)
33
+
34
+ def get_point(self, typ, ch):
35
+ if typ == "di":
36
+ return self.interface.di_interface.get_point(ch)
37
+ elif typ == "relays":
38
+ return self.interface.relay_interface.get_point(ch)
39
+
40
+ def get_di_state(self, ch):
41
+ return self.get_point("di", ch)
42
+
43
+ def get_relay_state(self, ch):
44
+ return self.get_point("relays", ch)
45
+
46
+ def get_model(self):
47
+ return self.controller.model
@@ -0,0 +1,19 @@
1
+ import unittest
2
+ from gravity_controller_operator.main import ControllerOperator, \
3
+ ControllerCreater
4
+
5
+
6
+ class TestCase(unittest.TestCase):
7
+ def test_operator(self):
8
+ inst = ControllerCreater.get_controller(
9
+ "wb_mr6lv12",False)
10
+ co = ControllerOperator(inst)
11
+ print(co.get_points())
12
+ # inst = ControllerOperator("localhost", 8234, "arm_k210",
13
+ # auto_update_points=True)
14
+ # while True:
15
+ # print(inst.get_points())
16
+
17
+
18
+ if __name__ == "__main__":
19
+ unittest.main()
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.1
2
+ Name: gravity_controller_operator
3
+ Version: 2.0.0
4
+ Summary: SDK предоставляющий универсальный интерфейс для работы со многими промышленными контроллерами ввода-вывода (количество расширяемо)
5
+ Author: PunchyArchy
6
+ Author-email: ksmdrmvscthny@gmail.com
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pyModbusTCP
13
+ Requires-Dist: pymodbus==3.6.9
14
+
15
+ # Gravity Controller Operator
16
+
17
+ Модуль для унифицированной работы с промышленными контроллерами (реле, дискретные входы и т.д.). Позволяет взаимодействовать с различными устройствами через единый интерфейс.
18
+
19
+ ---
20
+
21
+ ## 🚀 Поддерживаемые контроллеры
22
+
23
+ - **EmulatorController** — заглушка для тестов
24
+ - **ARMK210Controller** — Modbus TCP
25
+ - **WBMR6LV** — Modbus RTU
26
+ - **NetPing2Controller** — HTTP API
27
+ - **Sigur** — доступ через TCP (СКУД)
28
+ - **MoxaE1214** — REST API (vdn.dac.v1)
29
+
30
+ ---
31
+
32
+ ## 🧩 Архитектура
33
+
34
+ ### 🧠 Интерфейсы:
35
+
36
+ - `DIInterface`: чтение дискретных входов
37
+ - `RelayInterface`: чтение/управление релейными выходами
38
+ - `ControllerInterface`: объединяет оба
39
+
40
+ Все интерфейсы наследуют `SoftStateMixin`, в котором реализовано хранение состояния точек (`state`, `changed`, `addr`).
41
+
42
+ ### 🛠 Контроллер
43
+ Каждый контроллер реализует:
44
+ ```python
45
+ self.interface = ControllerInterface(di_interface=..., relay_interface=...)
46
+ ```
47
+
48
+ ### 🧪 Обёртки:
49
+
50
+ - `ControllerOperator` — отвечает за автоматическое обновление, mutex-безопасность и удобный API
51
+ - `ControllerCreator` — фабрика создания контроллеров по имени модели (см. `controller_factory.py`)
52
+
53
+ ---
54
+
55
+ ## 🧪 Как использовать
56
+
57
+ ```python
58
+ from gravity_controller_operator.controllers.moxa import MoxaE1214
59
+ from gravity_controller_operator.controller_operator import ControllerOperator
60
+
61
+ moxa = MoxaE1214("192.168.60.103")
62
+ operator = ControllerOperator(moxa)
63
+
64
+ print(operator.get_points())
65
+ operator.change_relay_state(0, 1) # включить реле 0
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 🔬 Тестирование
71
+
72
+ Установка:
73
+ ```bash
74
+ pip install pytest
75
+ ```
76
+
77
+ Запуск:
78
+ ```bash
79
+ pytest gravity_controller_operator/tests/
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 📦 Добавление нового контроллера
85
+
86
+ 1. Реализуй `YourControllerDI` и `YourControllerRelay`, наследуя `DIInterface`, `RelayInterface`
87
+ 2. Создай `YourController`, в котором:
88
+ ```python
89
+ self.interface = ControllerInterface(di_interface=..., relay_interface=...)
90
+ ```
91
+ 3. Добавь контроллер в `AVAILABLE_CONTROLLERS` в `controller_factory.py`
92
+
93
+ ---
94
+
95
+ ## 📁 Структура
96
+
97
+ ```
98
+ controllers_super.py # Базовые интерфейсы
99
+ controllers/ # Реализации контроллеров
100
+ controller_operator.py # Обёртка ControllerOperator ✅
101
+ controller_factory.py # AVAILABLE_CONTROLLERS + фабрика ✅
102
+ exceptions.py # Кастомные исключения ✅
103
+ tests/ # Тесты
104
+ README.md
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 🤝 Контакты
110
+
111
+ - 📧 Email: ksmdrmvstchny@gmail.com
112
+ - 💬 Telegram: [@PunchyArchy](https://t.me/PunchyArchy)
113
+ - 📞 Phone: +7 996 294-75-95
114
+
115
+ Проект поддерживается Артуром (PunchyArchy, Qodex)
@@ -0,0 +1,25 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ gravity_controller_operator/__init__.py
5
+ gravity_controller_operator/controller_factory.py
6
+ gravity_controller_operator/controllers_super.py
7
+ gravity_controller_operator/exceptions.py
8
+ gravity_controller_operator/main.py
9
+ gravity_controller_operator.egg-info/PKG-INFO
10
+ gravity_controller_operator.egg-info/SOURCES.txt
11
+ gravity_controller_operator.egg-info/dependency_links.txt
12
+ gravity_controller_operator.egg-info/requires.txt
13
+ gravity_controller_operator.egg-info/top_level.txt
14
+ gravity_controller_operator/controllers/__init__.py
15
+ gravity_controller_operator/controllers/arm_k210.py
16
+ gravity_controller_operator/controllers/emulator_contr.py
17
+ gravity_controller_operator/controllers/moxa.py
18
+ gravity_controller_operator/controllers/netping_relay.py
19
+ gravity_controller_operator/controllers/sigur.py
20
+ gravity_controller_operator/controllers/wb_mr6lv.py
21
+ gravity_controller_operator/controllers/tests/__init__.py
22
+ gravity_controller_operator/controllers/tests/test_moxa_controller.py
23
+ gravity_controller_operator/docs/__init__.py
24
+ gravity_controller_operator/tests/__init__.py
25
+ gravity_controller_operator/tests/main_tests.py
@@ -0,0 +1,2 @@
1
+ pyModbusTCP
2
+ pymodbus==3.6.9
@@ -0,0 +1 @@
1
+ gravity_controller_operator
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="gravity_controller_operator",
5
+ version="2.0.0",
6
+ description="SDK предоставляющий универсальный интерфейс для работы со многими промышленными контроллерами ввода-вывода (количество расширяемо)",
7
+ long_description=open("README.md", encoding="utf-8").read(),
8
+ long_description_content_type="text/markdown",
9
+ author="PunchyArchy",
10
+ author_email="ksmdrmvscthny@gmail.com",
11
+ packages=find_packages(),
12
+ python_requires=">=3.7",
13
+ install_requires=[
14
+ "pyModbusTCP",
15
+ "pymodbus==3.6.9",
16
+ ],
17
+ classifiers=[
18
+ "Programming Language :: Python :: 3",
19
+ "Operating System :: OS Independent",
20
+ ],
21
+ )