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/lib/config.py ADDED
@@ -0,0 +1,53 @@
1
+
2
+ # Default configuration
3
+ import os
4
+ import sys
5
+ import binascii
6
+ from configobj import ConfigObj
7
+
8
+ self = sys.modules[__name__]
9
+
10
+ # Default settings
11
+ DEF_ADDR = '224.0.29.200' # mcast address
12
+ DEF_PORT = 1236 # mcast port
13
+ DEF_HOPS = 10 # mcast hop
14
+ DEF_ALIVE_TIMER = 100 # Time between two alive msg
15
+ DEF_CIPHER_WINDOW = 60 * 2 # Time Window in seconds to avoid replay attacks
16
+ DEF_QUEUE_SIZE = 10 # How many packet we can send in one loop
17
+ DEF_LOG_LEVEL = 'DEBUG' # should be INFO|DEBUG|None
18
+ DEF_LOG_PATH = '/var/log/xaal' # where log are
19
+
20
+ # TBD : Move this stuff
21
+ STACK_VERSION = 7
22
+
23
+
24
+ if 'XAAL_CONF_DIR' in os.environ:
25
+ self.conf_dir = os.environ['XAAL_CONF_DIR']
26
+ else:
27
+ self.conf_dir = os.path.expanduser("~") + '/.xaal'
28
+
29
+
30
+ def load_config(name='xaal.ini'):
31
+ filename = os.path.join(self.conf_dir, name)
32
+ if not os.path.isfile(filename):
33
+ print("Unable to load xAAL config file [%s]" % filename)
34
+ sys.exit(-1)
35
+
36
+ cfg = ConfigObj(filename)
37
+ self.address = cfg.get('address',DEF_ADDR)
38
+ self.port = int(cfg.get('port',DEF_PORT))
39
+ self.hops = int(cfg.get('hops',DEF_HOPS))
40
+ self.alive_timer = int(cfg.get('alive_timer',DEF_ALIVE_TIMER))
41
+ self.cipher_window = int(cfg.get('ciper_window',DEF_CIPHER_WINDOW))
42
+ self.queue_size = int(cfg.get('queue_size',DEF_QUEUE_SIZE))
43
+ self.log_level = cfg.get('log_level',DEF_LOG_LEVEL)
44
+ self.log_path = cfg.get('log_path',DEF_LOG_PATH)
45
+ key = cfg.get('key',None)
46
+
47
+ if key:
48
+ self.key = binascii.unhexlify(key.encode('utf-8'))
49
+ else:
50
+ print("Please set key in config file [%s]" % filename)
51
+ self.key = None
52
+
53
+ load_config()
xaal/lib/core.py ADDED
@@ -0,0 +1,308 @@
1
+ #
2
+ # Copyright 2014, Jérôme Colin, Jérôme Kerdreux, Philippe Tanguy,
3
+ # Telecom Bretagne.
4
+ #
5
+ # This file is part of xAAL.
6
+ #
7
+ # xAAL is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # xAAL is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with xAAL. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+
21
+ from .messages import MessageType, MessageAction, MessageFactory, ALIVE_ADDR
22
+ from .exceptions import *
23
+
24
+ import time
25
+ import inspect
26
+
27
+ import logging
28
+ logger = logging.getLogger(__name__)
29
+
30
+ class EngineMixin(object):
31
+
32
+ __slots__ = ['devices','timers','subscribers','msg_filter','_attributesChange','network','msg_factory']
33
+
34
+ def __init__(self,address,port,hops,key):
35
+ self.devices = [] # list of devices / use (un)register_devices()
36
+ self.timers = [] # functions to call periodic
37
+ self.subscribers = [] # message receive workflow
38
+ self.msg_filter = None # message filter
39
+
40
+ self._attributesChange = [] # list of XAALAttributes instances
41
+
42
+ # network connector
43
+ self.network = None
44
+ # start msg worker
45
+ self.msg_factory = MessageFactory(key)
46
+ # filter function activated
47
+ self.enable_msg_filter()
48
+
49
+ #####################################################
50
+ # Devices management
51
+ #####################################################
52
+ def add_device(self, dev):
53
+ """register a new device """
54
+ if dev not in self.devices:
55
+ self.devices.append(dev)
56
+ dev.engine = self
57
+ if self.is_running():
58
+ self.send_alive(dev)
59
+
60
+ def add_devices(self, devs):
61
+ """register new devices"""
62
+ for dev in devs:
63
+ self.add_device(dev)
64
+
65
+ def remove_device(self, dev):
66
+ """unregister a device """
67
+ dev.engine = None
68
+ # Remove dev from devices list
69
+ self.devices.remove(dev)
70
+
71
+ #####################################################
72
+ # xAAL messages Tx handling
73
+ #####################################################
74
+ # Fifo for msg to send
75
+ def queue_msg(self, msg):
76
+ logger.critical("To be implemented queue_msg: %s", msg)
77
+
78
+ def send_request(self,dev,targets,action,body = None):
79
+ """queue a new request"""
80
+ msg = self.msg_factory.build_msg(dev, targets, MessageType.REQUEST, action, body)
81
+ self.queue_msg(msg)
82
+
83
+ def send_reply(self, dev, targets, action, body=None):
84
+ """queue a new reply"""
85
+ msg = self.msg_factory.build_msg(dev, targets, MessageType.REPLY, action, body)
86
+ self.queue_msg(msg)
87
+
88
+ def send_error(self, dev, errcode, description=None):
89
+ """queue a error message"""
90
+ msg = self.msg_factory.build_error_msg(dev, errcode, description)
91
+ self.queue_msg(msg)
92
+
93
+ def send_get_description(self, dev, targets):
94
+ """queue a get_description request"""
95
+ self.send_request(dev, targets, MessageAction.GET_DESCRIPTION.value)
96
+
97
+ def send_get_attributes(self, dev, targets):
98
+ """queue a get_attributes request"""
99
+ self.send_request(dev, targets, MessageAction.GET_ATTRIBUTES.value)
100
+
101
+ def send_notification(self, dev, action, body=None):
102
+ """queue a notificaton"""
103
+ msg = self.msg_factory.build_msg(dev, [], MessageType.NOTIFY, action,body)
104
+ self.queue_msg(msg)
105
+
106
+ def send_alive(self, dev):
107
+ """Send a Alive message for a given device"""
108
+ timeout = dev.get_timeout()
109
+ msg = self.msg_factory.build_alive_for(dev, timeout)
110
+ self.queue_msg(msg)
111
+ dev.update_alive()
112
+
113
+ def send_is_alive(self, dev, targets=[ALIVE_ADDR,], dev_types=["any.any",]):
114
+ """Send a is_alive message, w/ dev_types filtering"""
115
+ body = {'dev_types': dev_types}
116
+ self.send_request(dev,targets, MessageAction.IS_ALIVE.value, body)
117
+
118
+
119
+ #####################################################
120
+ # Messages filtering
121
+ #####################################################
122
+ def enable_msg_filter(self, func=None):
123
+ """enable message filter"""
124
+ self.msg_filter = func or self.default_msg_filter
125
+
126
+ def disable_msg_filter(self):
127
+ """disable message filter"""
128
+ self.msg_filter = None
129
+
130
+ def default_msg_filter(self, msg):
131
+ """
132
+ Filter messages:
133
+ - check if message has alive request address
134
+ - check if the message is for us
135
+ return False, if message should be dropped
136
+ """
137
+ # Alive request
138
+ if ALIVE_ADDR in msg.targets:
139
+ return True
140
+ # Managed device ?
141
+ for dev in self.devices:
142
+ if dev.address in msg.targets:
143
+ return True
144
+ return False
145
+
146
+ #####################################################
147
+ # Alive messages
148
+ #####################################################
149
+ def process_alives(self):
150
+ """Periodic sending alive messages"""
151
+ now = time.time()
152
+ for dev in self.devices:
153
+ if dev.next_alive < now :
154
+ self.send_alive(dev)
155
+
156
+ #####################################################
157
+ # xAAL attributes changes
158
+ #####################################################
159
+ def add_attributes_change(self, attr):
160
+ """add a new attribute change to the list"""
161
+ self._attributesChange.append(attr)
162
+
163
+ def get_attributes_change(self):
164
+ """return the pending attributes changes list"""
165
+ return self._attributesChange
166
+
167
+ def process_attributes_change(self):
168
+ """Processes (send notify) attributes changes for all devices"""
169
+ devices = {}
170
+ # Group attributes changed by device
171
+ for attr in self.get_attributes_change():
172
+ if attr.device not in devices.keys():
173
+ devices[attr.device] = {}
174
+ devices[attr.device][attr.name] = attr.value
175
+
176
+ for dev in devices:
177
+ self.send_notification(dev, MessageAction.ATTRIBUTES_CHANGE.value, devices[dev])
178
+ self._attributesChange = [] # empty array
179
+
180
+ #####################################################
181
+ # xAAL messages subscribers
182
+ #####################################################
183
+ def subscribe(self,func):
184
+ self.subscribers.append(func)
185
+
186
+ def unsubscribe(self,func):
187
+ self.subscribers.remove(func)
188
+
189
+ #####################################################
190
+ # timers
191
+ #####################################################
192
+ def add_timer(self, func, period,counter=-1):
193
+ """
194
+ func: function to call
195
+ period: period in second
196
+ counter: number of repeat, -1 => always
197
+ """
198
+ if counter == 0:
199
+ raise EngineError("Timer counter should =-1 or >0")
200
+ t = Timer(func, period, counter)
201
+ self.timers.append(t)
202
+ return t
203
+
204
+ def remove_timer(self, timer):
205
+ """remove a given timer from the list"""
206
+ self.timers.remove(timer)
207
+
208
+ #####################################################
209
+ # start/stop/run API
210
+ #####################################################
211
+ def start(self):
212
+ logger.critical("To be implemented start")
213
+
214
+ def stop(self):
215
+ logger.critical("To be implemented stop")
216
+
217
+ def shutdown(self):
218
+ logger.critical("To be implemented shutdown")
219
+
220
+ def run(self):
221
+ logger.critical("To be implemented run")
222
+
223
+ def is_running(self):
224
+ logger.critical("To be implemented is_running")
225
+
226
+ #####################################################
227
+ # Timer class
228
+ #####################################################
229
+ class Timer(object):
230
+ def __init__(self, func, period, counter):
231
+ self.func = func
232
+ self.period = period
233
+ self.counter = counter
234
+ self.deadline = time.time() + period
235
+
236
+ #####################################################
237
+ # Usefull functions to Engine developpers
238
+ #####################################################
239
+ def filter_msg_for_devices(msg, devices):
240
+ """
241
+ loop throught the devices, to find which are expected w/ the msg
242
+ - Filter on dev_types for is_alive broadcast request.
243
+ - Filter on device address
244
+ """
245
+ results = []
246
+ if msg.is_request_isalive() and (ALIVE_ADDR in msg.targets):
247
+ # if we receive a broadcast is_alive request, we reply
248
+ # with filtering on dev_tyes.
249
+ if 'dev_types' in msg.body.keys():
250
+ dev_types = msg.body['dev_types']
251
+ if 'any.any' in dev_types:
252
+ results = devices
253
+ else:
254
+ for dev in devices:
255
+ any_subtype = dev.dev_type.split('.')[0] + '.any'
256
+ if dev.dev_type in dev_types:
257
+ results.append(dev)
258
+ elif any_subtype in dev_types:
259
+ results.append(dev)
260
+ else:
261
+ # this is a normal request, only filter on device address
262
+ # note: direct is_alive are treated like normal request
263
+ # so dev_types filtering is discarded
264
+ for dev in devices:
265
+ if dev.address in msg.targets:
266
+ results.append(dev)
267
+ return results
268
+
269
+ def search_action(msg, device):
270
+ """
271
+ Extract an action (match with methods) from a msg on the device.
272
+ Return:
273
+ - None
274
+ - found method & matching parameters
275
+
276
+ Note: If method not found raise error, if wrong parameter error log
277
+ """
278
+ methods = device.get_methods()
279
+ params = {}
280
+ result = None
281
+ if msg.action in methods.keys():
282
+ method = methods[msg.action]
283
+ body_params = None
284
+ if msg.body:
285
+ method_params = get_args_method(method)
286
+ body_params = msg.body
287
+
288
+ for k in body_params:
289
+ temp = '_%s' %k
290
+ if temp in method_params:
291
+ params.update({temp:body_params[k]})
292
+ else:
293
+ logger.warning("Wrong method parameter [%s] for action %s" %(k, msg.action))
294
+ result = (method,params)
295
+ else:
296
+ raise XAALError("Method %s not found on device %s" % (msg.action,device))
297
+ return result
298
+
299
+ def get_args_method(method):
300
+ """ return the list on arguments for a given python method """
301
+ spec = inspect.getfullargspec(method)
302
+ try:
303
+ spec.args.remove('self')
304
+ except Exception:
305
+ pass
306
+ return spec.args
307
+
308
+
xaal/lib/devices.py ADDED
@@ -0,0 +1,285 @@
1
+ #
2
+ # Copyright 2014, Jérôme Colin, Jérôme Kerdreux, Philippe Tanguy,
3
+ # Telecom Bretagne.
4
+ #
5
+ # This file is part of xAAL.
6
+ #
7
+ # xAAL is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # xAAL is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with xAAL. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+
21
+
22
+ from . import config
23
+ from . import tools
24
+ from . import bindings
25
+ from .exceptions import DeviceError
26
+
27
+ from tabulate import tabulate
28
+ import logging
29
+ logger = logging.getLogger(__name__)
30
+
31
+ import time
32
+
33
+ class Attribute(object):
34
+
35
+ def __init__(self, name, dev=None, default=None):
36
+ self.name = name
37
+ self.default = default
38
+ self.device = dev
39
+ self.__value = default
40
+
41
+ @property
42
+ def value(self):
43
+ return self.__value
44
+
45
+ @value.setter
46
+ def value(self, value):
47
+ if value != self.__value:
48
+ eng = self.device.engine
49
+ if eng:
50
+ eng.add_attributes_change(self)
51
+ logger.debug("Attr change %s %s=%s" % (self.device.address,self.name,value))
52
+ self.__value = value
53
+
54
+ def __repr__(self): # pragma: no cover
55
+ return f"<{self.__module__}.Attribute {self.name} at 0x{id(self):x}>"
56
+
57
+
58
+ class Attributes(list):
59
+ """Devices owns a attributes list. This list also have dict-like access"""
60
+
61
+ def __getitem__(self,value):
62
+ if isinstance(value,int):
63
+ return list.__getitem__(self,value)
64
+ for k in self:
65
+ if (value == k.name):
66
+ return k.value
67
+ raise KeyError(value)
68
+
69
+ def __setitem__(self,name,value):
70
+ if isinstance(name,int):
71
+ return list.__setitem__(self,name,value)
72
+ for k in self:
73
+ if (name == k.name):
74
+ k.value = value
75
+ return
76
+ raise KeyError(name)
77
+
78
+ class Device(object):
79
+
80
+ __slots__ = ['__dev_type','__address','group_id',
81
+ 'vendor_id','product_id','hw_id',
82
+ '__version','__url','schema','info',
83
+ 'unsupported_attributes','unsupported_methods','unsupported_notifications',
84
+ 'alive_period','next_alive',
85
+ '__attributes','methods','engine']
86
+
87
+ def __init__(self,dev_type,addr=None,engine=None):
88
+ # xAAL internal attributes for a device
89
+ self.dev_type = dev_type # xaal dev_type
90
+ self.address = addr # xaal addr
91
+ self.group_id = None # group devices
92
+ self.vendor_id = None # vendor ID ie : ACME
93
+ self.product_id = None # product ID
94
+ self.hw_id = None # hardware info
95
+ self.__version = None # product release
96
+ self.__url = None # product URL
97
+ self.schema = None # schema URL
98
+ self.info = None # additionnal info
99
+
100
+ # Unsupported stuffs
101
+ self.unsupported_attributes = []
102
+ self.unsupported_methods = []
103
+ self.unsupported_notifications = []
104
+ # Alive management
105
+ self.alive_period = config.alive_timer # time in sec between two alive
106
+ self.next_alive = 0
107
+ # Default attributes & methods
108
+ self.__attributes = Attributes()
109
+ self.methods = {'get_attributes' : self._get_attributes,
110
+ 'get_description': self._get_description }
111
+ self.engine = engine
112
+
113
+ @property
114
+ def dev_type(self):
115
+ return self.__dev_type
116
+
117
+ @dev_type.setter
118
+ def dev_type(self, value):
119
+ if not tools.is_valid_dev_type(value):
120
+ raise DeviceError(f"The dev_type {value} is not valid")
121
+ self.__dev_type = value
122
+
123
+ @property
124
+ def version(self):
125
+ return self.__version
126
+
127
+ @version.setter
128
+ def version(self, value):
129
+ # version must be a string
130
+ if value:
131
+ self.__version = "%s" % value
132
+ else:
133
+ self.__version = None
134
+
135
+ @property
136
+ def address(self):
137
+ return self.__address
138
+
139
+ @address.setter
140
+ def address(self, value):
141
+ if value == None:
142
+ self.__address = None
143
+ return
144
+ if not tools.is_valid_address(value):
145
+ raise DeviceError("This address is not valid")
146
+ self.__address = value
147
+
148
+ @property
149
+ def url(self):
150
+ return self.__url
151
+
152
+ @url.setter
153
+ def url(self,value):
154
+ if value == None:
155
+ self.__url = None
156
+ else:
157
+ self.__url = bindings.URL(value)
158
+
159
+ # attributes
160
+ def new_attribute(self,name,default=None):
161
+ attr = Attribute(name,self,default)
162
+ self.add_attribute(attr)
163
+ return attr
164
+
165
+ def add_attribute(self, attr):
166
+ if attr:
167
+ self.__attributes.append(attr)
168
+ attr.device = self
169
+
170
+ def del_attribute(self,attr):
171
+ if attr:
172
+ attr.device = None
173
+ self.__attributes.remove(attr)
174
+
175
+ def get_attribute(self,name):
176
+ for attr in self.__attributes:
177
+ if attr.name == name:
178
+ return attr
179
+ return None
180
+
181
+ @property
182
+ def attributes(self):
183
+ return self.__attributes
184
+
185
+ @attributes.setter
186
+ def attributes(self,values):
187
+ if isinstance(values,Attributes):
188
+ self.__attributes = values
189
+ else:
190
+ raise DeviceError("Invalid attributes list, use class Attributes)")
191
+
192
+ def add_method(self,name,func):
193
+ self.methods.update({name:func})
194
+
195
+ def get_methods(self):
196
+ return self.methods
197
+
198
+ def update_alive(self):
199
+ """ update the alive timimg"""
200
+ self.next_alive = time.time() + self.alive_period
201
+
202
+ def get_timeout(self):
203
+ """ return Alive timeout used for isAlive msg"""
204
+ return 2 * self.alive_period
205
+
206
+ #####################################################
207
+ # Usefull methods
208
+ #####################################################
209
+ def dump(self):
210
+ print("= Device: %s" % self)
211
+ # info & description
212
+ r = []
213
+ r.append(['dev_type',self.dev_type])
214
+ r.append(['address',self.address])
215
+ for k,v in self._get_description().items():
216
+ r.append([k,v])
217
+ print(tabulate(r,tablefmt="fancy_grid"))
218
+
219
+ # attributes
220
+ if len(self._get_attributes()) > 0:
221
+ r = []
222
+ for k,v in self._get_attributes().items():
223
+ r.append([k,str(v)])
224
+ print(tabulate(r,tablefmt="fancy_grid"))
225
+
226
+ # methods
227
+ if len(self.methods) > 0:
228
+ r = []
229
+ for k,v in self.methods.items():
230
+ r.append([k,v.__name__])
231
+ print(tabulate(r,tablefmt="fancy_grid"))
232
+
233
+
234
+ def __repr__(self):
235
+ return f"<xaal.Device {id(self):x} {self.address} {self.dev_type}>"
236
+
237
+ #####################################################
238
+ # default public methods
239
+ #####################################################
240
+ def _get_description(self):
241
+ result = {}
242
+ if self.vendor_id: result['vendor_id'] = self.vendor_id
243
+ if self.product_id: result['product_id'] = self.product_id
244
+ if self.version: result['version'] = self.version
245
+ if self.url: result['url'] = self.url
246
+ if self.schema: result['schema'] = self.schema
247
+ if self.info: result['info'] = self.info
248
+ if self.hw_id: result['hw_id'] = self.hw_id
249
+ if self.group_id: result['group_id'] = self.group_id
250
+ if self.unsupported_methods: result['unsupported_methods'] = self.unsupported_methods
251
+ if self.unsupported_notifications: result['unsupported_notifications'] = self.unsupported_notifications
252
+ if self.unsupported_attributes: result['unsupported_attributes'] = self.unsupported_attributes
253
+ return result
254
+
255
+ def _get_attributes(self, _attributes=None):
256
+ """
257
+ attributes:
258
+ - None = body empty and means request all attributes
259
+ - Empty array means request all attributes
260
+ - Array of attributes (string) and means request attributes in the
261
+ list
262
+
263
+ TODO: (Waiting for spec. decision) add test on attribute devices
264
+ - case physical sensor not responding or value not ready add error
265
+ with specific error code and with value = suspicious/stale/cached
266
+ """
267
+ result = {}
268
+ dev_attr = {attr.name: attr for attr in self.__attributes}
269
+ if _attributes:
270
+ # Process attributes filter
271
+ for attr in _attributes:
272
+ if attr in dev_attr.keys():
273
+ result.update({dev_attr[attr].name: dev_attr[attr].value})
274
+ else:
275
+ logger.debug(f"Attribute {attr} not found")
276
+ else:
277
+ # Process all attributes
278
+ for attr in dev_attr.values():
279
+ result.update({attr.name: attr.value})
280
+ return result
281
+
282
+ def send_notification(self,notification,body={}):
283
+ """ queue an notification, this is just a method helper """
284
+ if self.engine:
285
+ self.engine.send_notification(self,notification,body)