PyAutomationIO 1.1.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.
Files changed (138) hide show
  1. automation/__init__.py +46 -0
  2. automation/alarms/__init__.py +563 -0
  3. automation/alarms/states.py +192 -0
  4. automation/alarms/trigger.py +64 -0
  5. automation/buffer.py +132 -0
  6. automation/core.py +1792 -0
  7. automation/dbmodels/__init__.py +23 -0
  8. automation/dbmodels/alarms.py +549 -0
  9. automation/dbmodels/core.py +86 -0
  10. automation/dbmodels/events.py +178 -0
  11. automation/dbmodels/logs.py +155 -0
  12. automation/dbmodels/machines.py +181 -0
  13. automation/dbmodels/opcua.py +81 -0
  14. automation/dbmodels/opcua_server.py +174 -0
  15. automation/dbmodels/tags.py +921 -0
  16. automation/dbmodels/users.py +259 -0
  17. automation/extensions/__init__.py +15 -0
  18. automation/extensions/api.py +149 -0
  19. automation/extensions/cors.py +18 -0
  20. automation/filter/__init__.py +19 -0
  21. automation/iad/__init__.py +3 -0
  22. automation/iad/frozen_data.py +54 -0
  23. automation/iad/out_of_range.py +51 -0
  24. automation/iad/outliers.py +51 -0
  25. automation/logger/__init__.py +0 -0
  26. automation/logger/alarms.py +434 -0
  27. automation/logger/core.py +265 -0
  28. automation/logger/datalogger.py +877 -0
  29. automation/logger/events.py +202 -0
  30. automation/logger/logdict.py +53 -0
  31. automation/logger/logs.py +203 -0
  32. automation/logger/machines.py +248 -0
  33. automation/logger/opcua_server.py +130 -0
  34. automation/logger/users.py +96 -0
  35. automation/managers/__init__.py +4 -0
  36. automation/managers/alarms.py +455 -0
  37. automation/managers/db.py +328 -0
  38. automation/managers/opcua_client.py +186 -0
  39. automation/managers/state_machine.py +183 -0
  40. automation/models.py +174 -0
  41. automation/modules/__init__.py +14 -0
  42. automation/modules/alarms/__init__.py +0 -0
  43. automation/modules/alarms/resources/__init__.py +10 -0
  44. automation/modules/alarms/resources/alarms.py +280 -0
  45. automation/modules/alarms/resources/summary.py +81 -0
  46. automation/modules/events/__init__.py +0 -0
  47. automation/modules/events/resources/__init__.py +10 -0
  48. automation/modules/events/resources/events.py +85 -0
  49. automation/modules/events/resources/logs.py +109 -0
  50. automation/modules/tags/__init__.py +0 -0
  51. automation/modules/tags/resources/__init__.py +8 -0
  52. automation/modules/tags/resources/tags.py +254 -0
  53. automation/modules/users/__init__.py +2 -0
  54. automation/modules/users/resources/__init__.py +10 -0
  55. automation/modules/users/resources/models/__init__.py +2 -0
  56. automation/modules/users/resources/models/roles.py +5 -0
  57. automation/modules/users/resources/models/users.py +14 -0
  58. automation/modules/users/resources/roles.py +38 -0
  59. automation/modules/users/resources/users.py +113 -0
  60. automation/modules/users/roles.py +121 -0
  61. automation/modules/users/users.py +335 -0
  62. automation/opcua/__init__.py +1 -0
  63. automation/opcua/models.py +541 -0
  64. automation/opcua/subscription.py +259 -0
  65. automation/pages/__init__.py +0 -0
  66. automation/pages/alarms.py +34 -0
  67. automation/pages/alarms_history.py +21 -0
  68. automation/pages/assets/styles.css +7 -0
  69. automation/pages/callbacks/__init__.py +28 -0
  70. automation/pages/callbacks/alarms.py +218 -0
  71. automation/pages/callbacks/alarms_summary.py +20 -0
  72. automation/pages/callbacks/db.py +222 -0
  73. automation/pages/callbacks/filter.py +238 -0
  74. automation/pages/callbacks/machines.py +29 -0
  75. automation/pages/callbacks/machines_detailed.py +581 -0
  76. automation/pages/callbacks/opcua.py +266 -0
  77. automation/pages/callbacks/opcua_server.py +244 -0
  78. automation/pages/callbacks/tags.py +495 -0
  79. automation/pages/callbacks/trends.py +119 -0
  80. automation/pages/communications.py +129 -0
  81. automation/pages/components/__init__.py +123 -0
  82. automation/pages/components/alarms.py +151 -0
  83. automation/pages/components/alarms_summary.py +45 -0
  84. automation/pages/components/database.py +128 -0
  85. automation/pages/components/gaussian_filter.py +69 -0
  86. automation/pages/components/machines.py +396 -0
  87. automation/pages/components/opcua.py +384 -0
  88. automation/pages/components/opcua_server.py +53 -0
  89. automation/pages/components/tags.py +253 -0
  90. automation/pages/components/trends.py +66 -0
  91. automation/pages/database.py +26 -0
  92. automation/pages/filter.py +55 -0
  93. automation/pages/machines.py +20 -0
  94. automation/pages/machines_detailed.py +41 -0
  95. automation/pages/main.py +63 -0
  96. automation/pages/opcua_server.py +28 -0
  97. automation/pages/tags.py +40 -0
  98. automation/pages/trends.py +35 -0
  99. automation/singleton.py +30 -0
  100. automation/state_machine.py +1674 -0
  101. automation/tags/__init__.py +2 -0
  102. automation/tags/cvt.py +1198 -0
  103. automation/tags/filter.py +55 -0
  104. automation/tags/tag.py +418 -0
  105. automation/tests/__init__.py +10 -0
  106. automation/tests/test_alarms.py +110 -0
  107. automation/tests/test_core.py +257 -0
  108. automation/tests/test_unit.py +21 -0
  109. automation/tests/test_user.py +155 -0
  110. automation/utils/__init__.py +164 -0
  111. automation/utils/decorators.py +222 -0
  112. automation/utils/npw.py +294 -0
  113. automation/utils/observer.py +21 -0
  114. automation/utils/units.py +118 -0
  115. automation/variables/__init__.py +55 -0
  116. automation/variables/adimentional.py +30 -0
  117. automation/variables/current.py +71 -0
  118. automation/variables/density.py +115 -0
  119. automation/variables/eng_time.py +68 -0
  120. automation/variables/force.py +90 -0
  121. automation/variables/length.py +104 -0
  122. automation/variables/mass.py +80 -0
  123. automation/variables/mass_flow.py +101 -0
  124. automation/variables/percentage.py +30 -0
  125. automation/variables/power.py +113 -0
  126. automation/variables/pressure.py +93 -0
  127. automation/variables/temperature.py +168 -0
  128. automation/variables/volume.py +70 -0
  129. automation/variables/volumetric_flow.py +100 -0
  130. automation/workers/__init__.py +2 -0
  131. automation/workers/logger.py +164 -0
  132. automation/workers/state_machine.py +207 -0
  133. automation/workers/worker.py +36 -0
  134. pyautomationio-1.1.1.dist-info/METADATA +199 -0
  135. pyautomationio-1.1.1.dist-info/RECORD +138 -0
  136. pyautomationio-1.1.1.dist-info/WHEEL +5 -0
  137. pyautomationio-1.1.1.dist-info/licenses/LICENSE +21 -0
  138. pyautomationio-1.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,541 @@
