indipyserver 0.0.1__py3-none-any.whl

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,9 @@
1
+
2
+ import sys
3
+
4
+ from .ipyserver import IPyServer
5
+
6
+ if sys.version_info < (3, 11):
7
+ raise ImportError('indipyserver requires Python >= 3.11')
8
+
9
+ version = "0.0.1"
@@ -0,0 +1,314 @@
1
+
2
+ import asyncio, copy
3
+
4
+ import xml.etree.ElementTree as ET
5
+
6
+ import logging
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ # All xml data sent from the driver should be contained in one of the following tags
11
+ TAGS = (b'message',
12
+ b'delProperty',
13
+ b'defSwitchVector',
14
+ b'setSwitchVector',
15
+ b'defLightVector',
16
+ b'setLightVector',
17
+ b'defTextVector',
18
+ b'setTextVector',
19
+ b'defNumberVector',
20
+ b'setNumberVector',
21
+ b'defBLOBVector',
22
+ b'setBLOBVector',
23
+ b'getProperties' # for snooping
24
+ )
25
+
26
+ # Note these are strings, as they are used for checking xmldata.tag values
27
+
28
+ DEFTAGS = ( 'defSwitchVector',
29
+ 'defLightVector',
30
+ 'defTextVector',
31
+ 'defNumberVector',
32
+ 'defBLOBVector'
33
+ )
34
+
35
+
36
+ NEWTAGS = ('newTextVector',
37
+ 'newNumberVector',
38
+ 'newSwitchVector',
39
+ 'newBLOBVector'
40
+ )
41
+
42
+ # _STARTTAGS is a tuple of ( b'<defTextVector', ... ) data received will be tested to start with such a starttag
43
+ _STARTTAGS = tuple(b'<' + tag for tag in TAGS)
44
+
45
+ # _ENDTAGS is a tuple of ( b'</defTextVector>', ... ) data received will be tested to end with such an endtag
46
+ _ENDTAGS = tuple(b'</' + tag + b'>' for tag in TAGS)
47
+
48
+
49
+ class ExVector:
50
+
51
+ def __init__(self, name, deftag):
52
+ "Object which only holds a name and vectortype"
53
+ self.name = name
54
+ self.vectortype = deftag[3:]
55
+
56
+
57
+ class ExDriver:
58
+
59
+ def __init__(self, program, *args, debug_enable=False):
60
+ "Executes a third party indi driver, communicates by stdin stdout"
61
+
62
+ self.program = program
63
+ self.args = args
64
+ self.debug_enable = debug_enable
65
+ self.proc = None
66
+
67
+ # An object for communicating is set when this driver is added to the server
68
+ self._commsobj = None
69
+
70
+ # This dictionary will be populated with devicename:{vectorname:vector}
71
+ # where vector is an ExVector object
72
+ self.devicenames = {}
73
+
74
+ self.snoopall = False # gets set to True if it is snooping everything
75
+ self.snoopdevices = set() # gets set to a set of device names
76
+ self.snoopvectors = set() # gets set to a set of (devicename,vectorname) tuples
77
+
78
+ self._remainder = b"" # Used to store intermediate data
79
+ self._stop = False # Gets set to True to stop communications
80
+
81
+
82
+ def shutdown(self):
83
+ self._stop = True
84
+ if self._commsobj is not None:
85
+ self._commsobj.shutdown()
86
+ if self.proc is not None:
87
+ self.proc.terminate()
88
+
89
+
90
+ def __contains__(self, item):
91
+ "So a devicename can easily be checked if it is in this driver"
92
+ return item in self.devicenames
93
+
94
+
95
+ async def _readdata(self, root):
96
+ """Called from communications object with received xmldata
97
+ and sends it towards the driver"""
98
+
99
+ if self._stop:
100
+ return
101
+
102
+ if root.tag == "enableBLOB":
103
+ return
104
+
105
+ if root.tag == "getProperties":
106
+ await self.xml_to_ext(root)
107
+ return
108
+
109
+ devicename = root.get("device")
110
+ propertyname = root.get("name")
111
+
112
+ if root.tag in NEWTAGS:
113
+ if devicename is None:
114
+ # invalid
115
+ return
116
+ if devicename in self.devicenames:
117
+ await self.xml_to_ext(root)
118
+ return
119
+ else:
120
+ # not for this driver
121
+ return
122
+
123
+ # so not in NEWTAGS
124
+ # Check if this driver is snooping on this device/vector
125
+ if self.snoopall:
126
+ await self.xml_to_ext(root)
127
+ elif devicename and (devicename in self.snoopdevices):
128
+ await self.xml_to_ext(root)
129
+ elif devicename and propertyname and ((devicename, propertyname) in self.snoopvectors):
130
+ await self.xml_to_ext(root)
131
+
132
+
133
+ async def xml_to_ext(self, root):
134
+ "Pass data to the external driver"
135
+
136
+ binarydata = ET.tostring(root)
137
+
138
+ # log the received data
139
+ if logger.isEnabledFor(logging.DEBUG) and self.debug_enable:
140
+ logger.debug(f"RX:: {binarydata.decode('utf-8')}")
141
+
142
+ binarydata += b"\n"
143
+ self.proc.stdin.write(binarydata)
144
+ await self.proc.stdin.drain()
145
+
146
+
147
+
148
+ async def _run_tx(self):
149
+ "Get data from driver and send it towards the server"
150
+ try:
151
+ # get block of xml.etree.ElementTree data
152
+ # from self._xmlinput and append it to self.writerque
153
+ while not self._stop:
154
+ txdata = await self._xmlinput()
155
+ if txdata is None:
156
+ return
157
+ # get block of xml.etree.ElementTree data
158
+ devicename = txdata.get("device")
159
+ if devicename and txdata.tag in DEFTAGS:
160
+ # its a definition
161
+ if devicename not in self.devicenames:
162
+ self.devicenames[devicename] = {}
163
+ vectorname = txdata.get("name")
164
+ if vectorname:
165
+ if vectorname not in self.devicenames[devicename]:
166
+ # add this vector to the self.devicenames[devicename] dictionary
167
+ self.devicenames[devicename][vectorname] = ExVector(vectorname, txdata.tag)
168
+ # check for a getProperties being sent, record what is being snooped
169
+ if txdata.tag == "getProperties":
170
+ vectorname = txdata.get("name")
171
+ if devicename is None:
172
+ self.snoopall = True
173
+ elif vectorname is None:
174
+ self.snoopdevices.add(devicename)
175
+ else:
176
+ self.snoopvectors.add((devicename,vectorname))
177
+
178
+ # transmit this data towards the server
179
+ await self._commsobj.run_tx(txdata)
180
+
181
+ if logger.isEnabledFor(logging.DEBUG) and self.debug_enable:
182
+ binarydata = ET.tostring(txdata)
183
+ logger.debug(f"TX:: {binarydata.decode('utf-8')}")
184
+
185
+ except Exception:
186
+ logger.exception("Exception report from ExDriver._run_tx")
187
+ raise
188
+
189
+
190
+ async def _xmlinput(self):
191
+ """get data from driver, parse it, and return it as xml.etree.ElementTree object
192
+ Returns None if stop flags arises"""
193
+ message = b''
194
+ messagetagnumber = None
195
+ while not self._stop:
196
+ data = await self._datainput()
197
+ # data is either None, or binary data ending in b">"
198
+ if data is None:
199
+ return
200
+ if self._stop:
201
+ return
202
+ if not message:
203
+ # data is expected to start with <tag, first strip any newlines
204
+ data = data.strip()
205
+ for index, st in enumerate(_STARTTAGS):
206
+ if data.startswith(st):
207
+ messagetagnumber = index
208
+ break
209
+ elif st in data:
210
+ # remove any data prior to a starttag
211
+ positionofst = data.index(st)
212
+ data = data[positionofst:]
213
+ messagetagnumber = index
214
+ break
215
+ else:
216
+ # data does not start with a recognised tag, so ignore it
217
+ # and continue waiting for a valid message start
218
+ continue
219
+ # set this data into the received message
220
+ message = data
221
+ # either further children of this tag are coming, or maybe its a single tag ending in "/>"
222
+ if message.endswith(b'/>'):
223
+ # the message is complete, handle message here
224
+ try:
225
+ root = ET.fromstring(message.decode("us-ascii"))
226
+ except Exception:
227
+ # failed to parse the message, continue at beginning
228
+ message = b''
229
+ messagetagnumber = None
230
+ continue
231
+ # xml datablock done, return it
232
+ return root
233
+ # and read either the next message, or the children of this tag
234
+ continue
235
+ # To reach this point, the message is in progress, with a messagetagnumber set
236
+ # keep adding the received data to message, until an endtag is reached
237
+ message += data
238
+ if message.endswith(_ENDTAGS[messagetagnumber]):
239
+ # the message is complete, handle message here
240
+ try:
241
+ root = ET.fromstring(message.decode("us-ascii"))
242
+ except Exception:
243
+ # failed to parse the message, continue at beginning
244
+ message = b''
245
+ messagetagnumber = None
246
+ continue
247
+ # xml datablock done, return it
248
+ return root
249
+ # so message is in progress, with a messagetagnumber set
250
+ # but no valid endtag received yet, so continue the loop
251
+
252
+
253
+ async def _datainput(self):
254
+ """Waits for binary string of data ending in > from the driver
255
+ Returns None if stop flags arises"""
256
+ remainder = self._remainder
257
+ if b">" in remainder:
258
+ # This returns with binary data ending in > as long
259
+ # as there are > characters in self._remainder
260
+ binarydata, self._remainder = remainder.split(b'>', maxsplit=1)
261
+ binarydata += b">"
262
+ return binarydata
263
+ # As soon as there are no > characters left in self._remainder
264
+ # get more data from the driver
265
+ while not self._stop:
266
+ await asyncio.sleep(0)
267
+ indata = await self.proc.stdout.read(100)
268
+ if not indata:
269
+ await asyncio.sleep(0.02)
270
+ continue
271
+ remainder += indata
272
+ if b">" in indata:
273
+ binarydata, self._remainder = remainder.split(b'>', maxsplit=1)
274
+ binarydata += b">"
275
+ return binarydata
276
+
277
+
278
+ async def _run_err(self):
279
+ """gets binary string of data from exdriver proc.stderr
280
+ and logs it to logging.error."""
281
+
282
+ while not self._stop:
283
+ bindata = await self.proc.stderr.readline()
284
+ if not bindata:
285
+ await asyncio.sleep(0.02)
286
+ continue
287
+ logger.error(bindata.decode('utf-8'))
288
+
289
+
290
+ async def asyncrun(self):
291
+ "Runs the external driver"
292
+
293
+ self._stop = False
294
+
295
+ try:
296
+
297
+ self.proc = await asyncio.create_subprocess_exec(self.program,
298
+ *self.args,
299
+ stdin=asyncio.subprocess.PIPE,
300
+ stdout=asyncio.subprocess.PIPE,
301
+ stderr=asyncio.subprocess.PIPE)
302
+
303
+ # wait for external program to start
304
+ await asyncio.sleep(0.1)
305
+ # send a getProperties into the driver
306
+ await self._readdata( ET.fromstring("""<getProperties version="1.7" />""") )
307
+
308
+ async with asyncio.TaskGroup() as tg:
309
+ tg.create_task( self._run_tx() ) # Get data from driver and send it towards the server
310
+ tg.create_task( self._run_err() ) # data from exdriver proc.stderr and logs it to logging.error
311
+ except Exception:
312
+ logger.exception("Driver shutdown")
313
+ finally:
314
+ self._stop = True