pygryfsmart 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.
- pygryfsmart-0.1.0/LICENSE +21 -0
- pygryfsmart-0.1.0/MANIFEST.in +2 -0
- pygryfsmart-0.1.0/PKG-INFO +17 -0
- pygryfsmart-0.1.0/README.md +2 -0
- pygryfsmart-0.1.0/pygryfsmart/__init__.py +1 -0
- pygryfsmart-0.1.0/pygryfsmart/const.py +38 -0
- pygryfsmart-0.1.0/pygryfsmart/device.py +164 -0
- pygryfsmart-0.1.0/pygryfsmart/feedback.py +130 -0
- pygryfsmart-0.1.0/pygryfsmart/rs232.py +55 -0
- pygryfsmart-0.1.0/pygryfsmart.egg-info/PKG-INFO +17 -0
- pygryfsmart-0.1.0/pygryfsmart.egg-info/SOURCES.txt +18 -0
- pygryfsmart-0.1.0/pygryfsmart.egg-info/dependency_links.txt +1 -0
- pygryfsmart-0.1.0/pygryfsmart.egg-info/requires.txt +2 -0
- pygryfsmart-0.1.0/pygryfsmart.egg-info/top_level.txt +2 -0
- pygryfsmart-0.1.0/pyproject.toml +3 -0
- pygryfsmart-0.1.0/setup.cfg +24 -0
- pygryfsmart-0.1.0/tests/__init__.py +0 -0
- pygryfsmart-0.1.0/tests/test_feedback.py +111 -0
- pygryfsmart-0.1.0/tests/test_rs232.py +47 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Gryf Smart
|
|
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
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pygryfsmart
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Library for the GryfSmart system
|
|
5
|
+
Home-page: https://github.com/karlowiczpl/pygryfsmart
|
|
6
|
+
Author: @karlowiczpl
|
|
7
|
+
Author-email: k.karlowski@outlook.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: pyserial
|
|
14
|
+
Requires-Dist: pyserial-asyncio
|
|
15
|
+
|
|
16
|
+
#Gryf smart system lib
|
|
17
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""GRYF SMART LIB"""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
BAUDRATE = 115200
|
|
4
|
+
|
|
5
|
+
class OUTPUT_STATES(IntEnum):
|
|
6
|
+
ON = 1
|
|
7
|
+
OFF = 2
|
|
8
|
+
TOGGLE = 3
|
|
9
|
+
|
|
10
|
+
class SCHUTTER_STATES(IntEnum):
|
|
11
|
+
CLOSE = 1
|
|
12
|
+
OPEN = 2
|
|
13
|
+
STOP = 3
|
|
14
|
+
STEP_MODE = 4
|
|
15
|
+
|
|
16
|
+
class KEY_MODE(IntEnum):
|
|
17
|
+
NO = 0
|
|
18
|
+
NC = 1
|
|
19
|
+
|
|
20
|
+
COMMAND_FUNCTION_IN = "I"
|
|
21
|
+
COMMAND_FUNCTION_OUT = "O"
|
|
22
|
+
COMMAND_FUNCTION_PWM = "LED"
|
|
23
|
+
COMMAND_FUNCTION_COVER = "R"
|
|
24
|
+
COMMAND_FUNCTION_FIND = "AT+FIND"
|
|
25
|
+
COMMAND_FUNCTION_PONG = "PONG"
|
|
26
|
+
COMMAND_FUNCTION_PRESS_SHORT = "PS"
|
|
27
|
+
COMMAND_FUNCTION_PRESS_LONG = "PL"
|
|
28
|
+
COMMAND_FUNCTION_TEMP = "T"
|
|
29
|
+
|
|
30
|
+
COMMAND_FUNCTION_GET_IN_STATE = "AT+StanIN"
|
|
31
|
+
COMMAND_FUNCTION_GET_OUT_STATE = "AT+StanOUT"
|
|
32
|
+
COMMAND_FUNCTION_SET_OUT = "AT+SetOut"
|
|
33
|
+
COMMAND_FUNCTION_SET_COVER = "AT+SetRol"
|
|
34
|
+
COMMAND_FUNCTION_SET_PWM = "AT+SetLED"
|
|
35
|
+
COMMAND_FUNCTION_PING = "PING"
|
|
36
|
+
COMMAND_FUNCTION_SET_PRESS_TIME = "AT+Key"
|
|
37
|
+
COMMADN_FUNCTION_SEARCH_MODULE = "AT+Search"
|
|
38
|
+
COMMAND_FUNCTION_RESET = "AT+RST"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from pygryfsmart.feedback import Feedback
|
|
2
|
+
from pygryfsmart.rs232 import RS232Handler
|
|
3
|
+
from pygryfsmart.const import (
|
|
4
|
+
BAUDRATE,
|
|
5
|
+
KEY_MODE,
|
|
6
|
+
OUTPUT_STATES,
|
|
7
|
+
SCHUTTER_STATES,
|
|
8
|
+
|
|
9
|
+
COMMAND_FUNCTION_IN,
|
|
10
|
+
COMMAND_FUNCTION_OUT,
|
|
11
|
+
|
|
12
|
+
COMMAND_FUNCTION_GET_IN_STATE,
|
|
13
|
+
COMMAND_FUNCTION_GET_OUT_STATE,
|
|
14
|
+
COMMAND_FUNCTION_SET_OUT,
|
|
15
|
+
COMMAND_FUNCTION_SET_COVER,
|
|
16
|
+
COMMAND_FUNCTION_SET_PWM,
|
|
17
|
+
COMMAND_FUNCTION_PING,
|
|
18
|
+
COMMAND_FUNCTION_SET_PRESS_TIME,
|
|
19
|
+
COMMADN_FUNCTION_SEARCH_MODULE,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_LOGGER = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
class Device(RS232Handler):
|
|
29
|
+
def __init__(self, port , callback = None):
|
|
30
|
+
super().__init__(port, BAUDRATE)
|
|
31
|
+
self.feedback = Feedback(callback=callback)
|
|
32
|
+
|
|
33
|
+
self._update_task = None
|
|
34
|
+
self._connection_task = None
|
|
35
|
+
self._last_ping = 0
|
|
36
|
+
|
|
37
|
+
def set_callback(self , callback):
|
|
38
|
+
self.feedback.callback = callback
|
|
39
|
+
|
|
40
|
+
async def stop_connection(self):
|
|
41
|
+
if self._connection_task:
|
|
42
|
+
self._connection_task.cancel()
|
|
43
|
+
try:
|
|
44
|
+
await self._connection_task
|
|
45
|
+
except asyncio.CancelledError:
|
|
46
|
+
_LOGGER.debug("Connection task was cancelled.")
|
|
47
|
+
if self._update_task:
|
|
48
|
+
self._update_task.cancel()
|
|
49
|
+
try:
|
|
50
|
+
await self._update_task
|
|
51
|
+
except asyncio.CancelledError:
|
|
52
|
+
_LOGGER.debug("Update task was cancelled.")
|
|
53
|
+
await self.close_connection()
|
|
54
|
+
_LOGGER.debug("Connection closed.")
|
|
55
|
+
|
|
56
|
+
async def __connection_task(self):
|
|
57
|
+
try:
|
|
58
|
+
while True:
|
|
59
|
+
line = await super().read_data()
|
|
60
|
+
_LOGGER.debug(f"Received data: {line}")
|
|
61
|
+
await self.feedback.input_data(line)
|
|
62
|
+
except asyncio.CancelledError:
|
|
63
|
+
_LOGGER.info("Connection task cancelled.")
|
|
64
|
+
await self.close_connection()
|
|
65
|
+
raise
|
|
66
|
+
except Exception as e:
|
|
67
|
+
_LOGGER.error(f"Error in connection task: {e}")
|
|
68
|
+
await self.close_connection()
|
|
69
|
+
raise
|
|
70
|
+
|
|
71
|
+
async def start_connection(self):
|
|
72
|
+
await super().open_connection()
|
|
73
|
+
self._connection_task = asyncio.create_task(self.__connection_task())
|
|
74
|
+
_LOGGER.info("Connection task started.")
|
|
75
|
+
|
|
76
|
+
async def send_data(self, data):
|
|
77
|
+
await super().send_data(data)
|
|
78
|
+
|
|
79
|
+
async def set_out(self, id: int, pin: int , state: OUTPUT_STATES | int):
|
|
80
|
+
states = ["0"] * 8 if pin > 6 else ["0"] * 6
|
|
81
|
+
states[pin - 1] = str(state)
|
|
82
|
+
|
|
83
|
+
command = f"{COMMAND_FUNCTION_SET_OUT}={id}," + ",".join(states) + "\n\r"
|
|
84
|
+
await self.send_data(command)
|
|
85
|
+
|
|
86
|
+
async def set_key_time(self , ps_time: int , pl_time: int , id: int , pin: int , type: KEY_MODE | int):
|
|
87
|
+
command = f"{COMMAND_FUNCTION_SET_PRESS_TIME}={id},{pin},{ps_time},{pl_time},{type}\n\r"
|
|
88
|
+
await self.send_data(command)
|
|
89
|
+
|
|
90
|
+
async def search_module(self , id: int):
|
|
91
|
+
if id != 0:
|
|
92
|
+
command = f"{COMMADN_FUNCTION_SEARCH_MODULE}=0,{id}\n\r"
|
|
93
|
+
await self.send_data(command)
|
|
94
|
+
|
|
95
|
+
async def search_modules(self , last_module: int):
|
|
96
|
+
for i in range(last_module):
|
|
97
|
+
command = f"{COMMADN_FUNCTION_SEARCH_MODULE}=0,{i + 1}\n\r"
|
|
98
|
+
await self.send_data(command)
|
|
99
|
+
|
|
100
|
+
async def ping(self , module_id: int):
|
|
101
|
+
command = f"{COMMAND_FUNCTION_PING}={module_id}\n\r"
|
|
102
|
+
await self.send_data(command)
|
|
103
|
+
await asyncio.sleep(0.05)
|
|
104
|
+
if self._last_ping == module_id:
|
|
105
|
+
self._last_ping = 0
|
|
106
|
+
return True
|
|
107
|
+
self._last_ping = 0
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
async def set_pwm(self , id: int , pin: int , level: int):
|
|
111
|
+
command = f"{COMMAND_FUNCTION_SET_PWM}={id},{pin},{level}\n\r"
|
|
112
|
+
await self.send_data(command)
|
|
113
|
+
|
|
114
|
+
async def set_cover(self , id: int , pin: int , time: int , operation: SCHUTTER_STATES | int):
|
|
115
|
+
if operation in {SCHUTTER_STATES.CLOSE , SCHUTTER_STATES.OPEN , SCHUTTER_STATES.STOP , SCHUTTER_STATES.STEP_MODE} and pin in {1 , 2 , 3 , 4}:
|
|
116
|
+
states = ["0"] * 4
|
|
117
|
+
states[pin - 1] = str(operation)
|
|
118
|
+
control_sum = id + time + int(states[0]) + int(states[1]) + int(states[2]) + int(states[3])
|
|
119
|
+
|
|
120
|
+
command = f"{COMMAND_FUNCTION_SET_COVER}={id},{time},{states[0]},{states[1]},{states[2]},{states[3]},{control_sum}\n\r"
|
|
121
|
+
await self.send_data(command)
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError(f"Argument out of scope: id: {id} , pin: {pin} , time: {time}, operation: {operation}")
|
|
124
|
+
|
|
125
|
+
async def reset(self , module_id: int , update_states: bool):
|
|
126
|
+
if module_id == 0:
|
|
127
|
+
command = "AT+RST=0\n\r"
|
|
128
|
+
await self.send_data(command)
|
|
129
|
+
if update_states == True:
|
|
130
|
+
module_count = len(self.feedback.data[COMMAND_FUNCTION_OUT])
|
|
131
|
+
await asyncio.sleep(2)
|
|
132
|
+
states = self.feedback.data[COMMAND_FUNCTION_OUT]
|
|
133
|
+
for i in range(module_count):
|
|
134
|
+
tabble = list(self.feedback.data[COMMAND_FUNCTION_OUT][i+1].values())
|
|
135
|
+
states = ",".join(map(str, tabble))
|
|
136
|
+
command = f"AT+SetOut={i+1},{states}\n\r"
|
|
137
|
+
await self.send_data(command)
|
|
138
|
+
|
|
139
|
+
def start_update_interval(self, time: int):
|
|
140
|
+
if not self._update_task:
|
|
141
|
+
self._update_task = asyncio.create_task(self.__states_update_interval(time))
|
|
142
|
+
_LOGGER.info("Update interval task started.")
|
|
143
|
+
|
|
144
|
+
async def __states_update_interval(self, time: int):
|
|
145
|
+
try:
|
|
146
|
+
while True:
|
|
147
|
+
module_count = max(len(self.feedback.data[COMMAND_FUNCTION_IN]), len(self.feedback.data[COMMAND_FUNCTION_OUT]))
|
|
148
|
+
|
|
149
|
+
for i in range(module_count):
|
|
150
|
+
try:
|
|
151
|
+
command = f"{COMMAND_FUNCTION_GET_IN_STATE}={i + 1}\n\r"
|
|
152
|
+
await self.send_data(command)
|
|
153
|
+
await asyncio.sleep(0.1)
|
|
154
|
+
|
|
155
|
+
command = f"{COMMAND_FUNCTION_GET_OUT_STATE}={i + 1}\n\r"
|
|
156
|
+
await self.send_data(command)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
_LOGGER.error(f"Error updating module {i + 1}: {e}")
|
|
159
|
+
|
|
160
|
+
await asyncio.sleep(time)
|
|
161
|
+
except asyncio.CancelledError:
|
|
162
|
+
_LOGGER.info("Update interval task cancelled.")
|
|
163
|
+
except Exception as e:
|
|
164
|
+
_LOGGER.error(f"Error in update interval: {e}")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from pygryfsmart.const import (
|
|
5
|
+
COMMAND_FUNCTION_IN,
|
|
6
|
+
COMMAND_FUNCTION_OUT,
|
|
7
|
+
COMMAND_FUNCTION_PWM,
|
|
8
|
+
COMMAND_FUNCTION_COVER,
|
|
9
|
+
COMMAND_FUNCTION_FIND,
|
|
10
|
+
COMMAND_FUNCTION_PONG,
|
|
11
|
+
COMMAND_FUNCTION_PRESS_SHORT,
|
|
12
|
+
COMMAND_FUNCTION_PRESS_LONG,
|
|
13
|
+
COMMAND_FUNCTION_TEMP,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_LOGGER = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class Feedback:
|
|
19
|
+
def __init__(self , callback=None) -> None:
|
|
20
|
+
self.callback = callback
|
|
21
|
+
self._data = {
|
|
22
|
+
COMMAND_FUNCTION_IN: {},
|
|
23
|
+
COMMAND_FUNCTION_OUT: {},
|
|
24
|
+
COMMAND_FUNCTION_PWM: {},
|
|
25
|
+
COMMAND_FUNCTION_COVER: {},
|
|
26
|
+
COMMAND_FUNCTION_FIND: {},
|
|
27
|
+
COMMAND_FUNCTION_PONG: {},
|
|
28
|
+
COMMAND_FUNCTION_TEMP: {},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def data(self):
|
|
33
|
+
return self._data
|
|
34
|
+
|
|
35
|
+
async def __parse_metod_1(self , parsed_states , line: str , function: str):
|
|
36
|
+
if len(parsed_states) not in {7 , 9}:
|
|
37
|
+
raise ValueError(f"Invalid number of arguments: {line}")
|
|
38
|
+
|
|
39
|
+
for i in range(1, len(parsed_states)):
|
|
40
|
+
if parsed_states[i] not in {"0" , "1"}:
|
|
41
|
+
raise ValueError(f"Wrong parameter value: {line}")
|
|
42
|
+
|
|
43
|
+
pin = int(parsed_states[0])
|
|
44
|
+
if pin not in self._data[function]:
|
|
45
|
+
self._data[function][pin] = {}
|
|
46
|
+
self._data[function][pin][i] = int(parsed_states[i])
|
|
47
|
+
|
|
48
|
+
async def __parse_metod_2(self , parsed_states , line: str , function: str , prefix: int):
|
|
49
|
+
if parsed_states[0] not in {"1" , "2" , "3" , "4" , "5" , "6" , "7" , "8"}:
|
|
50
|
+
raise ValueError(f"Argument out of scope: {line}")
|
|
51
|
+
|
|
52
|
+
pin = int(parsed_states[1])
|
|
53
|
+
id = int(parsed_states[0])
|
|
54
|
+
if id not in self._data[function]:
|
|
55
|
+
self._data[function][id] = {}
|
|
56
|
+
self._data[function][id][pin] = prefix
|
|
57
|
+
|
|
58
|
+
async def __parse_metod_3(self , parsed_states , line: str , function: str):
|
|
59
|
+
if parsed_states[0] not in {"1" , "2" , "3" , "4" , "5" , "6" , "7" , "8"}:
|
|
60
|
+
raise ValueError(f"Argument out of scope: {line}")
|
|
61
|
+
|
|
62
|
+
pin = int(parsed_states[1])
|
|
63
|
+
id = int(parsed_states[0])
|
|
64
|
+
if id not in self._data[function]:
|
|
65
|
+
self._data[function][id] = {}
|
|
66
|
+
self._data[function][id][pin] = parsed_states[2]
|
|
67
|
+
|
|
68
|
+
async def __parse_cover(self , parsed_states , line: str , function: str):
|
|
69
|
+
if len(parsed_states) != 5:
|
|
70
|
+
raise ValueError(f"Invalid number of arguments: {line}")
|
|
71
|
+
|
|
72
|
+
for i in range(1, len(parsed_states)):
|
|
73
|
+
if parsed_states[i] not in {"0" , "1"}:
|
|
74
|
+
raise ValueError(f"Wrong parameter value: {line}")
|
|
75
|
+
|
|
76
|
+
pin = int(parsed_states[0])
|
|
77
|
+
if pin not in self._data[function]:
|
|
78
|
+
self._data[function][pin] = {}
|
|
79
|
+
self._data[function][pin][i] = int(parsed_states[i])
|
|
80
|
+
|
|
81
|
+
async def __parse_temp(self , parsed_states , line: str):
|
|
82
|
+
if parsed_states[0] not in {"1" , "2" , "3" , "4" , "5" , "6" , "7" , "8"}:
|
|
83
|
+
raise ValueError(f"Argument out of scope: {line}")
|
|
84
|
+
|
|
85
|
+
pin = int(parsed_states[1])
|
|
86
|
+
id = int(parsed_states[0])
|
|
87
|
+
if id not in self._data[COMMAND_FUNCTION_TEMP]:
|
|
88
|
+
self._data[COMMAND_FUNCTION_TEMP][id] = {}
|
|
89
|
+
self._data[COMMAND_FUNCTION_TEMP][id][pin] = float(f"{parsed_states[2]}.{parsed_states[3]}")
|
|
90
|
+
|
|
91
|
+
async def __parse_find(self , parsed_states):
|
|
92
|
+
id = int(parsed_states[0])
|
|
93
|
+
self._data[COMMAND_FUNCTION_FIND][id] = float(f"{parsed_states[1]}.{parsed_states[2]}")
|
|
94
|
+
|
|
95
|
+
async def __parse_pong(self , parsed_states):
|
|
96
|
+
now = datetime.now()
|
|
97
|
+
|
|
98
|
+
id = int(parsed_states[0])
|
|
99
|
+
self._data[COMMAND_FUNCTION_PONG][id] = now.strftime("%H:%M")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def input_data(self , line):
|
|
103
|
+
if line == "??????????":
|
|
104
|
+
return
|
|
105
|
+
try:
|
|
106
|
+
parts = line.split('=')
|
|
107
|
+
parsed_states = parts[1].split(',')
|
|
108
|
+
last_state = parsed_states[-1].split(';')
|
|
109
|
+
parsed_states[-1] = last_state[0]
|
|
110
|
+
_LOGGER.debug(f"{parsed_states}")
|
|
111
|
+
|
|
112
|
+
COMMAND_MAPPER = {
|
|
113
|
+
COMMAND_FUNCTION_IN: lambda states , line : self.__parse_metod_1(states , line , COMMAND_FUNCTION_IN),
|
|
114
|
+
COMMAND_FUNCTION_OUT: lambda states , line : self.__parse_metod_1(states , line , COMMAND_FUNCTION_OUT),
|
|
115
|
+
COMMAND_FUNCTION_PRESS_SHORT: lambda states , line : self.__parse_metod_2(states , line , COMMAND_FUNCTION_IN , 2),
|
|
116
|
+
COMMAND_FUNCTION_PRESS_LONG: lambda states , line : self.__parse_metod_2(states , line , COMMAND_FUNCTION_IN , 3),
|
|
117
|
+
COMMAND_FUNCTION_TEMP: lambda states , line : self.__parse_temp(states , line),
|
|
118
|
+
COMMAND_FUNCTION_PWM: lambda states , line : self.__parse_metod_3(states , line , COMMAND_FUNCTION_PWM),
|
|
119
|
+
COMMAND_FUNCTION_COVER: lambda states , line : self.__parse_cover(states , line , COMMAND_FUNCTION_COVER),
|
|
120
|
+
COMMAND_FUNCTION_FIND: lambda states , line: self.__parse_find(states),
|
|
121
|
+
COMMAND_FUNCTION_PONG: lambda states , line: self.__parse_pong(states),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if str(parts[0]).upper() in COMMAND_MAPPER:
|
|
125
|
+
await COMMAND_MAPPER[str(parts[0]).upper()](parsed_states , line)
|
|
126
|
+
|
|
127
|
+
if self.callback:
|
|
128
|
+
await self.callback(self._data)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
_LOGGER.error(f"ERROR parsing data: {e}")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from serial_asyncio import open_serial_connection
|
|
3
|
+
|
|
4
|
+
_LOGGER = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
class RS232Handler:
|
|
7
|
+
def __init__(self, port, baudrate):
|
|
8
|
+
self.port = port
|
|
9
|
+
self.baudrate = baudrate
|
|
10
|
+
self.reader = None
|
|
11
|
+
self.writer = None
|
|
12
|
+
|
|
13
|
+
async def open_connection(self):
|
|
14
|
+
try:
|
|
15
|
+
self.reader, self.writer = await open_serial_connection(url=self.port, baudrate=self.baudrate)
|
|
16
|
+
_LOGGER.info(f"Connection opened on port {self.port} with baudrate {self.baudrate}")
|
|
17
|
+
except Exception as e:
|
|
18
|
+
_LOGGER.error(f"Failed to open connection on port {self.port}: {e}")
|
|
19
|
+
self.reader, self.writer = None, None
|
|
20
|
+
raise
|
|
21
|
+
|
|
22
|
+
async def close_connection(self):
|
|
23
|
+
if self.writer:
|
|
24
|
+
try:
|
|
25
|
+
self.writer.close()
|
|
26
|
+
await self.writer.wait_closed()
|
|
27
|
+
_LOGGER.info("Connection closed successfully.")
|
|
28
|
+
except Exception as e:
|
|
29
|
+
_LOGGER.error(f"Error while closing connection: {e}")
|
|
30
|
+
else:
|
|
31
|
+
_LOGGER.warning("Connection was already closed or not initialized.")
|
|
32
|
+
|
|
33
|
+
async def send_data(self, data):
|
|
34
|
+
if self.writer:
|
|
35
|
+
try:
|
|
36
|
+
self.writer.write(data.encode())
|
|
37
|
+
await self.writer.drain()
|
|
38
|
+
_LOGGER.debug(f"Sent data: {data}")
|
|
39
|
+
except Exception as e:
|
|
40
|
+
_LOGGER.error(f"Error while sending data: {e}")
|
|
41
|
+
else:
|
|
42
|
+
_LOGGER.warning("Cannot send data: Writer is not initialized.")
|
|
43
|
+
|
|
44
|
+
async def read_data(self):
|
|
45
|
+
if self.reader:
|
|
46
|
+
try:
|
|
47
|
+
data = await self.reader.readuntil(b"\n")
|
|
48
|
+
_LOGGER.debug(f"Read data: {data.decode().strip()}")
|
|
49
|
+
return data.decode().strip()
|
|
50
|
+
except Exception as e:
|
|
51
|
+
_LOGGER.error(f"Error while reading data: {e}")
|
|
52
|
+
return None
|
|
53
|
+
else:
|
|
54
|
+
_LOGGER.warning("Cannot read data: Reader is not initialized.")
|
|
55
|
+
return None
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pygryfsmart
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Library for the GryfSmart system
|
|
5
|
+
Home-page: https://github.com/karlowiczpl/pygryfsmart
|
|
6
|
+
Author: @karlowiczpl
|
|
7
|
+
Author-email: k.karlowski@outlook.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: pyserial
|
|
14
|
+
Requires-Dist: pyserial-asyncio
|
|
15
|
+
|
|
16
|
+
#Gryf smart system lib
|
|
17
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
setup.cfg
|
|
6
|
+
pygryfsmart/__init__.py
|
|
7
|
+
pygryfsmart/const.py
|
|
8
|
+
pygryfsmart/device.py
|
|
9
|
+
pygryfsmart/feedback.py
|
|
10
|
+
pygryfsmart/rs232.py
|
|
11
|
+
pygryfsmart.egg-info/PKG-INFO
|
|
12
|
+
pygryfsmart.egg-info/SOURCES.txt
|
|
13
|
+
pygryfsmart.egg-info/dependency_links.txt
|
|
14
|
+
pygryfsmart.egg-info/requires.txt
|
|
15
|
+
pygryfsmart.egg-info/top_level.txt
|
|
16
|
+
tests/__init__.py
|
|
17
|
+
tests/test_feedback.py
|
|
18
|
+
tests/test_rs232.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = pygryfsmart
|
|
3
|
+
version = 0.1.0
|
|
4
|
+
author = @karlowiczpl
|
|
5
|
+
author_email = k.karlowski@outlook.com
|
|
6
|
+
description = Library for the GryfSmart system
|
|
7
|
+
long_description = file: README.md
|
|
8
|
+
url = https://github.com/karlowiczpl/pygryfsmart
|
|
9
|
+
license = MIT
|
|
10
|
+
classifiers =
|
|
11
|
+
Programming Language :: Python :: 3
|
|
12
|
+
Operating System :: OS Independent
|
|
13
|
+
|
|
14
|
+
[options]
|
|
15
|
+
packages = find:
|
|
16
|
+
python_requires = >=3.8
|
|
17
|
+
install_requires =
|
|
18
|
+
pyserial
|
|
19
|
+
pyserial-asyncio
|
|
20
|
+
|
|
21
|
+
[egg_info]
|
|
22
|
+
tag_build =
|
|
23
|
+
tag_date = 0
|
|
24
|
+
|
|
File without changes
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import AsyncMock
|
|
3
|
+
from pygryfsmart.feedback import Feedback
|
|
4
|
+
from pygryfsmart.const import (
|
|
5
|
+
COMMAND_FUNCTION_IN,
|
|
6
|
+
COMMAND_FUNCTION_OUT,
|
|
7
|
+
COMMAND_FUNCTION_PWM,
|
|
8
|
+
COMMAND_FUNCTION_COVER,
|
|
9
|
+
COMMAND_FUNCTION_FIND,
|
|
10
|
+
COMMAND_FUNCTION_PONG,
|
|
11
|
+
COMMAND_FUNCTION_PRESS_SHORT,
|
|
12
|
+
COMMAND_FUNCTION_PRESS_LONG,
|
|
13
|
+
COMMAND_FUNCTION_TEMP,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def feedback():
|
|
19
|
+
return Feedback()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.asyncio
|
|
23
|
+
async def test_parse_metod_1(feedback):
|
|
24
|
+
line = "IN=1,1,0,1,0,1,0,1;"
|
|
25
|
+
parsed_states = ["1", "1", "0", "1", "0", "1", "0", "1" , "1"]
|
|
26
|
+
await feedback._Feedback__parse_metod_1(parsed_states, line, COMMAND_FUNCTION_IN)
|
|
27
|
+
|
|
28
|
+
assert feedback.data[COMMAND_FUNCTION_IN][1] == {1: 1, 2: 0, 3: 1, 4: 0, 5: 1, 6: 0, 7: 1, 8: 1}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_parse_metod_2(feedback):
|
|
33
|
+
line = "PS=2,3;"
|
|
34
|
+
parsed_states = ["2", "3"]
|
|
35
|
+
await feedback._Feedback__parse_metod_2(parsed_states, line, COMMAND_FUNCTION_PRESS_SHORT, 2)
|
|
36
|
+
|
|
37
|
+
assert feedback.data[COMMAND_FUNCTION_PRESS_SHORT][2][3] == 2
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_parse_metod_3(feedback):
|
|
42
|
+
line = "PWM=3,2,255;"
|
|
43
|
+
parsed_states = ["3", "2", "255"]
|
|
44
|
+
await feedback._Feedback__parse_metod_3(parsed_states, line, COMMAND_FUNCTION_PWM)
|
|
45
|
+
|
|
46
|
+
assert feedback.data[COMMAND_FUNCTION_PWM][3][2] == "255"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.asyncio
|
|
50
|
+
async def test_parse_cover(feedback):
|
|
51
|
+
line = "COVER=4,1,0,1,0;"
|
|
52
|
+
parsed_states = ["4", "1", "0", "1", "0"]
|
|
53
|
+
await feedback._Feedback__parse_cover(parsed_states, line, COMMAND_FUNCTION_COVER)
|
|
54
|
+
|
|
55
|
+
assert feedback.data[COMMAND_FUNCTION_COVER][4] == {1: 1, 2: 0, 3: 1, 4: 0}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_parse_temp(feedback):
|
|
60
|
+
line = "TEMP=5,2,25,5;"
|
|
61
|
+
parsed_states = ["5", "2", "25", "5"]
|
|
62
|
+
await feedback._Feedback__parse_temp(parsed_states, line)
|
|
63
|
+
|
|
64
|
+
assert feedback.data[COMMAND_FUNCTION_TEMP][5] == {2: 25.5}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_parse_find(feedback):
|
|
69
|
+
parsed_states = ["6", "123", "45"]
|
|
70
|
+
await feedback._Feedback__parse_find(parsed_states)
|
|
71
|
+
|
|
72
|
+
assert feedback.data[COMMAND_FUNCTION_FIND][6] == 123.45
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_input_data_valid(feedback):
|
|
76
|
+
callback = AsyncMock()
|
|
77
|
+
feedback.callback = callback
|
|
78
|
+
|
|
79
|
+
line = "IN=1,1,0,1,0,1,0,1;"
|
|
80
|
+
await feedback.input_data(line)
|
|
81
|
+
|
|
82
|
+
assert feedback.data == {
|
|
83
|
+
COMMAND_FUNCTION_IN: {},
|
|
84
|
+
COMMAND_FUNCTION_OUT: {},
|
|
85
|
+
COMMAND_FUNCTION_PWM: {},
|
|
86
|
+
COMMAND_FUNCTION_COVER: {},
|
|
87
|
+
COMMAND_FUNCTION_FIND: {},
|
|
88
|
+
COMMAND_FUNCTION_PONG: {},
|
|
89
|
+
COMMAND_FUNCTION_TEMP: {},
|
|
90
|
+
COMMAND_FUNCTION_PRESS_SHORT: {},
|
|
91
|
+
COMMAND_FUNCTION_PRESS_LONG: {},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@pytest.mark.asyncio
|
|
95
|
+
async def test_input_data_invalid_command(feedback):
|
|
96
|
+
line = "INVALID=1,1,0,1;"
|
|
97
|
+
await feedback.input_data(line)
|
|
98
|
+
|
|
99
|
+
# Brak zmian w danych, ponieważ komenda jest nieznana
|
|
100
|
+
assert feedback.data == {
|
|
101
|
+
COMMAND_FUNCTION_IN: {},
|
|
102
|
+
COMMAND_FUNCTION_OUT: {},
|
|
103
|
+
COMMAND_FUNCTION_PWM: {},
|
|
104
|
+
COMMAND_FUNCTION_COVER: {},
|
|
105
|
+
COMMAND_FUNCTION_FIND: {},
|
|
106
|
+
COMMAND_FUNCTION_PONG: {},
|
|
107
|
+
COMMAND_FUNCTION_TEMP: {},
|
|
108
|
+
COMMAND_FUNCTION_PRESS_SHORT: {},
|
|
109
|
+
COMMAND_FUNCTION_PRESS_LONG: {},
|
|
110
|
+
}
|
|
111
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import AsyncMock, patch
|
|
3
|
+
from pygryfsmart.rs232 import RS232Handler
|
|
4
|
+
|
|
5
|
+
@pytest.fixture
|
|
6
|
+
def rs232_handler():
|
|
7
|
+
return RS232Handler(port="/dev/ttyUSB0", baudrate=115200)
|
|
8
|
+
pytest.mark.asyncio
|
|
9
|
+
@patch("serial_asyncio.open_serial_connection")
|
|
10
|
+
async def test_open_connection(mock_open_serial):
|
|
11
|
+
mock_reader = AsyncMock()
|
|
12
|
+
mock_writer = AsyncMock()
|
|
13
|
+
mock_open_serial.return_value = (mock_reader, mock_writer)
|
|
14
|
+
|
|
15
|
+
rs232_handler = RS232Handler(port="/dev/ttyUSB0", baudrate=115200)
|
|
16
|
+
|
|
17
|
+
await rs232_handler.open_connection()
|
|
18
|
+
|
|
19
|
+
mock_open_serial.assert_called_once_with(url="/dev/ttyUSB0", baudrate=115200)
|
|
20
|
+
mock_writer = AsyncMock()
|
|
21
|
+
rs232_handler.writer = mock_writer
|
|
22
|
+
|
|
23
|
+
await rs232_handler.close_connection()
|
|
24
|
+
|
|
25
|
+
mock_writer.close.assert_called_once()
|
|
26
|
+
await mock_writer.wait_closed()
|
|
27
|
+
|
|
28
|
+
@pytest.mark.asyncio
|
|
29
|
+
async def test_send_data(rs232_handler):
|
|
30
|
+
mock_writer = AsyncMock()
|
|
31
|
+
rs232_handler.writer = mock_writer
|
|
32
|
+
|
|
33
|
+
await rs232_handler.send_data("Test data")
|
|
34
|
+
|
|
35
|
+
mock_writer.write.assert_called_once_with(b"Test data")
|
|
36
|
+
await mock_writer.drain()
|
|
37
|
+
|
|
38
|
+
@pytest.mark.asyncio
|
|
39
|
+
async def test_read_data(rs232_handler):
|
|
40
|
+
mock_reader = AsyncMock()
|
|
41
|
+
mock_reader.readuntil = AsyncMock(return_value=b"Test data\n")
|
|
42
|
+
rs232_handler.reader = mock_reader
|
|
43
|
+
|
|
44
|
+
data = await rs232_handler.read_data()
|
|
45
|
+
|
|
46
|
+
assert data == "Test data"
|
|
47
|
+
mock_reader.readuntil.assert_called_once_with(b"\n")
|