flexCommunicator 0.1.0__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.
Files changed (53) hide show
  1. flexCommunicator/__init__.py +0 -0
  2. flexCommunicator/clientLibraries/__init__.py +0 -0
  3. flexCommunicator/clientLibraries/flcpy/__init__.py +0 -0
  4. flexCommunicator/clientLibraries/flcpy/action/__init__.py +0 -0
  5. flexCommunicator/clientLibraries/flcpy/action/action.py +85 -0
  6. flexCommunicator/clientLibraries/flcpy/action/action_client.py +70 -0
  7. flexCommunicator/clientLibraries/flcpy/action/goal_handle.py +50 -0
  8. flexCommunicator/clientLibraries/flcpy/codec/ROS2Codec.py +227 -0
  9. flexCommunicator/clientLibraries/flcpy/codec/__init__.py +0 -0
  10. flexCommunicator/clientLibraries/flcpy/commands/__init__.py +0 -0
  11. flexCommunicator/clientLibraries/flcpy/commands/commandProcessor.py +113 -0
  12. flexCommunicator/clientLibraries/flcpy/commands/composer.py +25 -0
  13. flexCommunicator/clientLibraries/flcpy/communication/CommunicationManager.py +579 -0
  14. flexCommunicator/clientLibraries/flcpy/communication/__init__.py +0 -0
  15. flexCommunicator/clientLibraries/flcpy/contracts/__init__.py +0 -0
  16. flexCommunicator/clientLibraries/flcpy/contracts/contractVariable.py +93 -0
  17. flexCommunicator/clientLibraries/flcpy/flexCloud/__init__.py +0 -0
  18. flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudConfiguration.py +16 -0
  19. flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudManager.py +162 -0
  20. flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudOrchestrator.py +248 -0
  21. flexCommunicator/clientLibraries/flcpy/flexCloud/flexCloudVariable.py +130 -0
  22. flexCommunicator/clientLibraries/flcpy/flexNode.py +892 -0
  23. flexCommunicator/clientLibraries/flcpy/fmi/__init__.py +0 -0
  24. flexCommunicator/clientLibraries/flcpy/fmi/fmuNode.py +166 -0
  25. flexCommunicator/clientLibraries/flcpy/fmi/node.py +360 -0
  26. flexCommunicator/clientLibraries/flcpy/knowledge/KnowledgeManager.py +288 -0
  27. flexCommunicator/clientLibraries/flcpy/knowledge/__init__.py +0 -0
  28. flexCommunicator/clientLibraries/flcpy/logging/LoggingAndTracking.py +159 -0
  29. flexCommunicator/clientLibraries/flcpy/logging/__init__.py +0 -0
  30. flexCommunicator/clientLibraries/flcpy/messages/StandardizedMessages.py +64 -0
  31. flexCommunicator/clientLibraries/flcpy/messages/__init__.py +0 -0
  32. flexCommunicator/clientLibraries/flcpy/node.py +486 -0
  33. flexCommunicator/clientLibraries/flcpy/security/__init__.py +0 -0
  34. flexCommunicator/clientLibraries/flcpy/security/security.py +189 -0
  35. flexCommunicator/clientLibraries/flcpy/service/__init__.py +0 -0
  36. flexCommunicator/clientLibraries/flcpy/service/client.py +58 -0
  37. flexCommunicator/clientLibraries/flcpy/service/service.py +67 -0
  38. flexCommunicator/clientLibraries/flcpy/transport/PropertyBinding.py +108 -0
  39. flexCommunicator/clientLibraries/flcpy/transport/PropertyRegistry.py +142 -0
  40. flexCommunicator/clientLibraries/flcpy/transport/__init__.py +0 -0
  41. flexCommunicator/clientLibraries/flcpy/utils/Timer.py +22 -0
  42. flexCommunicator/clientLibraries/flcpy/utils/__init__.py +0 -0
  43. flexCommunicator/clientLibraries/flcpy/utils/auxiliary.py +261 -0
  44. flexCommunicator/clientLibraries/flcpy/utils/constants.py +276 -0
  45. flexCommunicator/clientLibraries/flcpy/utils/decorators.py +70 -0
  46. flexCommunicator/clientLibraries/flcpy/validityFrame/__init__.py +0 -0
  47. flexCommunicator/clientLibraries/flcpy/validityFrame/runtimeMonitor.py +101 -0
  48. flexCommunicator/clientLibraries/flcpy/validityFrame/validityFrameManager.py +958 -0
  49. flexcommunicator-0.1.0.dist-info/METADATA +568 -0
  50. flexcommunicator-0.1.0.dist-info/RECORD +53 -0
  51. flexcommunicator-0.1.0.dist-info/WHEEL +5 -0
  52. flexcommunicator-0.1.0.dist-info/licenses/LICENSE +201 -0
  53. flexcommunicator-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,486 @@
