gryfsmartio 0.1__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) 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,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: gryfsmartio
3
+ Version: 0.1
4
+ Summary: Library for the GryfSmart system
5
+ Home-page: https://github.com/karlowiczpl/gryfsmartio
6
+ Author: @karlowiczpl
7
+ Author-email: kkarlowicz@gryfsmart.pl
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENCE
14
+ Requires-Dist: pyserial-asyncio==0.6
15
+ Dynamic: license-file
File without changes
@@ -0,0 +1,155 @@
1
+ import logging
2
+
3
+ from typing import List, Optional
4
+ from pydantic import BaseModel, Field, EmailStr
5
+
6
+ _LOGGER = logging.getLogger(__name__)
7
+
8
+ class ParsedFunctions():
9
+ INPUTS = "I"
10
+ OUTPUTS = "O"
11
+ PWM = "LED"
12
+ COVER = "R"
13
+ TEMP = "T"
14
+ FIND = "AT+FIND"
15
+ PONG = "PONG"
16
+ PRESS_LONG = "PL"
17
+ PRESS_SHORT = "PS"
18
+
19
+ broadcastingFunctions = [
20
+ ParsedFunctions.INPUTS,
21
+ ParsedFunctions.OUTPUTS,
22
+ ParsedFunctions.COVER,
23
+ ]
24
+
25
+ subscriptableFunction = [
26
+ ParsedFunctions.INPUTS,
27
+ ParsedFunctions.OUTPUTS,
28
+ ParsedFunctions.PWM,
29
+ ParsedFunctions.COVER,
30
+ ParsedFunctions.TEMP,
31
+ ParsedFunctions.PRESS_SHORT,
32
+ ParsedFunctions.PRESS_LONG,
33
+ ]
34
+
35
+ class ParsedData:
36
+
37
+ _function: str
38
+ _pin: int
39
+ _id: int
40
+ _parsed_states: list[str]
41
+ _broadcast_function: bool
42
+ _error = True
43
+
44
+ def __init__(self, data: str) -> None:
45
+
46
+ if('=' not in data or not data):
47
+ return
48
+
49
+ try:
50
+ parts = data.split('=', 1)
51
+
52
+ self._function = parts[0].upper()
53
+ self._parsed_states = parts[1].split(',')
54
+ self._id = int(self._parsed_states[0])
55
+
56
+ if(self._function in broadcastingFunctions):
57
+ self._broadcast_function = True
58
+ else:
59
+ self._broadcast_function = False
60
+
61
+ self._error = False
62
+
63
+ except Exception as e:
64
+ _LOGGER.error(f"Error occurred while parsing: {e}")
65
+
66
+ def error_occurred(self) -> bool:
67
+ return self._error
68
+
69
+ @property
70
+ def is_broadcast(self) -> bool:
71
+ return self._broadcast_function
72
+
73
+ @property
74
+ def function(self) -> str:
75
+ return self._function
76
+
77
+ @property
78
+ def parsed_states(self) -> list[str]:
79
+ return self._parsed_states
80
+
81
+ @property
82
+ def id(self) -> int:
83
+ return self._id
84
+
85
+ @property
86
+ def pin(self) -> int:
87
+ if(not self._broadcast_function):
88
+ return int(self._parsed_states[1])
89
+
90
+ return 0
91
+
92
+ class Driver:
93
+ id: int
94
+ inputs: list[int]
95
+ outputs: list[int]
96
+ pwms: list[int]
97
+ covers: list[int]
98
+
99
+ def __init__(
100
+ self,
101
+ id: int
102
+ ) -> None:
103
+ self.id = id
104
+ self.inputs: list[int] = [0] * 20
105
+ self.outputs: list[int] = [0] * 20
106
+ self.pwms: list[int] = [0] * 20
107
+ self.covers: list[int] = [0] * 20
108
+
109
+ @property
110
+ def function_map(self) -> dict[str, list[int]]:
111
+ return {
112
+ ParsedFunctions.INPUTS: self.inputs,
113
+ ParsedFunctions.OUTPUTS: self.outputs,
114
+ ParsedFunctions.PWM: self.pwms,
115
+ ParsedFunctions.COVER: self.covers,
116
+ }
117
+
118
+ def __getitem__(self, key: str) -> list[int]:
119
+ return self.function_map[key]
120
+
121
+ class Subscription:
122
+ _function: str
123
+ _id: int
124
+ _pin: int
125
+ _fun_ptr = None
126
+
127
+ def __init__(
128
+ self,
129
+ hardware_id: int,
130
+ hardware_pin: int,
131
+ function: str,
132
+ async_fun_ptr
133
+ ) -> None:
134
+ self._id = hardware_id
135
+ self._pin = hardware_pin
136
+ self._function = function.strip()
137
+
138
+ self._fun_ptr = async_fun_ptr
139
+
140
+ def cover_with_data(self, parsed_data: ParsedData) -> bool:
141
+ if(parsed_data.function.strip() != self._function):
142
+ return False
143
+
144
+ if(parsed_data.is_broadcast):
145
+ if(parsed_data.id == self._id):
146
+ return True
147
+ else:
148
+ if(parsed_data.id == self._id and parsed_data.pin == self._pin):
149
+ return True
150
+
151
+ return False
152
+
153
+ async def exec_fun(self, parsed_data):
154
+ await self._fun_ptr(parsed_data)
155
+
@@ -0,0 +1,294 @@
1
+ import asyncio
2
+ import logging
3
+ import re
4
+ import serial_asyncio
5
+
6
+ from .parsing import ParsedData, ParsedFunctions, Subscription, subscriptableFunction
7
+ from gryfsmartio.parsing import Driver
8
+
9
+ _LOGGER = logging.getLogger(__name__)
10
+
11
+ TCP_PORT = 4510
12
+ SERIAL_BAUDRATE = 115200
13
+
14
+ class WriterBase:
15
+
16
+ def __init__(
17
+ self,
18
+ port: str,
19
+ ) -> None:
20
+ pass
21
+
22
+ def write(
23
+ self,
24
+ data: str
25
+ ) -> None:
26
+ pass
27
+
28
+ async def read(self) -> str:
29
+ return ""
30
+
31
+ async def close(self) -> None:
32
+ pass
33
+
34
+ async def open(self) -> None:
35
+ pass
36
+
37
+ class SerialWriter(WriterBase):
38
+ _port: str
39
+ _baudrate: int
40
+ _reader: asyncio.StreamReader | None = None
41
+ _writer: asyncio.StreamWriter | None = None
42
+
43
+ def __init__(self, port: str, baudrate: int = SERIAL_BAUDRATE) -> None:
44
+ self._port = port
45
+ self._baudrate = baudrate
46
+
47
+ async def open(self) -> None:
48
+ self._reader, self._writer = await serial_asyncio.open_serial_connection(
49
+ url=self._port,
50
+ baudrate=self._baudrate
51
+ )
52
+
53
+ async def write(
54
+ self,
55
+ data: str
56
+ ) -> None:
57
+ if self._writer is None or self._writer.is_closing():
58
+ return
59
+
60
+ try:
61
+ if not data.endswith("\n"):
62
+ data += "\n"
63
+
64
+ self._writer.write(data.encode("utf-8"))
65
+
66
+ await self._writer.drain()
67
+ _LOGGER.debug(f"Serial sent: {data.strip()}")
68
+
69
+ except Exception as err:
70
+ _LOGGER.error(f"Error while serial send: {err}")
71
+
72
+ async def read(self) -> str:
73
+ if self._reader is None:
74
+ return ""
75
+
76
+ line_bytes = await asyncio.wait_for(self._reader.readline(), timeout=10.0)
77
+
78
+ return line_bytes.decode("utf-8", errors="ignore").strip()
79
+
80
+ async def close(self) -> None:
81
+ if self._writer is not None:
82
+ try:
83
+ self._writer.close()
84
+ await self._writer.wait_closed()
85
+ except Exception:
86
+ pass
87
+
88
+ self._reader = None
89
+ self._writer = None
90
+
91
+ class TcpWriter(WriterBase):
92
+ _ip: str
93
+ _reader = None
94
+ _writer = None
95
+
96
+ def __init__(
97
+ self,
98
+ ip: str,
99
+ ) -> None:
100
+ self._ip = ip
101
+
102
+ async def write(
103
+ self,
104
+ data: str
105
+ ) -> None:
106
+ if self._writer is None or self._writer.is_closing():
107
+ _LOGGER.error("No active TCP connecction - cannot send data")
108
+ return
109
+
110
+ try:
111
+ if not data.endswith("\n"):
112
+ data += "\n"
113
+
114
+ self._writer.write(data.encode("utf-8"))
115
+ await self._writer.drain()
116
+
117
+ _LOGGER.debug(f"Command sended: {data.strip()}")
118
+
119
+ except Exception as err:
120
+ _LOGGER.error(f"En Error while bufforing TCP data: {err}")
121
+
122
+ async def read(self) -> str:
123
+ if self._reader is None:
124
+ _LOGGER.error("Connection don't exist")
125
+
126
+ try:
127
+ line_bytes = await asyncio.wait_for(self._reader.readline(), timeout=1000.0)
128
+
129
+ if not line_bytes:
130
+ _LOGGER.error("Connection don't exist")
131
+
132
+ return line_bytes.decode("utf-8", errors="ignore").strip()
133
+
134
+ except (AttributeError, Exception) as err:
135
+ _LOGGER.debug(f"En Error occurred while reading TCP: {err}")
136
+ raise err
137
+
138
+ async def close(self) -> None:
139
+ if self._writer is not None:
140
+ try:
141
+ self._writer.close()
142
+
143
+ await self._writer.wait_closed()
144
+ except Exception as e:
145
+ _LOGGER.error(f"An Error occurred while closing connection: {self._ip}")
146
+
147
+ self._reader = None
148
+ self._writer = None
149
+
150
+ async def open(self) -> None:
151
+ self._reader, self._writer = await asyncio.wait_for(
152
+ asyncio.open_connection(self._ip, TCP_PORT),
153
+ timeout=5.0
154
+ )
155
+
156
+ sock = self._writer.get_extra_info('socket')
157
+ if sock is not None:
158
+ import socket
159
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
160
+
161
+ class Transport():
162
+
163
+ _connection: WriterBase
164
+ _target: str
165
+ _subscriptions = None
166
+ _task: asyncio.Task | None = None
167
+ _drivers_data: list[Driver] = []
168
+
169
+ def __init__(
170
+ self,
171
+ connection_target: str
172
+ ) -> None:
173
+
174
+ self._target = connection_target
175
+
176
+ ipv4_patern = r"\b(?:\d{1,3}\.){3}\d{1,3}\b"
177
+ serial_port_patern = r"/dev/tty\S*"
178
+
179
+ if re.search(ipv4_patern , connection_target):
180
+ self._connection = TcpWriter(connection_target)
181
+ elif re.search(serial_port_patern , connection_target):
182
+ pass
183
+ else:
184
+ _LOGGER.error(f"Incorrect Communication Port: {connection_target}")
185
+
186
+ def register_subscription(self, subscription: Subscription):
187
+ if self._subscriptions is None:
188
+ self._subscriptions = []
189
+
190
+ self._subscriptions.append(subscription)
191
+
192
+ async def write(
193
+ self,
194
+ data: str,
195
+ ) -> None:
196
+ await self._connection.write(data)
197
+
198
+ def start_communication(self) -> None:
199
+
200
+ if self._task is not None and not self._task.done():
201
+ _LOGGER.warning("Communication: %s already exists!", self._target)
202
+ return
203
+
204
+ self._task = asyncio.create_task(
205
+ self.communication_task(),
206
+ name=f"transport_task_{self._target}"
207
+ )
208
+
209
+ async def stop_communication(self) -> None:
210
+
211
+ if self._task is not None:
212
+ self._task.cancel()
213
+ try:
214
+ await self._task
215
+ except asyncio.CancelledError:
216
+ pass
217
+ finally:
218
+ self._task = None
219
+
220
+ async def communication_task(self):
221
+
222
+ while True:
223
+ try:
224
+ _LOGGER.info(f"Connecting with: {self._target}")
225
+ await self._connection.open()
226
+
227
+ _LOGGER.info(f"Successfuly connected to: {self._target}")
228
+
229
+ while True:
230
+ try:
231
+ readed = await self._connection.read()
232
+
233
+ if(readed):
234
+ _LOGGER.info(f"New command has arived: {readed}")
235
+
236
+ if(readed == "??????????"):
237
+ continue
238
+
239
+ parsed_data = ParsedData(readed)
240
+
241
+ if(parsed_data.error_occurred() or parsed_data.function not in subscriptableFunction):
242
+ continue
243
+
244
+ if(self._subscriptions is not None):
245
+ for sub in self._subscriptions:
246
+ if(sub.cover_with_data(parsed_data)):
247
+ await sub.exec_fun(parsed_data)
248
+
249
+ exist = False
250
+ for item in self._drivers_data:
251
+ if item.id == parsed_data.id:
252
+ exist = True
253
+
254
+ if not exist:
255
+ self._drivers_data.append(Driver(parsed_data.id))
256
+
257
+ for item in self._drivers_data:
258
+ if item.id == parsed_data.id:
259
+ if parsed_data.is_broadcast:
260
+ pass
261
+ else:
262
+ item[parsed_data.function][int(parsed_data.parsed_states[1])] = int(parsed_data.parsed_states[2])
263
+
264
+
265
+ except asyncio.TimeoutError:
266
+ _LOGGER.debug("Sending heartbeat to keep TCP connection alive...")
267
+ await self._connection.write("\n")
268
+
269
+ except asyncio.CancelledError:
270
+ _LOGGER.info("Stopping Transport loop...")
271
+
272
+ await self._connection.close()
273
+
274
+ break
275
+
276
+ except Exception as err:
277
+ _LOGGER.error("Connection ERROR(%s): %s. Next connection attempt in 3s", self._target, err)
278
+
279
+ await self._connection.close()
280
+ await asyncio.sleep(3)
281
+
282
+ async def set_led(
283
+ self,
284
+ id: int,
285
+ pin: int,
286
+ level: int,
287
+ ) -> None:
288
+ attempts = 0
289
+ while attempts < 10:
290
+ await self.write(f"SetLED={id},{pin},{level}")
291
+ await self.write(f"StateLED={id},{pin}")
292
+
293
+ await asyncio.sleep(attempts * 0.1)
294
+ attempts += 1
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: gryfsmartio
3
+ Version: 0.1
4
+ Summary: Library for the GryfSmart system
5
+ Home-page: https://github.com/karlowiczpl/gryfsmartio
6
+ Author: @karlowiczpl
7
+ Author-email: kkarlowicz@gryfsmart.pl
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENCE
14
+ Requires-Dist: pyserial-asyncio==0.6
15
+ Dynamic: license-file
@@ -0,0 +1,11 @@
1
+ LICENCE
2
+ pyproject.toml
3
+ setup.cfg
4
+ gryfsmartio/__init__.py
5
+ gryfsmartio/parsing.py
6
+ gryfsmartio/transport.py
7
+ gryfsmartio.egg-info/PKG-INFO
8
+ gryfsmartio.egg-info/SOURCES.txt
9
+ gryfsmartio.egg-info/dependency_links.txt
10
+ gryfsmartio.egg-info/requires.txt
11
+ gryfsmartio.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ pyserial-asyncio==0.6
@@ -0,0 +1 @@
1
+ gryfsmartio
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,24 @@
1
+ [metadata]
2
+ name = gryfsmartio
3
+ version = 0.1
4
+ author = @karlowiczpl
5
+ author_email = kkarlowicz@gryfsmart.pl
6
+ description = Library for the GryfSmart system
7
+ long_description = file: README.md
8
+ long_description_content_type = text/markdown
9
+ url = https://github.com/karlowiczpl/gryfsmartio
10
+ license = MIT
11
+ classifiers =
12
+ Programming Language :: Python :: 3
13
+ Operating System :: OS Independent
14
+
15
+ [options]
16
+ packages = find:
17
+ python_requires = >=3.8
18
+ install_requires =
19
+ pyserial-asyncio==0.6
20
+
21
+ [egg_info]
22
+ tag_build =
23
+ tag_date = 0
24
+