1
+ from opcua import Client as OPCClient
2
+ from opcua import ua
3
+ # import sched
4
+ from opcua.ua.uatypes import NodeId, datatype_to_varianttype
5
+ import re, uuid, logging, time
6
+
7
+
8
+ class Client(OPCClient):
9
+ r"""
10
+ Documentation here
11
+ """
12
+ def __init__(self, url, client_name:str, timeout=60):
13
+ r"""
14
+ Documentation here
15
+ """
16
+ self._id = None
17
+ self._server_url = url
18
+ self._timeout = timeout
19
+ self.name = client_name
20
+ self._client = None
21
+ self._is_open = False
22
+ self._opc_ua_tree = dict()
23
+ # self.scheduler = sched.scheduler(time.time, time.sleep)
24
+ # self.token_renewal_interval = 30 # Cada 10 minutos
25
+ super(Client, self).__init__(url, timeout)
26
+
27
+ def get_id(self):
28
+ r"""
29
+ Documentation here
30
+ """
31
+ return self._id
32
+
33
+ def is_token_valid(self):
34
+ try:
35
+ secure_channel = self.uaclient._uasocket._connection
36
+ token_id = secure_channel.security_token.TokenId
37
+ if token_id == secure_channel.next_security_token.TokenId or token_id == secure_channel.prev_security_token.TokenId:
38
+
39
+ return True
40
+
41
+ else:
42
+ logging.error("Security token is not valid.")
43
+ return False
44
+ except Exception as e:
45
+ logging.error(f"Failed to check security token: {e}")
46
+ return False
47
+
48
+ def connect(self):
49
+ r"""
50
+ Documentation here
51
+ """
52
+ try:
53
+ # Connect to the server
54
+ super(Client, self).connect()
55
+
56
+ # Now you're connected again!
57
+ self._is_open = True
58
+ self._id = str(uuid.uuid4())
59
+ result = {
60
+ 'message': 'Successful connection',
61
+ 'url': self._server_url,
62
+ 'is_connected': self._is_open,
63
+ 'id': self.get_id()
64
+ }
65
+ return result, 200
66
+
67
+ except Exception as _err:
68
+ logger = logging.getLogger("pyautomation")
69
+ logger.error(f"Error during connection: {_err}")
70
+ self._is_open = False
71
+ result = {
72
+ 'message': 'Connection could not be established',
73
+ 'url': self._server_url,
74
+ 'is_connected': self._is_open,
75
+ 'id': self.get_id()
76
+ }
77
+ return result, 404
78
+
79
+ def revolve_security_tokens(self):
80
+ logging.critical("Trying revolving security token")
81
+ try:
82
+ self.uaclient._uasocket._connection.revolve_tokens()
83
+ logging.critical("Security tokens revolved successfully")
84
+ except Exception as e:
85
+ logging.error(f"Failed to revolve security tokens: {e}")
86
+
87
+ def reconnect(self):
88
+
89
+ # if not self.is_connected() or not self.is_token_valid():
90
+ if not self.is_connected():
91
+
92
+ from automation import PyAutomation
93
+ app = PyAutomation()
94
+ app.sio.emit("on.opcua.disconnected", data={"message": f"Disconneted from {self._server_url}"})
95
+ logging.critical(f"Attempting to reconnect to {self._server_url}")
96
+ try:
97
+
98
+ result, status = self.connect()
99
+
100
+ if status == 200:
101
+ # Revolver tokens de seguridad para asegurar la validez
102
+ # self.revolve_security_tokens()
103
+ app.sio.emit("on.opcua.connected", data={"message": f"Conneted to {self._server_url}"})
104
+ tags = app.get_tags()
105
+ for tag in tags:
106
+ _tag = app.cvt.get_tag(id=tag["id"])
107
+ app.subscribe_opcua(tag=_tag, opcua_address=tag['opcua_address'], node_namespace=tag['node_namespace'], scan_time=tag['scan_time'], reload=True)
108
+
109
+ logging.critical(f"Reconnected to {self._server_url}")
110
+ except:
111
+ logging.critical(f"Reconnection failed...")
112
+
113
+ def __reset_object_attributes(self):
114
+ r"""
115
+ Documentation here
116
+ """
117
+ self._server_url = None
118
+ self._client = None
119
+ self._opc_ua_tree = dict()
120
+
121
+ def disconnect(self):
122
+ r"""
123
+ Documentation here
124
+ """
125
+ try:
126
+ super(Client, self).disconnect()
127
+ self.__reset_object_attributes()
128
+ result = {
129
+ 'message': 'Successful disconnection',
130
+ 'is_connected': False
131
+ }
132
+ return result, 200
133
+
134
+ except Exception as _err:
135
+ result = {'message': 'Disconnect could not be performed'}
136
+ return result, 404
137
+
138
+ def get_opc_ua_tree(self):
139
+ r"""
140
+ Documentation here
141
+ """
142
+ try:
143
+ if self.is_connected():
144
+ root = self.get_objects_node()
145
+ node = self.get_node(root)
146
+ tree = self.__walk_into_nodes(node)
147
+ return tree, 200
148
+
149
+ except Exception as _err:
150
+ self.disconnect()
151
+ result = { 'message': str(_err)}
152
+ return result, 500
153
+
154
+ def __walk_into_nodes(self, node, tree=None):
155
+ r"""
156
+ Documentation here
157
+ """
158
+ if tree is None:
159
+
160
+ tree = dict()
161
+
162
+ _object = list()
163
+
164
+ if self.is_connected():
165
+
166
+ for ref in node.get_children_descriptions():
167
+
168
+ _node = self.get_node(ref.NodeId)
169
+ # ('Aliases', 'MyObjects', 'Server', 'StaticData')
170
+
171
+ if _node.get_browse_name().Name not in ('Aliases', 'MyObjects', 'Server', 'StaticData'):
172
+
173
+ result = self.__opc_ua_tree(ref.NodeId)
174
+
175
+ if _node.get_children():
176
+
177
+ _children = self.__get_children_node_recursively(_node)
178
+
179
+ result['children'] = _children
180
+
181
+ _object.append(result)
182
+
183
+ tree[f"{node.get_browse_name().Name}"] = _object
184
+
185
+ return tree
186
+
187
+ def __get_children_node_recursively(self, node, children=None):
188
+ r"""
189
+ Documentation here
190
+ """
191
+
192
+ if children is None:
193
+
194
+ children = list()
195
+ if self.is_connected():
196
+ for child in node.get_children():
197
+
198
+ result = self.__opc_ua_tree(child.nodeid)
199
+
200
+ if child.get_children():
201
+
202
+ _children = self.__get_children_node_recursively(child)
203
+
204
+ result['children'] = _children
205
+
206
+ children.append(result)
207
+
208
+ return children
209
+
210
+ def __opc_ua_tree(self, namespace_node):
211
+ r"""
212
+ Documentation here
213
+ """
214
+ if self.is_connected():
215
+ _node = self.get_node(namespace_node)
216
+
217
+ result = {
218
+ "title": _node.get_browse_name().Name,
219
+ "key": _node.nodeid.to_string(),
220
+ "children": [],
221
+ "NodeClass": _node.get_node_class().name,
222
+ }
223
+
224
+ return result
225
+
226
+ def get_values(self, nodes:list):
227
+ r"""
228
+ Documentation here
229
+ """
230
+ if self.is_connected():
231
+ results = self.uaclient.get_attributes(nodes, ua.AttributeIds.Value)
232
+ result = [{"Namespace": nodes[id].to_string(), "Value": result.Value.Value, "Timestamp": result.SourceTimestamp} for id, result in enumerate(results)]
233
+
234
+ return result, 200
235
+
236
+ def get_nodes_id_by_namespaces(self, namespaces:list):
237
+ r"""
238
+ Documentar here
239
+ """
240
+ nodes = list()
241
+
242
+ for namespace in namespaces:
243
+ if self.is_connected():
244
+ _node = self.get_node(NodeId.from_string(namespace))
245
+ nodes.append(_node)
246
+
247
+ return nodes
248
+
249
+ def get_node_id_by_namespace(self, namespace:str):
250
+ r"""
251
+ Documentar here
252
+ """
253
+ if self.is_connected():
254
+ return self.get_node(NodeId.from_string(namespace))
255
+
256
+ def get_nodes_values(self, namespaces:list)->list:
257
+ r"""
258
+ Documentation here
259
+ """
260
+ result = list()
261
+ nodes = list()
262
+
263
+ for namespace in namespaces:
264
+ if self.is_connected():
265
+ _node = self.get_node(NodeId.from_string(namespace))
266
+ nodes.append(_node)
267
+
268
+ if _node.get_node_class().name.lower()=='variable':
269
+ node = {
270
+ "Namespace": namespace,
271
+ "Value": _node.get_value()
272
+ }
273
+ result.append(node)
274
+
275
+ return result, 200
276
+
277
+ def write_value(self, node_namespace: str, value):
278
+ r"""
279
+ Escribe un valor en un nodo variable del servidor OPC UA
280
+
281
+ Args:
282
+ node_namespace: Namespace del nodo en formato string (ej: "ns=2;i=1234")
283
+ value: Valor a escribir (el tipo debe ser compatible con el nodo)
284
+
285
+ Returns:
286
+ tuple: (dict con resultado, status_code)
287
+ """
288
+ try:
289
+ if not self.is_connected():
290
+ return {
291
+ 'message': 'Cliente no conectado al servidor',
292
+ 'namespace': node_namespace,
293
+ 'success': False
294
+ }, 400
295
+
296
+ _node = self.get_node(NodeId.from_string(node_namespace))
297
+
298
+ # Verificar que es un nodo variable
299
+ if _node.get_node_class().name.lower() != 'variable':
300
+ return {
301
+ 'message': f'El nodo no es de tipo Variable, es {_node.get_node_class().name}',
302
+ 'namespace': node_namespace,
303
+ 'success': False
304
+ }, 400
305
+
306
+ # Verificar permisos de escritura
307
+ access_level = _node.get_access_level()
308
+ user_access_level = _node.get_user_access_level()
309
+
310
+ # Escribir el valor
311
+ _node.set_value(value)
312
+
313
+ result = {
314
+ 'message': 'Valor escrito exitosamente',
315
+ 'namespace': node_namespace,
316
+ 'value': value,
317
+ 'success': True
318
+ }
319
+ return result, 200
320
+
321
+ except Exception as err:
322
+ logger = logging.getLogger("pyautomation")
323
+ logger.error(f"Error escribiendo valor en {node_namespace}: {err}")
324
+ result = {
325
+ 'message': f'Error al escribir valor: {str(err)}',
326
+ 'namespace': node_namespace,
327
+ 'success': False
328
+ }
329
+ return result, 500
330
+
331
+ @staticmethod
332
+ def find_servers(hostname, port):
333
+ r"""
334
+ Documentation here
335
+ """
336
+
337
+ _client = OPCClient(f'opc.tcp://{hostname}:{port}')
338
+ servers = _client.connect_and_find_servers()
339
+ _servers = list()
340
+ for server in servers:
341
+ _server = dict()
342
+ _server['ApplicationUri'] = server.ApplicationUri
343
+ _server['ProductUri'] = server.ProductUri
344
+ _server['ApplicationName'] = server.ApplicationName.Text
345
+ _server['ApplicationType'] = server.ApplicationType.Server
346
+ _server['GatewayServerUri'] = server.GatewayServerUri
347
+ _server['DiscoveryProfileUri'] = server.DiscoveryProfileUri
348
+ _server['DiscoveryUrls'] = server.DiscoveryUrls
349
+ _servers.append(_server)
350
+
351
+ return _servers
352
+
353
+ @staticmethod
354
+ def get_endpoints(hostname, port):
355
+ r"""
356
+ Documentation here
357
+ """
358
+ try:
359
+ _client = OPCClient(f'opc.tcp://{hostname}:{port}')
360
+ endpoints = _client.connect_and_get_server_endpoints()
361
+ _endpoints = list()
362
+ for ep in endpoints:
363
+
364
+ if isinstance(ep.Server.DiscoveryUrls, list):
365
+
366
+ _endpoints.extend(ep.Server.DiscoveryUrls)
367
+
368
+ else:
369
+
370
+ _endpoints.append(ep.Server.DiscoveryUrls)
371
+
372
+ _endpoints = list(set(_endpoints))
373
+
374
+ for ep in _endpoints:
375
+ if not ep.startswith('opc.tcp'):
376
+ _endpoints.remove(ep)
377
+
378
+ result = [re.sub('//.*?/',f'//{hostname}:{port}/', __ep) for __ep in _endpoints]
379
+ result = {
380
+ 'message': 'Successful search',
381
+ 'endpoints': result
382
+ }
383
+
384
+ return result, 200
385
+
386
+ except Exception as err:
387
+
388
+ result = {
389
+ 'message': 'Unsuccessful search',
390
+ 'endpoints': []
391
+ }
392
+ return result, 400
393
+
394
+ def is_connected(self):
395
+ r"""
396
+ Documentation here
397
+ """
398
+ try:
399
+ return self.uaclient._uasocket._connection.is_open()
400
+
401
+ except Exception as _err:
402
+
403
+ return False
404
+
405
+ def get_node_attributes(self, node_namespace)->dict:
406
+ r"""
407
+ Documentation here
408
+ """
409
+ if self.is_connected():
410
+ _node = self.get_node(NodeId.from_string(node_namespace))
411
+
412
+ node_class = _node.get_node_class().name.lower()
413
+
414
+ if node_class=='variable':
415
+
416
+ result = {
417
+ "NamespaceIndex": _node.nodeid.NamespaceIndex,
418
+ "NamespaceUri": _node.nodeid.NamespaceUri,
419
+ "Identifier": _node.nodeid.Identifier,
420
+ "Namespace": _node.nodeid.to_string(),
421
+ "NodeClass": _node.get_node_class().name,
422
+ "BrowseName": _node.get_browse_name().Name,
423
+ "DataValue": _node.get_data_value(),
424
+ "DisplayName": _node.get_display_name().Text,
425
+ "DataType": datatype_to_varianttype(_node.get_data_type()).name,
426
+ "AccesLevel": [access_lvl.name for access_lvl in _node.get_access_level()],
427
+ "UserAccessLevel": [user_access_lvl.name for user_access_lvl in _node.get_user_access_level()],
428
+ "Description": _node.get_description().Text if _node.get_description() else None,
429
+ "Value": _node.get_value(),
430
+ "ArrayDimensions": _node.get_array_dimensions(),
431
+ "ValueRank": _node.get_value_rank().name
432
+ }
433
+
434
+ else:
435
+
436
+ result = {
437
+ "NamespaceIndex": _node.nodeid.NamespaceIndex,
438
+ "NamespaceUri": _node.nodeid.NamespaceUri,
439
+ "Identifier": _node.nodeid.Identifier,
440
+ "Namespace": _node.nodeid.to_string(),
441
+ "NodeClass": _node.get_node_class().name,
442
+ "BrowseName": _node.get_browse_name().Name,
443
+ "DisplayName": _node.get_display_name().Text,
444
+ "Description": _node.get_description().Text if _node.get_description() else ''
445
+ }
446
+
447
+ return result, 200
448
+
449
+ return {}, 400
450
+
451
+ def get_nodes_attributes(self, namespaces:list)->list:
452
+ r"""
453
+ Documentation here
454
+ """
455
+ nodes = list()
456
+ for namespace in namespaces:
457
+ if self.is_connected():
458
+ node = self.get_node_attributes(node_namespace=namespace)
459
+ nodes.append(node)
460
+
461
+ return nodes
462
+
463
+ def get_referenced_nodes(self, node_id):
464
+ r"""
465
+ Documentation here
466
+ """
467
+ result = list()
468
+ if self.is_connected():
469
+ _node = self.get_node(NodeId.from_string(node_id))
470
+ referenced_nodes = _node.get_referenced_nodes()
471
+
472
+ for count, node in enumerate(referenced_nodes):
473
+
474
+ node_name = node.get_browse_name().Name
475
+ if count==0:
476
+ result.append(('OrganizedBy', node_name))
477
+ elif count==1:
478
+ result.append(('HasTypeDefinition', node_name))
479
+ else:
480
+ result.append(('Organizes', node_name))
481
+
482
+ return result, 200
483
+
484
+ return result, 400
485
+
486
+ def browse_tree(self, node):
487
+ children_list = []
488
+ if self.is_connected():
489
+ if node.get_node_class() == ua.NodeClass.Object:
490
+ children = node.get_children()
491
+ for child_id in children:
492
+ child_node = self.get_node(child_id)
493
+ display_name = child_node.get_display_name().Text or "Unnamed Node"
494
+ if display_name not in ('Aliases', 'MyObjects', 'Server', 'StaticData', 'Types', 'ReferenceTypes', 'EventTypes', 'InterfaceTypes', 'Views'):
495
+ child_dict = {
496
+ "title": display_name,
497
+ "key": child_node.nodeid.to_string(),
498
+ "NodeClass": child_node.get_node_class().name,
499
+ "children": self.browse_tree(child_node) if child_node.get_node_class() == ua.NodeClass.Object else []
500
+ }
501
+ if child_node.get_node_class() == ua.NodeClass.Variable:
502
+ variable_info = {
503
+ "title": display_name,
504
+ "key": child_node.nodeid.to_string(),
505
+ "NodeClass": child_node.get_node_class().name,
506
+ "children": []
507
+ }
508
+ for prop_id in child_node.get_properties():
509
+ prop_node = self.get_node(prop_id)
510
+ prop_display_name = prop_node.get_display_name().Text or "Unnamed Property"
511
+ try:
512
+ prop_dict = {
513
+ "title": prop_display_name,
514
+ "key": prop_node.nodeid.to_string(),
515
+ "NodeClass": prop_node.get_node_class().name,
516
+ "value": prop_node.get_value(),
517
+ "children": []
518
+ }
519
+ variable_info["children"].append(prop_dict)
520
+ except ua.uaerrors.BadWaitingForInitialData:
521
+ variable_info["children"].append({
522
+ "title": prop_display_name,
523
+ "key": prop_node.nodeid.to_string(),
524
+ "NodeClass": prop_node.get_node_class().name,
525
+ "value": None,
526
+ "children": []
527
+ })
528
+ child_dict = variable_info
529
+ children_list.append(child_dict)
530
+ return children_list
531
+
532
+ def serialize(self):
533
+ r"""
534
+ Documentation here
535
+ """
536
+ return {
537
+ 'client_id': self.get_id(),
538
+ 'server_url': self._server_url,
539
+ 'timeout': self._timeout,
540
+ 'is_opened': self.is_connected()
541
+ }