xaal.lib 0.7.2__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.
xaal/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __path__ = __import__('pkgutil').extend_path(__path__, __name__)
xaal/lib/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+
2
+
3
+ # Load main class & modules.
4
+
5
+
6
+ from . import tools
7
+ from . import config
8
+ from . import bindings
9
+ from . import aiohelpers as helpers
10
+
11
+ from .core import Timer
12
+
13
+ # sync engine
14
+ from .engine import Engine
15
+ from .network import NetworkConnector
16
+
17
+ # async engine
18
+ from .aioengine import AsyncEngine
19
+ from .aionetwork import AsyncNetworkConnector
20
+
21
+ from .devices import Device, Attribute, Attributes
22
+ from .messages import Message,MessageFactory,MessageType
23
+ from .exceptions import *
xaal/lib/__main__.py ADDED
@@ -0,0 +1,2 @@
1
+ from . import test
2
+ test.run()
xaal/lib/aioengine.py ADDED
@@ -0,0 +1,388 @@
1
+ import asyncio
2
+
3
+ from . import core
4
+ from . import config
5
+ from . import tools
6
+ from .messages import MessageParserError
7
+ from .aionetwork import AsyncNetworkConnector
8
+ from .exceptions import *
9
+
10
+ import time
11
+ from enum import Enum
12
+ import aioconsole
13
+ import signal
14
+ import sys
15
+ from tabulate import tabulate
16
+ from pprint import pprint
17
+
18
+
19
+ import logging
20
+ logger = logging.getLogger(__name__)
21
+
22
+ class AsyncEngine(core.EngineMixin):
23
+
24
+ __slots__ = ['__txFifo','_loop','_tasks','_hooks','_watchdog_task','_kill_counter','running_event','watchdog_event','started_event']
25
+
26
+ def __init__(self,address=config.address,port=config.port,hops=config.hops,key=config.key):
27
+ core.EngineMixin.__init__(self,address,port,hops,key)
28
+
29
+ self.__txFifo = asyncio.Queue() # tx msg fifo
30
+ self._loop = None # event loop
31
+ self._hooks = [] # hooks
32
+ self._tasks = [] # tasks
33
+ self._watchdog_task = None # watchdog task
34
+ self._kill_counter = 0 # watchdog counter
35
+
36
+ self.started_event = asyncio.Event() # engine started event
37
+ self.running_event = asyncio.Event() # engine running event
38
+ self.watchdog_event = asyncio.Event() # watchdog event
39
+
40
+ signal.signal(signal.SIGTERM, self.sigkill_handler)
41
+ signal.signal(signal.SIGINT, self.sigkill_handler)
42
+
43
+ # message receive workflow
44
+ self.subscribe(self.handle_request)
45
+ # start network
46
+ self.network = AsyncNetworkConnector(address, port, hops)
47
+
48
+ #####################################################
49
+ # Hooks
50
+ #####################################################
51
+ def on_start(self,func,*args,**kwargs):
52
+ hook = Hook(HookType.start,func,*args,**kwargs)
53
+ self._hooks.append(hook)
54
+
55
+ def on_stop(self,func,*args,**kwargs):
56
+ hook = Hook(HookType.stop,func,*args,**kwargs)
57
+ self._hooks.append(hook)
58
+
59
+ async def run_hooks(self,hook_type):
60
+ hooks = list(filter(lambda hook: hook.type==hook_type,self._hooks))
61
+ if len(hooks)!=0:
62
+ logger.debug(f"Launching {hook_type} hooks")
63
+ for h in hooks:
64
+ await run_func(h.func,*h.args,**h.kwargs)
65
+
66
+ #####################################################
67
+ # timers
68
+ #####################################################
69
+ async def process_timers(self):
70
+ """Process all timers to find out which ones should be run"""
71
+ expire_list = []
72
+ now = time.time()
73
+ for t in self.timers:
74
+ if t.deadline < now:
75
+ try:
76
+ await run_func(t.func)
77
+ except CallbackError as e:
78
+ logger.error(e.description)
79
+ if t.counter != -1:
80
+ t.counter-= 1
81
+ if t.counter == 0:
82
+ expire_list.append(t)
83
+ t.deadline = now + t.period
84
+ # delete expired timers
85
+ for t in expire_list:
86
+ self.remove_timer(t)
87
+
88
+ #####################################################
89
+ # msg send / receive
90
+ #####################################################
91
+ def queue_msg(self, msg):
92
+ """queue a message"""
93
+ self.__txFifo.put_nowait(msg)
94
+
95
+ def send_msg(self, msg):
96
+ """Send an encoded message to the bus, use queue_msg instead"""
97
+ self.network.send(msg)
98
+
99
+ async def receive_msg(self):
100
+ """return new received message or None"""
101
+ data = await self.network.get_data()
102
+ if data:
103
+ try:
104
+ msg = self.msg_factory.decode_msg(data,self.msg_filter)
105
+ except MessageParserError as e:
106
+ logger.warning(e)
107
+ msg = None
108
+ return msg
109
+ return None
110
+
111
+ async def process_rx_msg(self):
112
+ """process incomming messages"""
113
+ msg = await self.receive_msg()
114
+ if msg:
115
+ for func in self.subscribers:
116
+ await run_func(func,msg)
117
+ self.process_attributes_change()
118
+
119
+ def handle_request(self, msg):
120
+ """Filter msg for devices according default xAAL API then process the
121
+ request for each targets identied in the engine
122
+ """
123
+ if not msg.is_request():
124
+ return
125
+ targets = core.filter_msg_for_devices(msg, self.devices)
126
+ for target in targets:
127
+ if msg.action == 'is_alive':
128
+ self.send_alive(target)
129
+ else:
130
+ self.new_task(self.handle_action_request(msg, target))
131
+
132
+ async def handle_action_request(self, msg, target):
133
+ try:
134
+ result = await run_action(msg, target)
135
+ if result != None:
136
+ self.send_reply(dev=target,targets=[msg.source],action=msg.action,body=result)
137
+ except CallbackError as e:
138
+ self.send_error(target, e.code, e.description)
139
+ except XAALError as e:
140
+ logger.error(e)
141
+
142
+ #####################################################
143
+ # Asyncio loop & Tasks
144
+ #####################################################
145
+ def get_loop(self):
146
+ if self._loop == None:
147
+ logger.debug('New event loop')
148
+ self._loop = asyncio.get_event_loop()
149
+ return self._loop
150
+
151
+ def new_task(self,coro,name=None):
152
+ # we maintain a task list, to be able to stop/start the engine
153
+ # on demand. needed by HASS
154
+ task = self.get_loop().create_task(coro,name=name)
155
+ self._tasks.append(task)
156
+ task.add_done_callback(self.task_callback)
157
+ return task
158
+
159
+ def task_callback(self, task):
160
+ # called when a task ended
161
+ self._tasks.remove(task)
162
+
163
+ def all_tasks(self):
164
+ return self._tasks
165
+
166
+ async def boot_task(self):
167
+ self.watchdog_event.clear()
168
+ # queue the alive before anything
169
+ for dev in self.devices:
170
+ self.send_alive(dev)
171
+ await self.network.connect()
172
+ self.running_event.set()
173
+ await self.run_hooks(HookType.start)
174
+
175
+ async def receive_task(self):
176
+ await self.running_event.wait()
177
+ while self.is_running():
178
+ await self.process_rx_msg()
179
+
180
+ async def send_task(self):
181
+ await self.running_event.wait()
182
+ while self.is_running():
183
+ temp = await self.__txFifo.get()
184
+ self.send_msg(temp)
185
+
186
+ async def timer_task(self):
187
+ await self.running_event.wait()
188
+ self.setup_alives_timer()
189
+ while self.is_running():
190
+ await asyncio.sleep(0.2)
191
+ await self.process_timers()
192
+ self.process_attributes_change()
193
+
194
+ async def watchdog_task(self):
195
+ await self.watchdog_event.wait()
196
+ await self.stop()
197
+ logger.info('Exit')
198
+
199
+ #####################################################
200
+ # start / stop / shutdown
201
+ #####################################################
202
+ def is_running(self):
203
+ return self.running_event.is_set()
204
+
205
+ def start(self):
206
+ if self.is_running():
207
+ logger.warning('Engine already started')
208
+ return
209
+ self.started_event.set()
210
+ self.new_task(self.boot_task(),name='Boot')
211
+ self.new_task(self.receive_task(),name='RecvQ')
212
+ self.new_task(self.send_task(),name='SendQ')
213
+ self.new_task(self.timer_task(),name='Timers')
214
+ self.new_task(console(locals()),name='Console')
215
+
216
+ def setup_alives_timer(self):
217
+ # needed on stop-start sequence
218
+ if self.process_alives in [t.func for t in self.timers]:
219
+ return
220
+ # process alives every 10 seconds
221
+ self.add_timer(self.process_alives,10)
222
+
223
+ async def stop(self):
224
+ logger.info('Stopping engine')
225
+ await self.run_hooks(HookType.stop)
226
+ self.running_event.clear()
227
+ self.started_event.clear()
228
+ # cancel all tasks
229
+ for task in self.all_tasks():
230
+ if task!=self._watchdog_task:
231
+ task.cancel()
232
+ await asyncio.sleep(0.1)
233
+
234
+ def sigkill_handler(self,signal,frame):
235
+ print("", end = "\r") #remove the uggly ^C
236
+ if not self.is_running():
237
+ logger.warning('Engine already stopped')
238
+ self._kill_counter = 1
239
+ self._kill_counter +=1
240
+ self.shutdown()
241
+ if self._kill_counter > 1:
242
+ logger.warning('Force quit')
243
+ sys.exit(-1)
244
+ else:
245
+ logger.warning('Kill requested')
246
+
247
+ def shutdown(self):
248
+ self.watchdog_event.set()
249
+
250
+ def run(self):
251
+ if not self.started_event.is_set():
252
+ self.start()
253
+ if self._watchdog_task == None:
254
+ # start the watchdog task
255
+ self._watchdog_task = self.new_task(self.watchdog_task(),name='Watchdog task')
256
+ self.get_loop().run_until_complete(self._watchdog_task)
257
+ else:
258
+ logger.warning('Engine already running')
259
+
260
+ #####################################################
261
+ # Debugging tools
262
+ #####################################################
263
+ def dump_timers(self):
264
+ headers = ['Func','Period','Counter','Deadline']
265
+ rows = []
266
+ now = time.time()
267
+ for t in self.timers:
268
+ remain = round(t.deadline-now,1)
269
+ rows.append([str(t.func),t.period,t.counter,remain])
270
+ print('= Timers')
271
+ print(tabulate(rows,headers=headers,tablefmt="fancy_grid"))
272
+
273
+
274
+ def dump_tasks(self):
275
+ headers = ["Name","Coro","Loop ID"]
276
+ rows = []
277
+ for t in self.all_tasks():
278
+ rows.append([t.get_name(),str(t.get_coro()),id(t.get_loop())])
279
+ print('= Tasks')
280
+ print(tabulate(rows,headers=headers,tablefmt="fancy_grid"))
281
+
282
+ def dump_devices(self):
283
+ headers = ["addr","dev_type","info"]
284
+ rows = []
285
+ for d in self.devices:
286
+ rows.append([d.address,d.dev_type,d.info])
287
+ print('= Devices')
288
+ print(tabulate(rows,headers=headers,tablefmt="fancy_grid"))
289
+
290
+ def dump_hooks(self):
291
+ headers = ["Type","Hook"]
292
+ rows = []
293
+ for h in self._hooks:
294
+ rows.append([h.type,str(h.func)])
295
+ print('= Hooks')
296
+ print(tabulate(rows,headers=headers,tablefmt="fancy_grid"))
297
+
298
+ def dump(self):
299
+ self.dump_devices()
300
+ self.dump_tasks()
301
+ self.dump_timers()
302
+ self.dump_hooks()
303
+
304
+ def get_device(self,uuid):
305
+ uuid = tools.get_uuid(uuid)
306
+ for dev in self.devices:
307
+ if dev.address == uuid:
308
+ return dev
309
+ return None
310
+
311
+
312
+ #####################################################
313
+ # Utilities functions
314
+ #####################################################
315
+ async def run_func(func,*args,**kwargs):
316
+ """run a function or a coroutine function """
317
+ if asyncio.iscoroutinefunction(func):
318
+ return await func(*args,**kwargs)
319
+ else:
320
+ return func(*args,**kwargs)
321
+
322
+
323
+ async def run_action(msg,device):
324
+ """
325
+ Extract an action & launch it
326
+ Return:
327
+ - action result
328
+ - None if no result
329
+
330
+ Notes:
331
+ - If an exception raised, it's logged, and raise an XAALError.
332
+ - Same API as legacy Engine, but accept coroutine functions
333
+ """
334
+ method,params = core.search_action(msg,device)
335
+ result = None
336
+ try:
337
+ if asyncio.iscoroutinefunction(method):
338
+ result = await method(**params)
339
+ else:
340
+ result = method(**params)
341
+ except Exception as e:
342
+ logger.error(e)
343
+ raise XAALError("Error in method:%s params:%s" % (msg.action,params))
344
+ return result
345
+
346
+
347
+ #####################################################
348
+ # Hooks
349
+ #####################################################
350
+ class HookType(Enum):
351
+ start = 0
352
+ stop = 1
353
+
354
+ class Hook(object):
355
+ __slots__ = ['type','func','args','kwargs']
356
+ def __init__(self,type_,func,*args,**kwargs):
357
+ self.type = type_
358
+ self.func = func
359
+ self.args = args
360
+ self.kwargs = kwargs
361
+
362
+
363
+ #####################################################
364
+ # Debugging console
365
+ #####################################################
366
+ async def console(locals=locals(),port=None):
367
+ """launch a console to enable remote engine inspection"""
368
+ if port == None:
369
+ # let's find a free port if not specified
370
+ def find_free_port():
371
+ import socketserver
372
+ with socketserver.TCPServer(("localhost", 0), None) as s:
373
+ return s.server_address[1]
374
+ port = find_free_port()
375
+
376
+ logger.debug(f'starting debug console on port {port}')
377
+ sys.ps1 = '[xAAL] >>> '
378
+ banner = '=' * 78 +"\nxAAL remote console\n" + '=' *78
379
+ locals.update({'pprint':pprint})
380
+
381
+ def factory(streams):
382
+ return aioconsole.AsynchronousConsole(locals=locals, streams=streams)
383
+ # start the console
384
+ try:
385
+ # debian with ipv6 disabled still state that localhost is ::1, which broke aioconsole
386
+ await aioconsole.start_interactive_server(host='127.0.0.1', port=port,factory=factory,banner=banner)
387
+ except OSError:
388
+ logger.warning('Unable to run console')
xaal/lib/aiohelpers.py ADDED
@@ -0,0 +1,38 @@
1
+ from . helpers import *
2
+ from decorator import decorator
3
+ import asyncio
4
+ import logging
5
+
6
+ @decorator
7
+ def spawn(func,*args,**kwargs):
8
+ return asyncio.get_event_loop().run_in_executor(None,func,*args,**kwargs)
9
+
10
+
11
+ def static_vars(**kwargs_):
12
+ def decorate(func):
13
+ for k in kwargs_:
14
+ setattr(func, k, kwargs_[k])
15
+ return func
16
+ return decorate
17
+
18
+
19
+ def run_async_package(pkg_name,pkg_setup,console_log = True,file_log=False):
20
+ if console_log:
21
+ set_console_title(pkg_name)
22
+ setup_console_logger()
23
+ if file_log:
24
+ setup_file_logger(pkg_name)
25
+
26
+ from .aioengine import AsyncEngine
27
+ eng = AsyncEngine()
28
+ eng.start()
29
+ logger = logging.getLogger(pkg_name)
30
+ logger.info('starting xaal package: %s'% pkg_name )
31
+ result = pkg_setup(eng)
32
+ if result != True:
33
+ logger.critical("something goes wrong with package: %s" % pkg_name)
34
+ try:
35
+ eng.run()
36
+ except KeyboardInterrupt:
37
+ eng.shutdown()
38
+ logger.info("Exit")
xaal/lib/aionetwork.py ADDED
@@ -0,0 +1,74 @@
1
+ import asyncio
2
+ import struct
3
+ import socket
4
+
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class AsyncNetworkConnector(object):
10
+
11
+ def __init__(self, addr, port, hops,bind_addr='0.0.0.0'):
12
+ self.addr = addr
13
+ self.port = port
14
+ self.hops = hops
15
+ self.bind_addr = bind_addr
16
+ self._rx_queue = asyncio.Queue()
17
+
18
+ async def connect(self):
19
+ loop = asyncio.get_running_loop()
20
+ on_con_lost = loop.create_future()
21
+ self.transport, self.protocol = await loop.create_datagram_endpoint(
22
+ lambda: XAALServerProtocol(on_con_lost,self.receive), sock = self.new_sock())
23
+ # In some conditions (containers), transport is connected but IGMP is delayed (up to 10ms)
24
+ # so we need to wait for IGMP to be really sent.
25
+ await asyncio.sleep(0.05)
26
+
27
+ def new_sock(self):
28
+ sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP)
29
+ try:
30
+ # Linux + MacOS + BSD
31
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
32
+ except:
33
+ # Windows
34
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
35
+ sock.bind((self.bind_addr, self.port))
36
+ mreq = struct.pack("=4s4s",socket.inet_aton(self.addr),socket.inet_aton(self.bind_addr))
37
+ sock.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP,mreq)
38
+ sock.setsockopt(socket.IPPROTO_IP,socket.IP_MULTICAST_TTL,10)
39
+ sock.setblocking(False)
40
+ return sock
41
+
42
+ def send(self,data):
43
+ self.protocol.datagram_send(data,self.addr,self.port)
44
+
45
+ def receive(self,data):
46
+ self._rx_queue.put_nowait(data)
47
+
48
+ async def get_data(self):
49
+ return await self._rx_queue.get()
50
+
51
+ class XAALServerProtocol(asyncio.Protocol):
52
+ def __init__(self,on_con_lost,on_dtg_recv):
53
+ self.on_con_lost = on_con_lost
54
+ self.on_dtg_recv = on_dtg_recv
55
+
56
+ def connection_made(self, transport):
57
+ logger.info("xAAL network connected")
58
+ self.transport = transport
59
+
60
+ def error_received(self, exc):
61
+ print('Error received:', exc)
62
+ logger.warning(f"Error received: {exc}")
63
+
64
+ def connection_lost(self, exc):
65
+ logger.info(f"Connexion closed: {exc}")
66
+ self.on_con_lost.set_result(True)
67
+
68
+ def datagram_send(self,data,ip,port):
69
+ self.transport.sendto(data,(ip,port))
70
+
71
+ def datagram_received(self, data, addr):
72
+ #print(f"pkt from {addr}")
73
+ self.on_dtg_recv(data)
74
+
xaal/lib/bindings.py ADDED
@@ -0,0 +1,98 @@
1
+ import uuid
2
+
3
+ from .exceptions import UUIDError
4
+
5
+ class UUID:
6
+ def __init__(self,*args,**kwargs):
7
+ self.__uuid = uuid.UUID(*args,**kwargs)
8
+
9
+ @staticmethod
10
+ def random_base(digit=2):
11
+ """zeros the last digits of a random uuid, usefull w/ you want to forge some addresses
12
+ two digit is great.
13
+ """
14
+ if (digit > 0) and (digit < 13):
15
+ tmp = str(uuid.uuid1())
16
+ st = "%s%s" % (tmp[:-digit],'0'*digit)
17
+ return UUID(st)
18
+ else:
19
+ raise UUIDError
20
+
21
+ @staticmethod
22
+ def random():
23
+ tmp = uuid.uuid1().int
24
+ return UUID(int=tmp)
25
+
26
+ def __add__(self,value):
27
+ tmp = self.__uuid.int + value
28
+ return UUID(int=tmp)
29
+
30
+ def __sub__(self,value):
31
+ tmp = self.__uuid.int - value
32
+ return UUID(int=tmp)
33
+
34
+ def __eq__(self,value):
35
+ return self.__uuid == value
36
+
37
+ def __lt__(self, value ):
38
+ return self.__uuid.int < value
39
+
40
+ def __gt__(self, value ):
41
+ return self.__uuid.int > value
42
+
43
+ def __str__(self):
44
+ return str(self.__uuid)
45
+
46
+ def __repr__(self): # pragma: no cover
47
+ return f"UUID('{self.__uuid}')"
48
+
49
+ def __hash__(self):
50
+ return self.__uuid.__hash__()
51
+
52
+ def get(self):
53
+ return self.__uuid
54
+
55
+ def set(self,value):
56
+ self.__uuid = value
57
+
58
+ @property
59
+ def str(self):
60
+ return str(self)
61
+
62
+ @property
63
+ def bytes(self):
64
+ return self.__uuid.bytes
65
+
66
+
67
+
68
+ class URL:
69
+ def __init__(self,value):
70
+ self.__url = value
71
+
72
+ def __eq__(self,value):
73
+ return self.__url == value
74
+
75
+ def __str__(self):
76
+ return str(self.__url)
77
+
78
+ def __repr__(self): # pragma: no cover
79
+ return f"URL('{self.__url}')"
80
+
81
+ def set(self,value):
82
+ self.__url = value
83
+
84
+ def get(self):
85
+ return self.__url
86
+
87
+ @property
88
+ def str(self):
89
+ return str(self)
90
+
91
+ @property
92
+ def bytes(self):
93
+ return self.__url
94
+
95
+
96
+
97
+ classes = [UUID,URL]
98
+
xaal/lib/cbor.py ADDED
@@ -0,0 +1,62 @@
1
+ from io import BytesIO
2
+ import cbor2
3
+ from cbor2 import CBORTag
4
+ # from cbor2 import CBORDecoder
5
+ from cbor2.decoder import CBORDecoder
6
+
7
+ from . import bindings
8
+
9
+ # ugly patch to remove default UUID decodeur
10
+ cbor2.decoder.semantic_decoders.pop(37)
11
+
12
+
13
+ def tag_hook(decoder, tag, shareable_index=None):
14
+ if tag.tag == 37:
15
+ return bindings.UUID(bytes=tag.value)
16
+ if tag.tag == 32:
17
+ return bindings.URL(tag.value)
18
+ return tag
19
+
20
+ def default_encoder(encoder, value):
21
+ if isinstance(value,bindings.UUID):
22
+ encoder.encode(CBORTag(37, value.bytes))
23
+
24
+ if isinstance(value,bindings.URL):
25
+ encoder.encode(CBORTag(32, value.bytes))
26
+
27
+ def dumps(obj, **kwargs):
28
+ return cbor2.dumps(obj,default=default_encoder,**kwargs)
29
+
30
+ def loads(payload, **kwargs):
31
+ #return cbor2.loads(payload,tag_hook=tag_hook,**kwargs)
32
+ return _loads(payload,tag_hook=tag_hook,**kwargs)
33
+
34
+ def _loads(s, **kwargs):
35
+ with BytesIO(s) as fp:
36
+ return CBORDecoder(fp, **kwargs).decode()
37
+
38
+ #class CustomDecoder(CBORDecoder):pass
39
+
40
+
41
+ def cleanup(obj):
42
+ """
43
+ recursive walk a object to search for un-wanted CBOR tags.
44
+ Transform this tag in string format, this can be UUID, URL..
45
+ Should be Ok, with list, dicts..
46
+ Warning: This operate in-place changes.
47
+ Warning: This won't work for tags in dict keys.
48
+ """
49
+ if isinstance(obj,list):
50
+ for i in range(0,len(obj)):
51
+ obj[i] = cleanup(obj[i])
52
+ return obj
53
+
54
+ if isinstance(obj,dict):
55
+ for k in obj.keys():
56
+ obj.update({k:cleanup(obj[k])})
57
+ return obj
58
+
59
+ if type(obj) in bindings.classes:
60
+ return str(obj)
61
+ else:
62
+ return obj