OpenOPC.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  1. ###########################################################################
  2. #
  3. # OpenOPC for Python Library Module
  4. #
  5. # Copyright (c) 2007-2012 Barry Barnreiter (barry_b@users.sourceforge.net)
  6. # Copyright (c) 2014 Anton D. Kachalov (mouse@yandex.ru)
  7. # Copyright (c) 2017 José A. Maita (jose.a.maita@gmail.com)
  8. #
  9. ###########################################################################
  10. import os
  11. import sys
  12. import time
  13. import types
  14. import string
  15. import socket
  16. import re
  17. import Pyro4.core
  18. from multiprocessing import Queue
  19. __version__ = '1.2.0'
  20. current_client = None
  21. # Win32 only modules not needed for 'open' protocol mode
  22. if os.name == 'nt':
  23. try:
  24. import win32com.client
  25. import win32com.server.util
  26. import win32event
  27. import pythoncom
  28. import pywintypes
  29. import SystemHealth
  30. # Win32 variant types
  31. pywintypes.datetime = pywintypes.TimeType
  32. vt = dict([(pythoncom.__dict__[vtype], vtype) for vtype in pythoncom.__dict__.keys() if vtype[:2] == "VT"])
  33. # Allow gencache to create the cached wrapper objects
  34. win32com.client.gencache.is_readonly = False
  35. # Under p2exe the call in gencache to __init__() does not happen
  36. # so we use Rebuild() to force the creation of the gen_py folder
  37. win32com.client.gencache.Rebuild(verbose=0)
  38. # So we can work on Windows in "open" protocol mode without the need for the win32com modules
  39. except ImportError:
  40. win32com_found = False
  41. else:
  42. win32com_found = True
  43. else:
  44. win32com_found = False
  45. # OPC Constants
  46. SOURCE_CACHE = 1
  47. SOURCE_DEVICE = 2
  48. OPC_STATUS = (0, 'Running', 'Failed', 'NoConfig', 'Suspended', 'Test')
  49. BROWSER_TYPE = (0, 'Hierarchical', 'Flat')
  50. ACCESS_RIGHTS = (0, 'Read', 'Write', 'Read/Write')
  51. OPC_QUALITY = ('Bad', 'Uncertain', 'Unknown', 'Good')
  52. OPC_CLASS = 'Matrikon.OPC.Automation;Graybox.OPC.DAWrapper;HSCOPC.Automation;RSI.OPCAutomation;OPC.Automation'
  53. OPC_SERVER = 'Hci.TPNServer;HwHsc.OPCServer;opc.deltav.1;AIM.OPC.1;Yokogawa.ExaopcDAEXQ.1;OSI.DA.1;OPC.PHDServerDA.1;Aspen.Infoplus21_DA.1;National Instruments.OPCLabVIEW;RSLinx OPC Server;KEPware.KEPServerEx.V4;Matrikon.OPC.Simulation;Prosys.OPC.Simulation;CCOPC.XMLWrapper.1;OPC.SimaticHMI.CoRtHmiRTm.1'
  54. OPC_CLIENT = 'OpenOPC'
  55. def quality_str(quality_bits):
  56. """Convert OPC quality bits to a descriptive string"""
  57. quality = (quality_bits >> 6) & 3
  58. return OPC_QUALITY[quality]
  59. def type_check(tags):
  60. """Perform a type check on a list of tags"""
  61. if type(tags) in (list, tuple):
  62. single = False
  63. elif tags == None:
  64. tags = []
  65. single = False
  66. else:
  67. tags = [tags]
  68. single = True
  69. if len([t for t in tags if type(t) not in (str,bytes)]) == 0:
  70. valid = True
  71. else:
  72. valid = False
  73. return tags, single, valid
  74. def wild2regex(string):
  75. """Convert a Unix wildcard glob into a regular expression"""
  76. return string.replace('.','\.').replace('*','.*').replace('?','.').replace('!','^')
  77. def tags2trace(tags):
  78. """Convert a list tags into a formatted string suitable for the trace callback log"""
  79. arg_str = ''
  80. for i,t in enumerate(tags[1:]):
  81. if i > 0: arg_str += ','
  82. arg_str += '%s' % t
  83. return arg_str
  84. def exceptional(func, alt_return=None, alt_exceptions=(Exception,), final=None, catch=None):
  85. """Turns exceptions into an alternative return value"""
  86. def _exceptional(*args, **kwargs):
  87. try:
  88. try:
  89. return func(*args, **kwargs)
  90. except alt_exceptions:
  91. return alt_return
  92. except:
  93. if catch: return catch(sys.exc_info(), lambda:func(*args, **kwargs))
  94. raise
  95. finally:
  96. if final: final()
  97. return _exceptional
  98. def get_sessions(host='localhost', port=7766):
  99. """Return sessions in OpenOPC Gateway Service as GUID:host hash"""
  100. import Pyro4.core
  101. server_obj = Pyro4.Proxy("PYRO:opc@{0}:{1}".format(host, port))
  102. return server_obj.get_clients()
  103. def open_client(host='localhost', port=7766):
  104. """Connect to the specified OpenOPC Gateway Service"""
  105. import Pyro4.core
  106. server_obj = Pyro4.Proxy("PYRO:opc@{0}:{1}".format(host, port))
  107. return server_obj.create_client()
  108. class TimeoutError(Exception):
  109. def __init__(self, txt):
  110. Exception.__init__(self, txt)
  111. class OPCError(Exception):
  112. def __init__(self, txt):
  113. Exception.__init__(self, txt)
  114. class GroupEvents:
  115. def __init__(self):
  116. self.client = current_client
  117. def OnDataChange(self, TransactionID, NumItems, ClientHandles, ItemValues, Qualities, TimeStamps):
  118. self.client.callback_queue.put((TransactionID, ClientHandles, ItemValues, Qualities, TimeStamps))
  119. @Pyro4.expose # needed for 4.55
  120. class client():
  121. def __init__(self, opc_class=None, client_name=None):
  122. """Instantiate OPC automation class"""
  123. self.callback_queue = Queue()
  124. pythoncom.CoInitialize()
  125. if opc_class == None:
  126. if 'OPC_CLASS' in os.environ:
  127. opc_class = os.environ['OPC_CLASS']
  128. else:
  129. opc_class = OPC_CLASS
  130. opc_class_list = opc_class.split(';')
  131. for i,c in enumerate(opc_class_list):
  132. try:
  133. self._opc = win32com.client.gencache.EnsureDispatch(c, 0)
  134. self.opc_class = c
  135. break
  136. except pythoncom.com_error as err:
  137. if i == len(opc_class_list)-1:
  138. error_msg = 'Dispatch: %s' % self._get_error_str(err)
  139. raise OPCError(error_msg)
  140. self._event = win32event.CreateEvent(None,0,0,None)
  141. self.opc_server = None
  142. self.opc_host = None
  143. self.client_name = client_name
  144. self._groups = {}
  145. self._group_tags = {}
  146. self._group_valid_tags = {}
  147. self._group_server_handles = {}
  148. self._group_handles_tag = {}
  149. self._group_hooks = {}
  150. self._open_serv = None
  151. self._open_self = None
  152. self._open_host = None
  153. self._open_port = None
  154. self._open_guid = None
  155. self._prev_serv_time = None
  156. self._tx_id = 0
  157. self.trace = None
  158. self.cpu = None
  159. def set_trace(self, trace):
  160. if self._open_serv == None:
  161. self.trace = trace
  162. def connect(self, opc_server=None, opc_host='localhost'):
  163. """Connect to the specified OPC server"""
  164. pythoncom.CoInitialize()
  165. if opc_server == None:
  166. # Initial connect using environment vars
  167. if self.opc_server == None:
  168. if 'OPC_SERVER' in os.environ:
  169. opc_server = os.environ['OPC_SERVER']
  170. else:
  171. opc_server = OPC_SERVER
  172. # Reconnect using previous server name
  173. else:
  174. opc_server = self.opc_server
  175. opc_host = self.opc_host
  176. opc_server_list = opc_server.split(';')
  177. connected = False
  178. for s in opc_server_list:
  179. try:
  180. if self.trace: self.trace('Connect(%s,%s)' % (s, opc_host))
  181. self._opc.Connect(s, opc_host)
  182. except pythoncom.com_error as err:
  183. if len(opc_server_list) == 1:
  184. error_msg = 'Connect: %s' % self._get_error_str(err)
  185. raise OPCError(error_msg)
  186. else:
  187. # Set client name since some OPC servers use it for security
  188. try:
  189. if self.client_name == None:
  190. if 'OPC_CLIENT' in os.environ:
  191. self._opc.ClientName = os.environ['OPC_CLIENT']
  192. else:
  193. self._opc.ClientName = OPC_CLIENT
  194. else:
  195. self._opc.ClientName = self.client_name
  196. except:
  197. pass
  198. connected = True
  199. break
  200. if not connected:
  201. raise OPCError('Connect: Cannot connect to any of the servers in the OPC_SERVER list')
  202. # With some OPC servers, the next OPC call immediately after Connect()
  203. # will occationally fail. Sleeping for 1/100 second seems to fix this.
  204. time.sleep(0.01)
  205. self.opc_server = opc_server
  206. if opc_host == 'localhost':
  207. opc_host = socket.gethostname()
  208. self.opc_host = opc_host
  209. # On reconnect we need to remove the old group names from OpenOPC's internal
  210. # cache since they are now invalid
  211. self._groups = {}
  212. self._group_tags = {}
  213. self._group_valid_tags = {}
  214. self._group_server_handles = {}
  215. self._group_handles_tag = {}
  216. self._group_hooks = {}
  217. def GUID(self):
  218. return self._open_guid
  219. def close(self, del_object=True):
  220. """Disconnect from the currently connected OPC server"""
  221. try:
  222. pythoncom.CoInitialize()
  223. self.remove(self.groups())
  224. except pythoncom.com_error as err:
  225. error_msg = 'Disconnect: %s' % self._get_error_str(err)
  226. raise OPCError(error_msg)
  227. except OPCError:
  228. pass
  229. finally:
  230. if self.trace: self.trace('Disconnect()')
  231. self._opc.Disconnect()
  232. # Remove this object from the open gateway service
  233. if self._open_serv and del_object:
  234. self._open_serv.release_client(self._open_self)
  235. def iread(self, tags=None, group=None, size=None, pause=0, source='hybrid', update=-1, timeout=5000, sync=False, include_error=False, rebuild=False):
  236. """Iterable version of read()"""
  237. def add_items(tags):
  238. names = list(tags)
  239. names.insert(0,0)
  240. errors = []
  241. if self.trace: self.trace('Validate(%s)' % tags2trace(names))
  242. try:
  243. errors = opc_items.Validate(len(names)-1, names)
  244. except:
  245. pass
  246. valid_tags = []
  247. valid_values = []
  248. client_handles = []
  249. if not sub_group in self._group_handles_tag:
  250. self._group_handles_tag[sub_group] = {}
  251. n = 0
  252. elif len(self._group_handles_tag[sub_group]) > 0:
  253. n = max(self._group_handles_tag[sub_group]) + 1
  254. else:
  255. n = 0
  256. for i, tag in enumerate(tags):
  257. if errors[i] == 0:
  258. valid_tags.append(tag)
  259. client_handles.append(n)
  260. self._group_handles_tag[sub_group][n] = tag
  261. n += 1
  262. elif include_error:
  263. error_msgs[tag] = self._opc.GetErrorString(errors[i])
  264. if self.trace and errors[i] != 0: self.trace('%s failed validation' % tag)
  265. client_handles.insert(0,0)
  266. valid_tags.insert(0,0)
  267. server_handles = []
  268. errors = []
  269. if self.trace: self.trace('AddItems(%s)' % tags2trace(valid_tags))
  270. try:
  271. server_handles, errors = opc_items.AddItems(len(client_handles)-1, valid_tags, client_handles)
  272. except:
  273. pass
  274. valid_tags_tmp = []
  275. server_handles_tmp = []
  276. valid_tags.pop(0)
  277. if not sub_group in self._group_server_handles:
  278. self._group_server_handles[sub_group] = {}
  279. for i, tag in enumerate(valid_tags):
  280. if errors[i] == 0:
  281. valid_tags_tmp.append(tag)
  282. server_handles_tmp.append(server_handles[i])
  283. self._group_server_handles[sub_group][tag] = server_handles[i]
  284. elif include_error:
  285. error_msgs[tag] = self._opc.GetErrorString(errors[i])
  286. valid_tags = valid_tags_tmp
  287. server_handles = server_handles_tmp
  288. return valid_tags, server_handles
  289. def remove_items(tags):
  290. if self.trace: self.trace('RemoveItems(%s)' % tags2trace(['']+tags))
  291. server_handles = [self._group_server_handles[sub_group][tag] for tag in tags]
  292. server_handles.insert(0,0)
  293. errors = []
  294. try:
  295. errors = opc_items.Remove(len(server_handles)-1, server_handles)
  296. except pythoncom.com_error as err:
  297. error_msg = 'RemoveItems: %s' % self._get_error_str(err)
  298. raise OPCError(error_msg)
  299. try:
  300. self._update_tx_time()
  301. pythoncom.CoInitialize()
  302. if include_error:
  303. sync = True
  304. if sync:
  305. update = -1
  306. tags, single, valid = type_check(tags)
  307. if not valid:
  308. raise TypeError("iread(): 'tags' parameter must be a string or a list of strings")
  309. # Group exists
  310. if group in self._groups and not rebuild:
  311. num_groups = self._groups[group]
  312. data_source = SOURCE_CACHE
  313. # Group non-existant
  314. else:
  315. if size:
  316. # Break-up tags into groups of 'size' tags
  317. tag_groups = [tags[i:i+size] for i in range(0, len(tags), size)]
  318. else:
  319. tag_groups = [tags]
  320. num_groups = len(tag_groups)
  321. data_source = SOURCE_DEVICE
  322. results = []
  323. for gid in range(num_groups):
  324. if gid > 0 and pause > 0: time.sleep(pause/1000.0)
  325. error_msgs = {}
  326. opc_groups = self._opc.OPCGroups
  327. opc_groups.DefaultGroupUpdateRate = update
  328. # Anonymous group
  329. if group == None:
  330. try:
  331. if self.trace: self.trace('AddGroup()')
  332. opc_group = opc_groups.Add()
  333. except pythoncom.com_error as err:
  334. error_msg = 'AddGroup: %s' % self._get_error_str(err)
  335. raise OPCError(error_msg)
  336. sub_group = group
  337. new_group = True
  338. else:
  339. sub_group = '%s.%d' % (group, gid)
  340. # Existing named group
  341. try:
  342. if self.trace: self.trace('GetOPCGroup(%s)' % sub_group)
  343. opc_group = opc_groups.GetOPCGroup(sub_group)
  344. new_group = False
  345. # New named group
  346. except:
  347. try:
  348. if self.trace: self.trace('AddGroup(%s)' % sub_group)
  349. opc_group = opc_groups.Add(sub_group)
  350. except pythoncom.com_error as err:
  351. error_msg = 'AddGroup: %s' % self._get_error_str(err)
  352. raise OPCError(error_msg)
  353. self._groups[str(group)] = len(tag_groups)
  354. new_group = True
  355. opc_items = opc_group.OPCItems
  356. if new_group:
  357. opc_group.IsSubscribed = 1
  358. opc_group.IsActive = 1
  359. if not sync:
  360. if self.trace: self.trace('WithEvents(%s)' % opc_group.Name)
  361. global current_client
  362. current_client = self
  363. self._group_hooks[opc_group.Name] = win32com.client.WithEvents(opc_group, GroupEvents)
  364. tags = tag_groups[gid]
  365. valid_tags, server_handles = add_items(tags)
  366. self._group_tags[sub_group] = tags
  367. self._group_valid_tags[sub_group] = valid_tags
  368. # Rebuild existing group
  369. elif rebuild:
  370. tags = tag_groups[gid]
  371. valid_tags = self._group_valid_tags[sub_group]
  372. add_tags = [t for t in tags if t not in valid_tags]
  373. del_tags = [t for t in valid_tags if t not in tags]
  374. if len(add_tags) > 0:
  375. valid_tags, server_handles = add_items(add_tags)
  376. valid_tags = self._group_valid_tags[sub_group] + valid_tags
  377. if len(del_tags) > 0:
  378. remove_items(del_tags)
  379. valid_tags = [t for t in valid_tags if t not in del_tags]
  380. self._group_tags[sub_group] = tags
  381. self._group_valid_tags[sub_group] = valid_tags
  382. if source == 'hybrid': data_source = SOURCE_DEVICE
  383. # Existing group
  384. else:
  385. tags = self._group_tags[sub_group]
  386. valid_tags = self._group_valid_tags[sub_group]
  387. if sync:
  388. server_handles = [item.ServerHandle for item in opc_items]
  389. tag_value = {}
  390. tag_quality = {}
  391. tag_time = {}
  392. tag_error = {}
  393. # Sync Read
  394. if sync:
  395. values = []
  396. errors = []
  397. qualities = []
  398. timestamps= []
  399. if len(valid_tags) > 0:
  400. server_handles.insert(0,0)
  401. if source != 'hybrid':
  402. data_source = SOURCE_CACHE if source == 'cache' else SOURCE_DEVICE
  403. if self.trace: self.trace('SyncRead(%s)' % data_source)
  404. try:
  405. values, errors, qualities, timestamps = opc_group.SyncRead(data_source, len(server_handles)-1, server_handles)
  406. except pythoncom.com_error as err:
  407. error_msg = 'SyncRead: %s' % self._get_error_str(err)
  408. raise OPCError(error_msg)
  409. for i,tag in enumerate(valid_tags):
  410. tag_value[tag] = values[i]
  411. tag_quality[tag] = qualities[i]
  412. tag_time[tag] = timestamps[i]
  413. tag_error[tag] = errors[i]
  414. # Async Read
  415. else:
  416. if len(valid_tags) > 0:
  417. if self._tx_id >= 0xFFFF:
  418. self._tx_id = 0
  419. self._tx_id += 1
  420. if source != 'hybrid':
  421. data_source = SOURCE_CACHE if source == 'cache' else SOURCE_DEVICE
  422. if self.trace: self.trace('AsyncRefresh(%s)' % data_source)
  423. try:
  424. opc_group.AsyncRefresh(data_source, self._tx_id)
  425. except pythoncom.com_error as err:
  426. error_msg = 'AsyncRefresh: %s' % self._get_error_str(err)
  427. raise OPCError(error_msg)
  428. tx_id = 0
  429. start = time.time() * 1000
  430. while tx_id != self._tx_id:
  431. now = time.time() * 1000
  432. if now - start > timeout:
  433. raise TimeoutError('Callback: Timeout waiting for data')
  434. if self.callback_queue.empty():
  435. pythoncom.PumpWaitingMessages()
  436. else:
  437. tx_id, handles, values, qualities, timestamps = self.callback_queue.get()
  438. for i,h in enumerate(handles):
  439. tag = self._group_handles_tag[sub_group][h]
  440. tag_value[tag] = values[i]
  441. tag_quality[tag] = qualities[i]
  442. tag_time[tag] = timestamps[i]
  443. for tag in tags:
  444. if tag in tag_value:
  445. if (not sync and len(valid_tags) > 0) or (sync and tag_error[tag] == 0):
  446. value = tag_value[tag]
  447. if type(value) == pywintypes.TimeType:
  448. value = str(value)
  449. quality = quality_str(tag_quality[tag])
  450. timestamp = str(tag_time[tag])
  451. else:
  452. value = None
  453. quality = 'Error'
  454. timestamp = None
  455. if include_error:
  456. error_msgs[tag] = self._opc.GetErrorString(tag_error[tag]).strip('\r\n')
  457. else:
  458. value = None
  459. quality = 'Error'
  460. timestamp = None
  461. if include_error and not tag in error_msgs:
  462. error_msgs[tag] = ''
  463. if single:
  464. if include_error:
  465. yield (value, quality, timestamp, error_msgs[tag])
  466. else:
  467. yield (value, quality, timestamp)
  468. else:
  469. if include_error:
  470. yield (tag, value, quality, timestamp, error_msgs[tag])
  471. else:
  472. yield (tag, value, quality, timestamp)
  473. if group == None:
  474. try:
  475. if not sync and opc_group.Name in self._group_hooks:
  476. if self.trace: self.trace('CloseEvents(%s)' % opc_group.Name)
  477. self._group_hooks[opc_group.Name].close()
  478. if self.trace: self.trace('RemoveGroup(%s)' % opc_group.Name)
  479. opc_groups.Remove(opc_group.Name)
  480. except pythoncom.com_error as err:
  481. error_msg = 'RemoveGroup: %s' % self._get_error_str(err)
  482. raise OPCError(error_msg)
  483. except pythoncom.com_error as err:
  484. error_msg = 'read: %s' % self._get_error_str(err)
  485. raise OPCError(error_msg)
  486. def read(self, tags=None, group=None, size=None, pause=0, source='hybrid', update=-1, timeout=5000, sync=False, include_error=False, rebuild=False):
  487. """Return list of (value, quality, time) tuples for the specified tag(s)"""
  488. tags_list, single, valid = type_check(tags)
  489. if not valid:
  490. raise TypeError("read(): 'tags' parameter must be a string or a list of strings")
  491. num_health_tags = len([t for t in tags_list if t[:1] == '@'])
  492. num_opc_tags = len([t for t in tags_list if t[:1] != '@'])
  493. if num_health_tags > 0:
  494. if num_opc_tags > 0:
  495. raise TypeError("read(): system health and OPC tags cannot be included in the same group")
  496. results = self._read_health(tags)
  497. else:
  498. results = self.iread(tags, group, size, pause, source, update, timeout, sync, include_error, rebuild)
  499. if single:
  500. return list(results)[0]
  501. else:
  502. return list(results)
  503. def _read_health(self, tags):
  504. """Return values of special system health monitoring tags"""
  505. self._update_tx_time()
  506. tags, single, valid = type_check(tags)
  507. time_str = time.strftime('%x %H:%M:%S')
  508. results = []
  509. for t in tags:
  510. if t == '@MemFree': value = SystemHealth.mem_free()
  511. elif t == '@MemUsed': value = SystemHealth.mem_used()
  512. elif t == '@MemTotal': value = SystemHealth.mem_total()
  513. elif t == '@MemPercent': value = SystemHealth.mem_percent()
  514. elif t == '@DiskFree': value = SystemHealth.disk_free()
  515. elif t == '@SineWave': value = SystemHealth.sine_wave()
  516. elif t == '@SawWave': value = SystemHealth.saw_wave()
  517. elif t == '@CpuUsage':
  518. if self.cpu == None:
  519. self.cpu = SystemHealth.CPU()
  520. time.sleep(0.1)
  521. value = self.cpu.get_usage()
  522. else:
  523. value = None
  524. m = re.match('@TaskMem\((.*?)\)', t)
  525. if m:
  526. image_name = m.group(1)
  527. value = SystemHealth.task_mem(image_name)
  528. m = re.match('@TaskCpu\((.*?)\)', t)
  529. if m:
  530. image_name = m.group(1)
  531. value = SystemHealth.task_cpu(image_name)
  532. m = re.match('@TaskExists\((.*?)\)', t)
  533. if m:
  534. image_name = m.group(1)
  535. value = SystemHealth.task_exists(image_name)
  536. if value == None:
  537. quality = 'Error'
  538. else:
  539. quality = 'Good'
  540. if single:
  541. results.append((value, quality, time_str))
  542. else:
  543. results.append((t, value, quality, time_str))
  544. return results
  545. def iwrite(self, tag_value_pairs, size=None, pause=0, include_error=False):
  546. """Iterable version of write()"""
  547. try:
  548. self._update_tx_time()
  549. pythoncom.CoInitialize()
  550. def _valid_pair(p):
  551. if type(p) in (list, tuple) and len(p) >= 2 and type(p[0]) in (str,bytes):
  552. return True
  553. else:
  554. return False
  555. if type(tag_value_pairs) not in (list, tuple):
  556. raise TypeError("write(): 'tag_value_pairs' parameter must be a (tag, value) tuple or a list of (tag,value) tuples")
  557. if tag_value_pairs == None:
  558. tag_value_pairs = ['']
  559. single = False
  560. elif type(tag_value_pairs[0]) in (str,bytes):
  561. tag_value_pairs = [tag_value_pairs]
  562. single = True
  563. else:
  564. single = False
  565. invalid_pairs = [p for p in tag_value_pairs if not _valid_pair(p)]
  566. if len(invalid_pairs) > 0:
  567. raise TypeError("write(): 'tag_value_pairs' parameter must be a (tag, value) tuple or a list of (tag,value) tuples")
  568. names = [tag[0] for tag in tag_value_pairs]
  569. tags = [tag[0] for tag in tag_value_pairs]
  570. values = [tag[1] for tag in tag_value_pairs]
  571. # Break-up tags & values into groups of 'size' tags
  572. if size:
  573. name_groups = [names[i:i+size] for i in range(0, len(names), size)]
  574. tag_groups = [tags[i:i+size] for i in range(0, len(tags), size)]
  575. value_groups = [values[i:i+size] for i in range(0, len(values), size)]
  576. else:
  577. name_groups = [names]
  578. tag_groups = [tags]
  579. value_groups = [values]
  580. num_groups = len(tag_groups)
  581. status = []
  582. for gid in range(num_groups):
  583. if gid > 0 and pause > 0: time.sleep(pause/1000.0)
  584. opc_groups = self._opc.OPCGroups
  585. opc_group = opc_groups.Add()
  586. opc_items = opc_group.OPCItems
  587. names = name_groups[gid]
  588. tags = tag_groups[gid]
  589. values = value_groups[gid]
  590. names.insert(0,0)
  591. errors = []
  592. try:
  593. errors = opc_items.Validate(len(names)-1, names)
  594. except:
  595. pass
  596. n = 1
  597. valid_tags = []
  598. valid_values = []
  599. client_handles = []
  600. error_msgs = {}
  601. for i, tag in enumerate(tags):
  602. if errors[i] == 0:
  603. valid_tags.append(tag)
  604. valid_values.append(values[i])
  605. client_handles.append(n)
  606. error_msgs[tag] = ''
  607. n += 1
  608. elif include_error:
  609. error_msgs[tag] = self._opc.GetErrorString(errors[i])
  610. client_handles.insert(0,0)
  611. valid_tags.insert(0,0)
  612. server_handles = []
  613. errors = []
  614. try:
  615. server_handles, errors = opc_items.AddItems(len(client_handles)-1, valid_tags, client_handles)
  616. except:
  617. pass
  618. valid_tags_tmp = []
  619. valid_values_tmp = []
  620. server_handles_tmp = []
  621. valid_tags.pop(0)
  622. for i, tag in enumerate(valid_tags):
  623. if errors[i] == 0:
  624. valid_tags_tmp.append(tag)
  625. valid_values_tmp.append(valid_values[i])
  626. server_handles_tmp.append(server_handles[i])
  627. error_msgs[tag] = ''
  628. elif include_error:
  629. error_msgs[tag] = self._opc.GetErrorString(errors[i])
  630. valid_tags = valid_tags_tmp
  631. valid_values = valid_values_tmp
  632. server_handles = server_handles_tmp
  633. server_handles.insert(0,0)
  634. valid_values.insert(0,0)
  635. errors = []
  636. if len(valid_values) > 1:
  637. try:
  638. errors = opc_group.SyncWrite(len(server_handles)-1, server_handles, valid_values)
  639. except:
  640. pass
  641. n = 0
  642. for tag in tags:
  643. if tag in valid_tags:
  644. if errors[n] == 0:
  645. status = 'Success'
  646. else:
  647. status = 'Error'
  648. if include_error: error_msgs[tag] = self._opc.GetErrorString(errors[n])
  649. n += 1
  650. else:
  651. status = 'Error'
  652. # OPC servers often include newline and carriage return characters
  653. # in their error message strings, so remove any found.
  654. if include_error: error_msgs[tag] = error_msgs[tag].strip('\r\n')
  655. if single:
  656. if include_error:
  657. yield (status, error_msgs[tag])
  658. else:
  659. yield status
  660. else:
  661. if include_error:
  662. yield (tag, status, error_msgs[tag])
  663. else:
  664. yield (tag, status)
  665. opc_groups.Remove(opc_group.Name)
  666. except pythoncom.com_error as err:
  667. error_msg = 'write: %s' % self._get_error_str(err)
  668. raise OPCError(error_msg)
  669. def write(self, tag_value_pairs, size=None, pause=0, include_error=False):
  670. """Write list of (tag, value) pair(s) to the server"""
  671. if type(tag_value_pairs) in (list, tuple) and type(tag_value_pairs[0]) in (list, tuple):
  672. single = False
  673. else:
  674. single = True
  675. status = self.iwrite(tag_value_pairs, size, pause, include_error)
  676. if single:
  677. return list(status)[0]
  678. else:
  679. return list(status)
  680. def groups(self):
  681. """Return a list of active tag groups"""
  682. return self._groups.keys()
  683. def remove(self, groups):
  684. """Remove the specified tag group(s)"""
  685. try:
  686. pythoncom.CoInitialize()
  687. opc_groups = self._opc.OPCGroups
  688. if type(groups) in (str,bytes):
  689. groups = [groups]
  690. single = True
  691. else:
  692. single = False
  693. status = []
  694. for group in groups:
  695. if group in self._groups:
  696. for i in range(self._groups[group]):
  697. sub_group = '%s.%d' % (group, i)
  698. if sub_group in self._group_hooks:
  699. if self.trace: self.trace('CloseEvents(%s)' % sub_group)
  700. self._group_hooks[sub_group].close()
  701. try:
  702. if self.trace: self.trace('RemoveGroup(%s)' % sub_group)
  703. errors = opc_groups.Remove(sub_group)
  704. except pythoncom.com_error as err:
  705. error_msg = 'RemoveGroup: %s' % self._get_error_str(err)
  706. raise OPCError(error_msg)
  707. del(self._group_tags[sub_group])
  708. del(self._group_valid_tags[sub_group])
  709. del(self._group_handles_tag[sub_group])
  710. del(self._group_server_handles[sub_group])
  711. del(self._groups[group])
  712. except pythoncom.com_error as err:
  713. error_msg = 'remove: %s' % self._get_error_str(err)
  714. raise OPCError(error_msg)
  715. def iproperties(self, tags, id=None):
  716. """Iterable version of properties()"""
  717. try:
  718. self._update_tx_time()
  719. pythoncom.CoInitialize()
  720. tags, single_tag, valid = type_check(tags)
  721. if not valid:
  722. raise TypeError("properties(): 'tags' parameter must be a string or a list of strings")
  723. try:
  724. id.remove(0)
  725. include_name = True
  726. except:
  727. include_name = False
  728. if id != None:
  729. descriptions= []
  730. if isinstance(id, list) or isinstance(id, tuple):
  731. property_id = list(id)
  732. single_property = False
  733. else:
  734. property_id = [id]
  735. single_property = True
  736. for i in property_id:
  737. descriptions.append('Property id %d' % i)
  738. else:
  739. single_property = False
  740. properties = []
  741. for tag in tags:
  742. if id == None:
  743. descriptions = []
  744. property_id = []
  745. count, property_id, descriptions, datatypes = self._opc.QueryAvailableProperties(tag)
  746. # Remove bogus negative property id (not sure why this sometimes happens)
  747. tag_properties = list(map(lambda x, y: (x, y), property_id, descriptions))
  748. property_id = [p for p, d in tag_properties if p > 0]
  749. descriptions = [d for p, d in tag_properties if p > 0]
  750. property_id.insert(0, 0)
  751. values = []
  752. errors = []
  753. values, errors = self._opc.GetItemProperties(tag, len(property_id)-1, property_id)
  754. property_id.pop(0)
  755. values = [str(v) if type(v) == pywintypes.TimeType else v for v in values]
  756. # Replace variant id with type strings
  757. try:
  758. i = property_id.index(1)
  759. values[i] = vt[values[i]]
  760. except:
  761. pass
  762. # Replace quality bits with quality strings
  763. try:
  764. i = property_id.index(3)
  765. values[i] = quality_str(values[i])
  766. except:
  767. pass
  768. # Replace access rights bits with strings
  769. try:
  770. i = property_id.index(5)
  771. values[i] = ACCESS_RIGHTS[values[i]]
  772. except:
  773. pass
  774. if id != None:
  775. if single_property:
  776. if single_tag:
  777. tag_properties = values
  778. else:
  779. tag_properties = [values]
  780. else:
  781. tag_properties = list(map(lambda x, y: (x, y), property_id, values))
  782. else:
  783. tag_properties = list(map(lambda x, y, z: (x, y, z), property_id, descriptions, values))
  784. tag_properties.insert(0, (0, 'Item ID (virtual property)', tag))
  785. if include_name: tag_properties.insert(0, (0, tag))
  786. if not single_tag: tag_properties = [tuple([tag] + list(p)) for p in tag_properties]
  787. for p in tag_properties: yield p
  788. except pythoncom.com_error as err:
  789. error_msg = 'properties: %s' % self._get_error_str(err)
  790. raise OPCError(error_msg)
  791. def properties(self, tags, id=None):
  792. """Return list of property tuples (id, name, value) for the specified tag(s) """
  793. if type(tags) not in (list, tuple) and type(id) not in (type(None), list, tuple):
  794. single = True
  795. else:
  796. single = False
  797. props = self.iproperties(tags, id)
  798. if single:
  799. return list(props)[0]
  800. else:
  801. return list(props)
  802. def ilist(self, paths='*', recursive=False, flat=False, include_type=False):
  803. """Iterable version of list()"""
  804. try:
  805. self._update_tx_time()
  806. pythoncom.CoInitialize()
  807. try:
  808. browser = self._opc.CreateBrowser()
  809. # For OPC servers that don't support browsing
  810. except:
  811. return
  812. paths, single, valid = type_check(paths)
  813. if not valid:
  814. raise TypeError("list(): 'paths' parameter must be a string or a list of strings")
  815. if len(paths) == 0: paths = ['*']
  816. nodes = {}
  817. for path in paths:
  818. if flat:
  819. browser.MoveToRoot()
  820. browser.Filter = ''
  821. browser.ShowLeafs(True)
  822. pattern = re.compile('^%s$' % wild2regex(path) , re.IGNORECASE)
  823. matches = filter(pattern.search, browser)
  824. if include_type: matches = [(x, node_type) for x in matches]
  825. for node in matches: yield node
  826. continue
  827. queue = []
  828. queue.append(path)
  829. while len(queue) > 0:
  830. tag = queue.pop(0)
  831. browser.MoveToRoot()
  832. browser.Filter = ''
  833. pattern = None
  834. path_str = '/'
  835. path_list = tag.replace('.','/').split('/')
  836. path_list = [p for p in path_list if len(p) > 0]
  837. found_filter = False
  838. path_postfix = '/'
  839. for i, p in enumerate(path_list):
  840. if found_filter:
  841. path_postfix += p + '/'
  842. elif p.find('*') >= 0:
  843. pattern = re.compile('^%s$' % wild2regex(p) , re.IGNORECASE)
  844. found_filter = True
  845. elif len(p) != 0:
  846. pattern = re.compile('^.*$')
  847. browser.ShowBranches()
  848. # Branch node, so move down
  849. if len(browser) > 0:
  850. try:
  851. browser.MoveDown(p)
  852. path_str += p + '/'
  853. except:
  854. if i < len(path_list)-1: return
  855. pattern = re.compile('^%s$' % wild2regex(p) , re.IGNORECASE)
  856. # Leaf node, so append all remaining path parts together
  857. # to form a single search expression
  858. else:
  859. p = string.join(path_list[i:], '.')
  860. pattern = re.compile('^%s$' % wild2regex(p) , re.IGNORECASE)
  861. break
  862. browser.ShowBranches()
  863. if len(browser) == 0:
  864. browser.ShowLeafs(False)
  865. lowest_level = True
  866. node_type = 'Leaf'
  867. else:
  868. lowest_level = False
  869. node_type = 'Branch'
  870. matches = filter(pattern.search, browser)
  871. if not lowest_level and recursive:
  872. queue += [path_str + x + path_postfix for x in matches]
  873. else:
  874. if lowest_level: matches = [exceptional(browser.GetItemID,x)(x) for x in matches]
  875. if include_type: matches = [(x, node_type) for x in matches]
  876. for node in matches:
  877. if not node in nodes: yield node
  878. nodes[node] = True
  879. except pythoncom.com_error as err:
  880. error_msg = 'list: %s' % self._get_error_str(err)
  881. raise OPCError(error_msg)
  882. def list(self, paths='*', recursive=False, flat=False, include_type=False):
  883. """Return list of item nodes at specified path(s) (tree browser)"""
  884. nodes = self.ilist(paths, recursive, flat, include_type)
  885. return list(nodes)
  886. def servers(self, opc_host='localhost'):
  887. """Return list of available OPC servers"""
  888. try:
  889. pythoncom.CoInitialize()
  890. servers = self._opc.GetOPCServers(opc_host)
  891. servers = [s for s in servers if s != None]
  892. return servers
  893. except pythoncom.com_error as err:
  894. error_msg = 'servers: %s' % self._get_error_str(err)
  895. raise OPCError(error_msg)
  896. def info(self):
  897. """Return list of (name, value) pairs about the OPC server"""
  898. try:
  899. self._update_tx_time()
  900. pythoncom.CoInitialize()
  901. info_list = []
  902. if self._open_serv:
  903. mode = 'OpenOPC'
  904. else:
  905. mode = 'DCOM'
  906. info_list += [('Protocol', mode)]
  907. if mode == 'OpenOPC':
  908. info_list += [('Gateway Host', '%s:%s' % (self._open_host, self._open_port))]
  909. info_list += [('Gateway Version', '%s' % __version__)]
  910. info_list += [('Class', self.opc_class)]
  911. info_list += [('Client Name', self._opc.ClientName)]
  912. info_list += [('OPC Host', self.opc_host)]
  913. info_list += [('OPC Server', self._opc.ServerName)]
  914. info_list += [('State', OPC_STATUS[self._opc.ServerState])]
  915. info_list += [('Version', '%d.%d (Build %d)' % (self._opc.MajorVersion, self._opc.MinorVersion, self._opc.BuildNumber))]
  916. try:
  917. browser = self._opc.CreateBrowser()
  918. browser_type = BROWSER_TYPE[browser.Organization]
  919. except:
  920. browser_type = 'Not Supported'
  921. info_list += [('Browser', browser_type)]
  922. info_list += [('Start Time', str(self._opc.StartTime))]
  923. info_list += [('Current Time', str(self._opc.CurrentTime))]
  924. info_list += [('Vendor', self._opc.VendorInfo)]
  925. return info_list
  926. except pythoncom.com_error as err:
  927. error_msg = 'info: %s' % self._get_error_str(err)
  928. raise OPCError(error_msg)
  929. def ping(self):
  930. """Check if we are still talking to the OPC server"""
  931. try:
  932. # Convert OPC server time to milliseconds
  933. opc_serv_time = int(float(self._opc.CurrentTime) * 1000000.0)
  934. if opc_serv_time == self._prev_serv_time:
  935. return False
  936. else:
  937. self._prev_serv_time = opc_serv_time
  938. return True
  939. except pythoncom.com_error:
  940. return False
  941. def _get_error_str(self, err):
  942. """Return the error string for a OPC or COM error code"""
  943. hr, msg, exc, arg = err.args
  944. if exc == None:
  945. error_str = str(msg)
  946. else:
  947. scode = exc[5]
  948. try:
  949. opc_err_str = unicode(self._opc.GetErrorString(scode)).strip('\r\n')
  950. except:
  951. opc_err_str = None
  952. try:
  953. com_err_str = unicode(pythoncom.GetScodeString(scode)).strip('\r\n')
  954. except:
  955. com_err_str = None
  956. # OPC error codes and COM error codes are overlapping concepts,
  957. # so we combine them together into a single error message.
  958. if opc_err_str == None and com_err_str == None:
  959. error_str = str(scode)
  960. elif opc_err_str == com_err_str:
  961. error_str = opc_err_str
  962. elif opc_err_str == None:
  963. error_str = com_err_str
  964. elif com_err_str == None:
  965. error_str = opc_err_str
  966. else:
  967. error_str = '%s (%s)' % (opc_err_str, com_err_str)
  968. return error_str
  969. def _update_tx_time(self):
  970. """Update the session's last transaction time in the Gateway Service"""
  971. if self._open_serv:
  972. self._open_serv._tx_times[self._open_guid] = time.time()
  973. def __getitem__(self, key):
  974. """Read single item (tag as dictionary key)"""
  975. value, quality, time = self.read(key)
  976. return value
  977. def __setitem__(self, key, value):
  978. """Write single item (tag as dictionary key)"""
  979. self.write((key, value))
  980. return