1
+ #**************************************************************************
2
+ # * Copyright (C) 2025-present Bert Van Acker (B.MKR) <bva.bmkr@gmail.com>
3
+ # *
4
+ # * This file is part of the flexIA project.
5
+ # *
6
+ # * flexIA can not be copied and/or distributed without the express
7
+ # * permission of Bert Van Acker
8
+ # *************************************************************************
9
+ import yaml
10
+ import jsonpickle
11
+ import logging
12
+ from datetime import datetime
13
+
14
+ # OWN MODULES
15
+ from flexCommunicator.clientLibraries.flcpy.communication.CommunicationManager import CommunicationManager
16
+ from flexCommunicator.clientLibraries.flcpy.knowledge.KnowledgeManager import KnowledgeManager
17
+ from flexCommunicator.clientLibraries.flcpy.logging.LoggingAndTracking import LoggingAndTrackingHandler
18
+ from flexCommunicator.clientLibraries.flcpy.flexCloud.flexCloudManager import flexCloudManager
19
+ from flexCommunicator.clientLibraries.flcpy.utils.Timer import Timer
20
+ from flexCommunicator.clientLibraries.flcpy.flexCloud.flexCloudVariable import flexCloudVariable
21
+ from flexCommunicator.clientLibraries.flcpy.security.security import *
22
+ from flexCommunicator.clientLibraries.flcpy.transport.PropertyRegistry import PropertyRegistry
23
+ from flexCommunicator.clientLibraries.flcpy.utils.constants import *
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Configuration Keys
27
+ # ---------------------------------------------------------------------------
28
+ KEY_EVENT_MANAGER_CONFIG = "Event_Manager_Config"
29
+ KEY_MESSAGE_MANAGER_CONFIG = "Message_Manager_Config"
30
+ KEY_LOGGER_CONFIG = "Logger_Config"
31
+ KEY_KNOWLEDGE_CONFIG = "Knowledge_Config"
32
+ KEY_APPLICATION_DESCRIPTOR = "application_Descriptor"
33
+ KEY_LOG_LEVEL = "log_level"
34
+ KEY_LOG_FORMAT = "format"
35
+ KEY_PROTOCOL = "protocol"
36
+
37
+ KEY_COMMUNICATION_MATRIX = "Communication_Matrix"
38
+ KEY_DATA_MANAGER_CONFIG = "Data_Manager_Config"
39
+ KEY_FLEXCLOUD_MANAGER_CONFIG = "flexCloud_Manager_Config"
40
+ KEY_COMMUNICATION_MANAGERS_CONFIG = "Communication_managers"
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Default Values and Messages
44
+ # ---------------------------------------------------------------------------
45
+ DEFAULT_EVENT_MESSAGE = {}
46
+ ENCODING_UTF8 = "utf-8"
47
+
48
+
49
+ WARN_REGISTER_EVENT_CALLBACK_NOT_SET = "Event manager is not set for registering event callbacks."
50
+
51
+ MSG_INITIALIZING_KNOWLEDGE = "Initializing knowledge: {} knowledge"
52
+ MSG_INITIALIZING_EVENT_MANAGER = "Initializing Event Manager"
53
+ MSG_INITIALIZING_IO_MANAGER = "Initializing IO Manager"
54
+ MSG_USING_COMMUNICATION_MANAGER = "{} is using {} Communication Manager"
55
+ MSG_STARTING = "{} is starting..."
56
+ MSG_SHUTTING_DOWN = "{} is shutting down..."
57
+ MSG_REGISTER_EVENT_CALLBACK = "Registered callback for event: {}"
58
+
59
+ #flexCommunicator MESSAGES
60
+ MSG_INITIALIZING_DATA_MANAGER = "Initializing Data Manager"
61
+ MSG_INITIALIZING_FLEXCLOUD_MANAGER = "Initializing flexCloud Manager"
62
+ MSG_INITIALIZING_APPLICATION_DESCRIPTOR = "Initializing flexCloud Manager"
63
+ MSG_INITIALIZING_PROPERTY_REGISTRY = "Initializing Property Registry"
64
+
65
+ # flexCommunicator WARNINGS
66
+ WARN_NOT_IMPLEMENTED = "Called functions not implemented yet."
67
+ WARN_DATA_MANAGER_NOT_SET = "Data manager is not set for setting data."
68
+ WARN_KNOWLEDGE_MANAGER_NOT_SET = "knowledge manager is not set for storing data."
69
+ WARN_EVENT_MANAGER_NOT_SET = "Event manager is not set for Event publishing."
70
+
71
+ ERR_PROPERTY_REGISTRY_NOT_FOUND = "Given property is not defined in the communication matrix or transport protocol is not defined."
72
+ ERR_COMMUNICATION_MANAGER_NOT_FOUND = "No matching communication manager found, please verify the transport protocol."
73
+
74
+ # OTHER CONSTANTS
75
+ BACKEND_REDIS = "redis"
76
+ BACKEND_KAFKA = "kafka"
77
+
78
+ class Node:
79
+ def __init__(self, config,loaded_config=False, communicationMatrix = None,multi_transport=False, verbose=False):
80
+ self.timers = []
81
+ if loaded_config:
82
+ self.config = config
83
+ else:
84
+ self.config = self.load_config(config)
85
+
86
+ # -- initialize application descriptor --
87
+ self.applicationDescriptor = self._initialize_application_descriptor()
88
+
89
+ # -- initialize logger --
90
+ self.logger = self._initialize_logger()
91
+ # -- initialize storage/knowledge -- #ASSUMPTION:always redis of now
92
+ self.knowledge = self._initialize_knowledge()
93
+
94
+ # -- initialize data manager --
95
+ self.data_manager = self._initialize_data_manager() #ASSUMPTION: if used, allows only one transport protocal per component
96
+ # -- initialize event manager --
97
+ self.event_manager = self._initialize_event_manager() #TODO: determine if necessary in flexIA #ASSUMPTION: MQTT or ROS2 supported
98
+ # -- initialize flexCloud manager --
99
+ self.flexCloud_manager = self._initialize_flexCloud_manager() #ASSUMPTION: always redis cloud!
100
+
101
+ # Initialize MQTT and ROS2 Event
102
+ if self.event_manager:
103
+ self.logger.info(
104
+ MSG_USING_COMMUNICATION_MANAGER.format(
105
+ self.__class__.__name__,
106
+ self.config[KEY_EVENT_MANAGER_CONFIG][KEY_PROTOCOL]
107
+ )
108
+ )
109
+
110
+
111
+ # -- REGISTER APPLICATION STATUS AS FLEXCLOUD VARIABLE
112
+ self.application_status = flexCloudVariable(name=FLEXCLOUD_VARIABLES.STATUS,statusVariable=True)
113
+ self.register_flexCloud_variable(self.application_status)
114
+ self.application_status.set(value=APPLICATION_STATUS.INITIALIZING)
115
+
116
+ # -- LOAD COMMUNICATION MATRIX AND INITIALIZE PROPERTY REGISTRY
117
+ self.property_registry = self._initialize_property_registry(communicationMatrixFile=communicationMatrix)
118
+
119
+ # -- LOAD AND INITIALIZE MULTI-TRANSPORT COMMUNICATION MANAGERS
120
+ self._multi_transport = multi_transport
121
+ if multi_transport:
122
+ self._communicationManagers = []
123
+ comm_config = self.config[KEY_COMMUNICATION_MANAGERS_CONFIG]
124
+ for comm in comm_config:
125
+ transport_protocol = comm['Data_Manager_Config']['protocol']
126
+ comm_manager = self._initialize_communication_manager(transport_protocol=transport_protocol,config=comm)
127
+ self._communicationManagers.append(comm_manager)
128
+ else:
129
+ self._communicationManagers = []
130
+
131
+ x = 2
132
+
133
+ def load_config(self, config_file):
134
+ with open(config_file, 'r') as file:
135
+ return yaml.safe_load(file)
136
+
137
+ def _initialize_property_registry(self,communicationMatrixFile = "communicationMatrix.yaml"):
138
+ """ Initialize the property registry"""
139
+ try:
140
+ pr = PropertyRegistry(config_path=communicationMatrixFile)
141
+ self.logger.info(MSG_INITIALIZING_PROPERTY_REGISTRY)
142
+ return pr
143
+ except:
144
+ return None
145
+
146
+ def _initialize_logger(self):
147
+ # Create the main logger.
148
+ logger = logging.getLogger(self.__class__.__name__)
149
+ logger.setLevel(self.config[KEY_LOGGER_CONFIG][KEY_LOG_LEVEL])
150
+ # Instantiate the custom logging handler.
151
+ custom_handler = LoggingAndTrackingHandler(config=self.config[KEY_LOGGER_CONFIG], applicationDescriptor=self.applicationDescriptor)
152
+ formatter = logging.Formatter(self.config[KEY_LOGGER_CONFIG][KEY_LOG_FORMAT])
153
+ custom_handler.setFormatter(formatter)
154
+ logger.addHandler(custom_handler)
155
+ return logger
156
+
157
+ def _initialize_knowledge(self):
158
+ """Initialize the knowledge object based on the config."""
159
+ self.logger.info(
160
+ MSG_INITIALIZING_KNOWLEDGE.format(
161
+ self.config[KEY_KNOWLEDGE_CONFIG]["knowledge_type"]
162
+ )
163
+ )
164
+ return KnowledgeManager(self.config[KEY_KNOWLEDGE_CONFIG])
165
+
166
+ def _initialize_event_manager(self):
167
+ """Initialize the Event Manager based on the config."""
168
+ self.logger.info(MSG_INITIALIZING_EVENT_MANAGER)
169
+ return CommunicationManager(config=self.config[KEY_EVENT_MANAGER_CONFIG])
170
+
171
+ def _initialize_flexCloud_manager(self):
172
+ """Initialize the Event Manager based on the config."""
173
+ self.logger.info(MSG_INITIALIZING_FLEXCLOUD_MANAGER)
174
+ return flexCloudManager(config=self.config[KEY_FLEXCLOUD_MANAGER_CONFIG])
175
+
176
+
177
+ def _initialize_data_manager(self):
178
+ """Initialize the Data Manager based on the config."""
179
+ self.logger.info(MSG_INITIALIZING_IO_MANAGER)
180
+ return CommunicationManager(config=self.config[KEY_DATA_MANAGER_CONFIG])
181
+
182
+ def _initialize_application_descriptor(self):
183
+ """Initialize the application description based on the config."""
184
+ #self.logger.info(MSG_INITIALIZING_APPLICATION_DESCRIPTOR)
185
+ return self.config[KEY_APPLICATION_DESCRIPTOR]
186
+
187
+ def spin(self):
188
+ """Start the component and enable Event."""
189
+ self.logger.info(MSG_STARTING.format(self.__class__.__name__))
190
+ if self.event_manager:
191
+ self.event_manager.start()
192
+ if self.data_manager:
193
+ self.data_manager.start()
194
+ if self._multi_transport:
195
+ for comm in self._communicationManagers:
196
+ comm.start()
197
+ # activate all auto-start timers
198
+ for timer in self.timers:
199
+ timer.start()
200
+
201
+ def shutdown(self):
202
+ """Shutdown the component and stop Event."""
203
+ self.logger.info(MSG_SHUTTING_DOWN.format(self.__class__.__name__))
204
+ if self.event_manager:
205
+ self.event_manager.stop()
206
+ if self.data_manager:
207
+ self.data_manager.stop()
208
+ if self._multi_transport:
209
+ for comm in self._communicationManagers:
210
+ comm.stop()
211
+ # deactivate all auto-start timers
212
+ for timer in self.timers:
213
+ timer.stop()
214
+
215
+
216
+ def publish_event(self, event_key, message=DEFAULT_EVENT_MESSAGE):
217
+ """Publish Event using the Event manager."""
218
+ # Generate a unique ID for the message
219
+ message_id = str(uuid.uuid4().hex[:8])
220
+ # Get the current timestamp in ISO 8601 format
221
+ timestamp = datetime.now().isoformat()
222
+
223
+ message['uid'] = message_id
224
+ message['timestamp'] = timestamp
225
+
226
+ if self.event_manager:
227
+ self.event_manager.publish(event_key, json.dumps(message))
228
+ else:
229
+ self.logger.warning(WARN_EVENT_MANAGER_NOT_SET)
230
+
231
+ #def publish_message(self, event_key, message=DEFAULT_EVENT_MESSAGE):
232
+ # """Publish Event using the Event manager."""
233
+ # if self.message_manager:
234
+ # self.message_manager.publish(event_key, message)
235
+ # else:
236
+ # self.logger.warning(WARN_EVENT_MANAGER_NOT_SET)
237
+
238
+
239
+ def _read_knowledge(self, key, queueSize=1,decode=True):
240
+ """Read a value from the knowledge Manager."""
241
+ value = self.knowledge.read(key, queueSize)
242
+ if decode:
243
+ if value is not None:
244
+ value = value.decode(ENCODING_UTF8) # Convert bytes to string
245
+ try:
246
+ value = json.loads(value) # Try to deserialize the value if it's a JSON string
247
+ except json.JSONDecodeError:
248
+ pass # If it's not JSON, return it as a string
249
+ return value
250
+
251
+ def _write_knowledge(self, key, value):
252
+ """Write a value to the knowledge Manager."""
253
+ if isinstance(key, str):
254
+ if isinstance(value, str):
255
+ value = str(value)
256
+ return self.knowledge.write(key, value)
257
+ else:
258
+ # Convert the class instance to a dictionary
259
+ class_dict = {}
260
+ for key_attr, attr_value in key.__dict__.items():
261
+ public_key = key_attr.lstrip('_')
262
+ class_dict[public_key] = attr_value
263
+ value = json.dumps(class_dict) # Serialize the dictionary to a JSON string
264
+ return self.knowledge.write(key.name, value)
265
+
266
+ # --------------------------------------------------------------------------------------
267
+ # - ------------------------- flexCommunicator helpers ---------------------------------
268
+ # --------------------------------------------------------------------------------------
269
+ #TODO: move to auxiliary??
270
+ def _encode(self, message):
271
+ return jsonpickle.encode(message)
272
+
273
+
274
+ def _initialize_communication_manager(self,config=None,transport_protocol=''):
275
+ """Initialize a generic Communication Manager based on the config."""
276
+ self.logger.info(MSG_INITIALIZING_IO_MANAGER + " - TYPE: "+transport_protocol)
277
+ return CommunicationManager(config=config[KEY_DATA_MANAGER_CONFIG])
278
+
279
+ def _determine_communication_manager(self,transport_protocol):
280
+ _communication_manager = None
281
+ for comm in self._communicationManagers:
282
+ if comm.protocol == transport_protocol:
283
+ _communication_manager= comm
284
+
285
+ if _communication_manager == None:
286
+ self.logger.error(ERR_COMMUNICATION_MANAGER_NOT_FOUND)
287
+ return _communication_manager
288
+
289
+
290
+
291
+
292
+ # --------------------------------------------------------------------------------------
293
+ # --------------------------------------------------------------------------------------
294
+ # flexCommunicator API
295
+ # --------------------------------------------------------------------------------------
296
+ # --------------------------------------------------------------------------------------
297
+
298
+ # --------------------------------------------------------------------------------------
299
+ # - --------------------------------TIMERS --------------------------------------------
300
+ # --------------------------------------------------------------------------------------
301
+ def create_timer(self,timer_period=1,callback=None, autostart = True):
302
+ """Creates a timer for periodic execution of functions"""
303
+ timer = Timer(timer_period, callback)
304
+ if autostart:
305
+ self.timers.append(timer)
306
+ return timer
307
+
308
+
309
+ #--------------------------------------------------------------------------------------
310
+ #-------------------------------DATA HANDLING -----------------------------------------
311
+ #--------------------------------------------------------------------------------------
312
+ def set_data(self, data_name="", data=DEFAULT_EVENT_MESSAGE):
313
+ """Publish or emit data as a message."""
314
+
315
+ #--------------- MULTI-TRANSPORT------------------------
316
+ if self._multi_transport:
317
+ # DETERMINE COMMUNCATION MANAGER FOR GIVEN DATA
318
+ protocol = self.property_registry.get_transport(name=data_name)
319
+ _comm_manager = self._determine_communication_manager(transport_protocol=protocol)
320
+ # DISABLE ENCODING FOR OPC-UA
321
+ if protocol == TRANSPORT_PROTOCOLS.PROTOCOL_OPC_UA:
322
+ message_encoded = data
323
+ else:
324
+ message_encoded = self._encode(data)
325
+
326
+ if _comm_manager:
327
+ try:
328
+ # determing transport binding for given data property
329
+ transport = self.property_registry.get_binding(name=data_name, transport=protocol)
330
+ _comm_manager.publish_transport(transport=transport, message=message_encoded)
331
+ except:
332
+ self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
333
+ else:
334
+ self.logger.warning(WARN_DATA_MANAGER_NOT_SET)
335
+
336
+ # --------------- SINGLE-TRANSPORT USING DATA_MANAGER------------------------
337
+ else:
338
+ # DISABLE ENCODING FOR OPC-UA
339
+ if self.data_manager.protocol == TRANSPORT_PROTOCOLS.PROTOCOL_OPC_UA :
340
+ message_encoded = data
341
+ else:
342
+ message_encoded = self._encode(data)
343
+
344
+ if self.data_manager:
345
+ try:
346
+ # determing transport binding for given data property
347
+ transport = self.property_registry.get_binding(name=data_name,transport=self.data_manager.protocol)
348
+ self.data_manager.publish_transport(transport=transport,message=message_encoded)
349
+ except:
350
+ self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
351
+ else:
352
+ self.logger.warning(WARN_DATA_MANAGER_NOT_SET)
353
+
354
+ def get_data(self, data_name=""):
355
+ """Retrieve data."""
356
+ # --------------- MULTI-TRANSPORT------------------------
357
+ if self._multi_transport:
358
+ # DETERMINE COMMUNCATION MANAGER FOR GIVEN DATA
359
+ protocol = self.property_registry.get_transport(name=data_name)
360
+ _comm_manager = self._determine_communication_manager(transport_protocol=protocol)
361
+ if _comm_manager:
362
+ try:
363
+ # determing transport binding for given data property
364
+ transport = self.property_registry.get_binding(name=data_name,transport=_comm_manager.protocol)
365
+ return _comm_manager.read_transport(transport=transport)
366
+ except:
367
+ self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
368
+ return -1
369
+ else:
370
+ self.logger.warning(WARN_DATA_MANAGER_NOT_SET)
371
+ return -1
372
+ # --------------- SINGLE-TRANSPORT USING DATA_MANAGER------------------------
373
+ else:
374
+ if self.data_manager:
375
+ try:
376
+ # determing transport binding for given data property
377
+ transport = self.property_registry.get_binding(name=data_name,transport=self.data_manager.protocol)
378
+ return self.data_manager.read_transport(transport=transport)
379
+ except:
380
+ self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
381
+ return -1
382
+ else:
383
+ self.logger.warning(WARN_DATA_MANAGER_NOT_SET)
384
+ return -1
385
+
386
+ # --------------------------------------------------------------------------------------
387
+ # -------------------------------DATA HANDLING -----------------------------------------
388
+ # --------------------------------------------------------------------------------------
389
+ def store_data(self, data_name="", data=DEFAULT_EVENT_MESSAGE):
390
+ """Store data as a message in a knowledge service."""
391
+ message_encoded = self._encode(data)
392
+ if self.knowledge:
393
+ try:
394
+ # determing transport binding for given data property
395
+ data_key = self.property_registry.get_binding(name=data_name, transport=self.knowledge.backend).generic_endpoint
396
+ self._write_knowledge(key=data_key,value=message_encoded)
397
+ except:
398
+ self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
399
+ else:
400
+ self.logger.warning(WARN_KNOWLEDGE_MANAGER_NOT_SET)
401
+
402
+ def retrieve_data(self, data_name):
403
+ """Retrieve data stored as a message in a knowledge service."""
404
+ if self.knowledge:
405
+ try:
406
+ # determing transport binding for given data property
407
+ data_key = self.property_registry.get_binding(name=data_name, transport=self.knowledge.backend).generic_endpoint
408
+ data = self._read_knowledge(key=data_key,decode=False)
409
+ except:
410
+ data = None
411
+ self.logger.error(ERR_PROPERTY_REGISTRY_NOT_FOUND)
412
+ else:
413
+ data = None
414
+ self.logger.warning(WARN_KNOWLEDGE_MANAGER_NOT_SET)
415
+ return data
416
+
417
+ def delete_data(self, data_key, data=DEFAULT_EVENT_MESSAGE):
418
+ """Delete data stored as a message in a knowledge service."""
419
+ self.logger.warning(WARN_NOT_IMPLEMENTED)
420
+
421
+ def exists_data(self, data_key, data=DEFAULT_EVENT_MESSAGE):
422
+ """Check if data exists in a knowledge service."""
423
+ self.logger.warning(WARN_NOT_IMPLEMENTED)
424
+
425
+ def register_data_callback(self, data_name, callback):
426
+ """Register a callback for Data manager events (MQTT or Redis)."""
427
+ # --------------- MULTI-TRANSPORT------------------------
428
+ if self._multi_transport:
429
+ # DETERMINE COMMUNCATION MANAGER FOR GIVEN DATA
430
+ protocol = self.property_registry.get_transport(name=data_name)
431
+ _comm_manager = self._determine_communication_manager(transport_protocol=protocol)
432
+ if _comm_manager:
433
+ transport = self.property_registry.get_binding(name=data_name, transport=_comm_manager.protocol)
434
+ _comm_manager.subscribe_transport(transport=transport, callback=callback)
435
+ self.logger.info(MSG_REGISTER_EVENT_CALLBACK.format(data_name))
436
+ else:
437
+ self.logger.warning(WARN_REGISTER_EVENT_CALLBACK_NOT_SET)
438
+
439
+ # --------------- SINGLE-TRANSPORT USING DATA_MANAGER------------------------
440
+ else:
441
+ if self.data_manager:
442
+ transport = self.property_registry.get_binding(name=data_name, transport=self.data_manager.protocol)
443
+ self.data_manager.subscribe_transport(transport=transport, callback=callback)
444
+ self.logger.info(MSG_REGISTER_EVENT_CALLBACK.format(data_name))
445
+ else:
446
+ self.logger.warning(WARN_REGISTER_EVENT_CALLBACK_NOT_SET)
447
+
448
+ # --------------------------------------------------------------------------------------
449
+ # - -----------------------------EVENT HANDLING -----------------------------------------
450
+ # --------------------------------------------------------------------------------------
451
+ def emit_event(self, event_key, data=DEFAULT_EVENT_MESSAGE):
452
+ """Publish or emit event as a message."""
453
+ message_encoded = self._encode(data)
454
+ if self.event_manager:
455
+ self.event_manager.publish(event_key, message_encoded)
456
+ else:
457
+ self.logger.warning(WARN_EVENT_MANAGER_NOT_SET)
458
+ def register_event_callback(self, event_key, callback):
459
+ """Register a callback for Event manager events (MQTT or Redis)."""
460
+ #TODO: add timestamp and unique ID to event message
461
+ if self.event_manager:
462
+ self.event_manager.subscribe(event_key, callback)
463
+ self.logger.info(MSG_REGISTER_EVENT_CALLBACK.format(event_key))
464
+ else:
465
+ self.logger.warning(WARN_REGISTER_EVENT_CALLBACK_NOT_SET)
466
+
467
+ # --------------------------------------------------------------------------------------
468
+ # - -----------------------flexCloud variable handling ---------------------------------
469
+ # --------------------------------------------------------------------------------------
470
+ def register_flexCloud_variable(self,variable=None):
471
+ """Register a flexCloud variable."""
472
+ # assign flexCloud manager + assign flexCloud connectorID
473
+ variable.flexCloud_manager = self.flexCloud_manager
474
+ variable.flexCloud_connectorID = self._get_flexCloud_connectorID()
475
+ variable.flexCloud_applicationID = self.applicationDescriptor["id"]
476
+ self.logger.info("flexCloud variable registered, name:" + variable._name)
477
+
478
+ def _get_flexCloud_connectorID(self):
479
+
480
+ # TODO: determine the connectorID, either static or dynamic via OS variables
481
+ if self.applicationDescriptor["connectorID_type"] == "static":
482
+ flexCloud_connectorID = self.applicationDescriptor["connectorID"]
483
+ else:
484
+ flexCloud_connectorID = get_os_variable(SECURITY_STORE.FLEXCLOUD_CONNECTORID_SECRET)
485
+
486
+ return flexCloud_connectorID
@@ -0,0 +1,189 @@
1
+ #**************************************************************************
2
+ # * Copyright (C) 2025-present Bert Van Acker (B.MKR) <bva.bmkr@gmail.com>
3
+ # *
4
+ # * This file is part of the flexIA project.
5
+ # *
6
+ # * flexIA can not be copied and/or distributed without the express
7
+ # * permission of Bert Van Acker
8
+ # *************************************************************************
9
+ import os
10
+ import uuid
11
+ import json
12
+ import hmac
13
+ import hashlib
14
+ import secrets
15
+ import bcrypt
16
+ import random
17
+ import string
18
+
19
+ from flexCommunicator.clientLibraries.flcpy.utils.constants import *
20
+ def compute_signature(command):
21
+ # Create a consistent string representation including the new fields
22
+ SHARED_SECRET = os.environ.get(SECURITY_STORE.FLEXCLOUD_ACTIVE_SHARED_KEY)
23
+
24
+ # KEY FOR BETA-TESTING!! REMOVE WHEN DEPLOYED
25
+ if SHARED_SECRET is None:
26
+ SHARED_SECRET = "b'\\xae\\xc1?\\x14\\xb3X\\xb1\\xb9#\\xbe\\xe6\\x9d\\xab\\x82])9\\xb9\\xa1*\\xf5=~\\x04p\\xe0\\x9a\\x0c\\x1b\\x17\\xd5\\x9c'"
27
+
28
+ data_to_sign = (
29
+ command["id"] +
30
+ command["cmd"] +
31
+ str(command["priority"]) +
32
+ str(command["expiry"]) +
33
+ json.dumps(command["params"], sort_keys=True) +
34
+ str(command["timestamp"]) +
35
+ command["nonce"]
36
+ )
37
+ return hmac.new(SHARED_SECRET.encode(), data_to_sign.encode(), hashlib.sha256).hexdigest()
38
+
39
+ def verify_signature(command):
40
+ SHARED_SECRET = os.environ.get(SECURITY_STORE.FLEXCLOUD_ACTIVE_SHARED_KEY)
41
+
42
+ # KEY FOR BETA-TESTING!! REMOVE WHEN DEPLOYED
43
+ if SHARED_SECRET is None:
44
+ SHARED_SECRET = "b'\\xae\\xc1?\\x14\\xb3X\\xb1\\xb9#\\xbe\\xe6\\x9d\\xab\\x82])9\\xb9\\xa1*\\xf5=~\\x04p\\xe0\\x9a\\x0c\\x1b\\x17\\xd5\\x9c'"
45
+
46
+ expected = hmac.new(
47
+ SHARED_SECRET.encode(),
48
+ (
49
+ command["id"] +
50
+ command["cmd"] +
51
+ str(command["priority"]) +
52
+ str(command["expiry"]) +
53
+ json.dumps(command["params"], sort_keys=True) +
54
+ str(command["timestamp"]) +
55
+ command["nonce"]
56
+ ).encode(),
57
+ hashlib.sha256
58
+ ).hexdigest()
59
+ return hmac.compare_digest(expected, command["signature"])
60
+
61
+
62
+ def generate_shared_secret(length=32):
63
+ """
64
+ Generates a cryptographically secure random shared secret.
65
+
66
+ Args:
67
+ length (int): The number of bytes for the secret (default 32 bytes for 256 bits).
68
+
69
+ Returns:
70
+ bytes: The generated shared secret.
71
+ """
72
+ return secrets.token_bytes(length)
73
+
74
+ import os
75
+
76
+ def register_os_variable(secret, env_var_name=SECURITY_STORE.FLEXCLOUD_ACTIVE_SHARED_KEY):
77
+ """
78
+ Stores a given shared secret in an OS environment variable.
79
+
80
+ Args:
81
+ secret (str or bytes): The shared secret to store. If bytes, it will be converted to a hexadecimal string.
82
+ env_var_name (str): The name of the environment variable to set (default is "SHARED_SECRET").
83
+
84
+ Note:
85
+ This only affects the current process (and its child processes). It does not persist across reboots
86
+ or new sessions.
87
+ """
88
+ # If the secret is provided as bytes, convert it to a hex string for easier storage
89
+ if isinstance(secret, bytes):
90
+ secret = secret.hex()
91
+ try:
92
+ os.environ[env_var_name] = secret
93
+ return True
94
+ except:
95
+ return False
96
+
97
+ def get_os_variable(env_var_name=""):
98
+ return os.getenv(env_var_name)
99
+
100
+ def generate_app_tag(prefix="APP", unique_length=10):
101
+ """
102
+ Generates a unique tag for an application.
103
+
104
+ The tag is composed of a prefix and a unique identifier generated
105
+ from a UUID. The unique identifier is taken as the first `unique_length`
106
+ characters of the hexadecimal representation of a UUID.
107
+
108
+ Args:
109
+ prefix (str): A string to prefix the tag (default is "APP").
110
+ unique_length (int): Number of characters from the UUID to use (default is 8).
111
+
112
+ Returns:
113
+ str: The generated application tag.
114
+ """
115
+ # Generate a UUID and take the first `unique_length` characters of its hex representation
116
+ unique_part = uuid.uuid4().hex[:unique_length]
117
+ if prefix is not None:
118
+ return f"{prefix}-{unique_part}"
119
+ else:
120
+ return unique_part
121
+
122
+
123
+ def hash_password(password: str) -> str:
124
+ """
125
+ Hash a password using bcrypt.
126
+
127
+ Args:
128
+ password (str): The plaintext password to hash.
129
+
130
+ Returns:
131
+ str: The hashed password.
132
+ """
133
+ # Convert the password to bytes
134
+ password_bytes = password.encode('utf-8')
135
+ # Generate a salt (the default work factor is secure for most purposes)
136
+ salt = bcrypt.gensalt()
137
+ # Generate the hashed password
138
+ hashed_password = bcrypt.hashpw(password_bytes, salt)
139
+ # Return the hashed password as a string
140
+ return hashed_password.decode('utf-8')
141
+
142
+
143
+
144
+
145
+ def generate_custom_strong_password(length=12, use_upper=True, use_lower=True, use_digits=True, use_symbols=True):
146
+ """
147
+ Generates a custom strong password.
148
+
149
+ Parameters:
150
+ length (int): Desired total length of the password.
151
+ use_upper (bool): If True, include uppercase letters.
152
+ use_lower (bool): If True, include lowercase letters.
153
+ use_digits (bool): If True, include digits.
154
+ use_symbols (bool): If True, include punctuation/symbols.
155
+
156
+ Returns:
157
+ str: A randomly generated strong password.
158
+
159
+ Raises:
160
+ ValueError: If no character sets are selected or length is too short.
161
+ """
162
+ # Build list of chosen character sets
163
+ char_sets = []
164
+ if use_upper:
165
+ char_sets.append(string.ascii_uppercase)
166
+ if use_lower:
167
+ char_sets.append(string.ascii_lowercase)
168
+ if use_digits:
169
+ char_sets.append(string.digits)
170
+ if use_symbols:
171
+ char_sets.append(string.punctuation)
172
+
173
+ if not char_sets:
174
+ raise ValueError("At least one type of characters must be selected.")
175
+
176
+ # Ensure the password length can include at least one character from each selected set
177
+ if length < len(char_sets):
178
+ raise ValueError(f"Password length must be at least {len(char_sets)} to include one of each selected type.")
179
+
180
+ # Guarantee each selected type is represented at least once
181
+ password_chars = [random.choice(char_set) for char_set in char_sets]
182
+
183
+ # Fill the rest of the password length with a random selection from all allowed characters
184
+ all_allowed_chars = ''.join(char_sets)
185
+ password_chars += random.choices(all_allowed_chars, k=length - len(password_chars))
186
+
187
+ # Shuffle the list to ensure randomness and then convert it to a string
188
+ random.shuffle(password_chars)
189
+ return ''.join(password_chars)