pymscada 0.1.0a5__py3-none-any.whl → 0.1.1a2__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.

Potentially problematic release.


This version of pymscada might be problematic. Click here for more details.

@@ -161,7 +161,8 @@ class ModbusServer:
161
161
  """
162
162
  self.busclient = None
163
163
  if bus_ip is not None:
164
- self.busclient = BusClient(bus_ip, bus_port)
164
+ self.busclient = BusClient(bus_ip, bus_port,
165
+ module='Modbus Server')
165
166
  self.mapping = ModbusMaps(tags)
166
167
  self.connections: list[ModbusServerConnector] = []
167
168
  for rtu in rtus:
@@ -104,7 +104,7 @@ class PingClient:
104
104
  """
105
105
  self.busclient = None
106
106
  if bus_ip is not None:
107
- self.busclient = BusClient(bus_ip, bus_port)
107
+ self.busclient = BusClient(bus_ip, bus_port, module='Ping Client')
108
108
  self.mapping = PingMaps(tags)
109
109
  self.pinger = PingClientConnector(mapping=self.mapping)
110
110
 
@@ -9,14 +9,14 @@ from pymscada.iodrivers.snmp_map import SnmpMaps
9
9
  class SnmpClientConnector:
10
10
  """Poll snmp devices, write and traps are not implemented."""
11
11
 
12
- def __init__(self, name: str, ip: str, rate: float, read: list,
12
+ def __init__(self, name: str, ip: str, rate: float, poll: list,
13
13
  community: str, mapping: SnmpMaps):
14
14
  """Set up polling client."""
15
15
  self.snmp_name = name
16
16
  self.ip = ip
17
17
  self.community = community
18
18
  self.read_oids = [snmp.ObjectType(snmp.ObjectIdentity(x))
19
- for x in read]
19
+ for x in poll]
20
20
  self.mapping = mapping
21
21
  self.periodic = Periodic(self.poll, rate)
22
22
  self.snmp_engine = snmp.SnmpEngine()
@@ -57,7 +57,7 @@ class SnmpClient:
57
57
  """
58
58
  self.busclient = None
59
59
  if bus_ip is not None:
60
- self.busclient = BusClient(bus_ip, bus_port)
60
+ self.busclient = BusClient(bus_ip, bus_port, module='SNMP Client')
61
61
  self.mapping = SnmpMaps(tags)
62
62
  self.connections: list[SnmpClientConnector] = []
63
63
  for rtu in rtus:
@@ -52,7 +52,7 @@ class SnmpMaps:
52
52
  # use the plc_name then variable name to access a list of maps.
53
53
  self.var_map: dict[str, dict[str, list[SnmpMap]]] = {}
54
54
  for tagname, v in tags.items():
55
- addr = v['addr']
55
+ addr = v['read']
56
56
  map = SnmpMap(tagname, v['type'], addr)
57
57
  if map.plc not in self.var_map:
58
58
  self.var_map[map.plc] = {}
pymscada/main.py CHANGED
@@ -10,110 +10,253 @@ from pymscada.config import Config
10
10
  from pymscada.console import Console
11
11
  from pymscada.files import Files
12
12
  from pymscada.history import History
13
+ from pymscada.opnotes import OpNotes
14
+ from pymscada.www_server import WwwServer
13
15
  from pymscada.iodrivers.logix_client import LogixClient
14
16
  from pymscada.iodrivers.modbus_client import ModbusClient
15
17
  from pymscada.iodrivers.modbus_server import ModbusServer
16
18
  from pymscada.iodrivers.ping_client import PingClient
17
19
  from pymscada.iodrivers.snmp_client import SnmpClient
18
- from pymscada.www_server import WwwServer
19
20
  from pymscada.validate import validate
20
21
 
21
22
 
22
- MODULES = {}
23
+ class Module():
24
+ """Default Module."""
25
+
26
+ name = None
27
+ help = None
28
+ epilog = None
29
+ module = None
30
+ config = True
31
+ tags = True
32
+ sub = None
33
+ await_future = True
34
+
35
+ def __init__(self, subparser: argparse._SubParsersAction):
36
+ """Add arguments common to all subparsers."""
37
+ self.sub = subparser.add_parser(
38
+ self.name, help=self.help, epilog=self.epilog)
39
+ self.sub.set_defaults(app=self)
40
+ if self.config:
41
+ self.sub.add_argument(
42
+ '--config', metavar='file', default=f'{self.name}.yaml',
43
+ help=f"Config file, default is '{self.name}.yaml'")
44
+ if self.tags:
45
+ self.sub.add_argument(
46
+ '--tags', metavar='file', default='tags.yaml',
47
+ help="Tags file, default is 'tags.yaml'")
48
+ self.sub.add_argument('--verbose', action='store_true',
49
+ help="Set level to logging.INFO")
50
+
51
+
52
+ class _Bus(Module):
53
+ """Bus Server."""
54
+
55
+ name = 'bus'
56
+ help = 'run the message bus'
57
+ tags = False
58
+
59
+ def run_once(self, options):
60
+ """Create the module."""
61
+ config = Config(options.config)
62
+ self.module = BusServer(**config)
63
+
64
+
65
+ class _WwwServer(Module):
66
+ """WWW Server Module."""
67
+
68
+ name = 'wwwserver'
69
+ help = 'serve web pages'
70
+
71
+ def run_once(self, options):
72
+ """Create the module."""
73
+ config = Config(options.config)
74
+ tag_info = dict(Config(options.tags))
75
+ self.module = WwwServer(tag_info=tag_info, **config)
76
+
77
+
78
+ class _History(Module):
79
+ """History Module."""
80
+
81
+ name = 'history'
82
+ help = 'collect and serve history'
83
+
84
+ def run_once(self, options):
85
+ """Create the module."""
86
+ config = Config(options.config)
87
+ tag_info = dict(Config(options.tags))
88
+ self.module = History(tag_info=tag_info, **config)
89
+
90
+
91
+ class _Files(Module):
92
+ """Files Module."""
93
+
94
+ name = 'files'
95
+ help = 'receive and send files'
96
+ tags = False
97
+
98
+ def run_once(self, options):
99
+ """Create the module."""
100
+ config = Config(options.config)
101
+ self.module = Files(**config)
102
+
103
+
104
+ class _OpNotes(Module):
105
+ """Operator Notes Module."""
106
+
107
+ name = 'opnotes'
108
+ help = 'present and manage operator notes'
109
+ tags = False
110
+
111
+ def run_once(self, options):
112
+ """Create the module."""
113
+ config = Config(options.config)
114
+ self.module = OpNotes(**config)
115
+
116
+
117
+ class _Console(Module):
118
+ """Bus Module."""
119
+
120
+ name = 'console'
121
+ help = 'interactive bus console'
122
+ config = False
123
+ await_future = False
124
+
125
+ def __init__(self, subparser: argparse._SubParsersAction):
126
+ super().__init__(subparser)
127
+ self.sub.add_argument(
128
+ '-p', '--port', action='store', type=int, default=1324,
129
+ help='connect to port (default: 1324)')
130
+ self.sub.add_argument(
131
+ '-i', '--ip', action='store', default='localhost',
132
+ help='connect to ip address (default: locahost)')
133
+
134
+ def run_once(self, options):
135
+ """Create the module."""
136
+ tag_info = dict(Config(options.tags))
137
+ self.module = Console(options.ip, options.port, tag_info)
138
+
139
+
140
+ class _checkout(Module):
141
+ """Bus Module."""
142
+
143
+ name = 'checkout'
144
+ help = 'create example config files'
145
+ epilog = """
146
+ To add to systemd `f="pymscada-bus" && cp config/$f.service
147
+ /lib/systemd/system && systemctl enable $f && systemctl start
148
+ $f`"""
149
+ config = False
150
+ tags = False
151
+
152
+ def __init__(self, subparser: argparse._SubParsersAction):
153
+ super().__init__(subparser)
154
+ self.sub.add_argument(
155
+ '--overwrite', action='store_true', default=False,
156
+ help='checkout may overwrite files, CARE!')
157
+ self.sub.add_argument(
158
+ '--diff', action='store_true', default=False,
159
+ help='compare default with existing')
23
160
 
161
+ def run_once(self, options):
162
+ """Create the module."""
163
+ checkout(overwrite=options.overwrite, diff=options.diff)
24
164
 
25
- async def bus(options):
26
- """Return bus module."""
27
- config = Config(options.config)
28
- return BusServer(**config)
29
165
 
166
+ class _validate(Module):
167
+ """Bus Module."""
30
168
 
31
- async def wwwserver(options):
32
- """Return wwwserver module."""
33
- config = Config(options.config)
34
- tag_info = dict(Config(options.tags))
35
- return WwwServer(tag_info=tag_info, **config)
169
+ name = 'validate'
170
+ help = 'validate config files'
171
+ config = False
172
+ tags = False
36
173
 
174
+ def __init__(self, subparser: argparse._SubParsersAction):
175
+ super().__init__(subparser)
176
+ self.sub.add_argument(
177
+ '--path', metavar='file',
178
+ help='default is current working directory')
37
179
 
38
- async def history(options):
39
- """Return history module."""
40
- config = Config(options.config)
41
- tag_info = dict(Config(options.tags))
42
- return History(tag_info=tag_info, **config)
180
+ def run_once(self, options):
181
+ """Create the module."""
182
+ r, e, p = validate(options.path)
183
+ if r:
184
+ print(f'Config files in {p} valid.')
185
+ else:
186
+ print(e)
43
187
 
44
188
 
45
- async def files(options):
46
- """TODO Return files module."""
47
- config = Config(options.config)
48
- return Files(**config)
189
+ class _LogixClient(Module):
190
+ """Bus Module."""
49
191
 
192
+ name = 'logixclient'
193
+ help = 'poll/write to logix devices'
50
194
 
51
- async def console(_options):
52
- """TODO Return console module."""
53
- return Console()
195
+ def run_once(self, options):
196
+ """Create the module."""
197
+ config = Config(options.config)
198
+ self.module = LogixClient(**config)
54
199
 
55
200
 
56
- async def _checkout(options):
57
- """Checkout files in current working directory."""
58
- checkout(overwrite=options.overwrite, diff=options.diff)
59
- return
201
+ class _ModbusServer(Module):
202
+ """Bus Module."""
60
203
 
204
+ name = 'modbusserver'
205
+ help = 'receive modbus messages'
206
+ epilog = """
207
+ Needs `setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/python3.nn` to
208
+ bind to port 502."""
209
+ tags = False
61
210
 
62
- async def _validate(options):
63
- """Validate config files."""
64
- r, e, p = validate(options.path)
65
- if r:
66
- print(f'Config files in {p} valid.')
67
- else:
68
- print(e)
69
- return
211
+ def run_once(self, options):
212
+ """Create the module."""
213
+ config = Config(options.config)
214
+ self.module = ModbusServer(**config)
70
215
 
71
216
 
72
- async def modbusserver(options):
73
- """Return modbusserver module."""
74
- config = Config(options.config)
75
- return ModbusServer(**config)
217
+ class _ModbusClient(Module):
218
+ """Bus Module."""
76
219
 
220
+ name = 'modbusclient'
221
+ help = 'poll/write to modbus devices'
222
+ tags = False
77
223
 
78
- async def modbusclient(options):
79
- """Return modbusclient module."""
80
- config = Config(options.config)
81
- return ModbusClient(**config)
224
+ def run_once(self, options):
225
+ """Create the module."""
226
+ config = Config(options.config)
227
+ self.module = ModbusClient(**config)
82
228
 
83
229
 
84
- async def logixclient(options):
85
- """Return logixclient module."""
86
- config = Config(options.config)
87
- return LogixClient(**config)
230
+ class _PingClient(Module):
231
+ """Bus Module."""
88
232
 
233
+ name = 'ping'
234
+ help = 'ping a list of addresses, return time'
235
+ epilog = """
236
+ Needs `setcap CAP_NET_RAW+ep /usr/bin/python3.nn` to open SOCK_RAW
237
+ """
238
+ tags = False
89
239
 
90
- async def ping(options):
91
- """Return logixclient module."""
92
- if sys.platform.startswith("win"):
93
- asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
94
- config = Config(options.config)
95
- return PingClient(**config)
240
+ def run_once(self, options):
241
+ """Create the module."""
242
+ if sys.platform.startswith("win"):
243
+ asyncio.set_event_loop_policy(
244
+ asyncio.WindowsSelectorEventLoopPolicy())
245
+ config = Config(options.config)
246
+ self.module = PingClient(**config)
96
247
 
97
248
 
98
- async def snmpclient(options):
99
- """Return snmpclient module."""
100
- config = Config(options.config)
101
- return SnmpClient(**config)
249
+ class _SnmpClient(Module):
250
+ """Bus Module."""
102
251
 
252
+ name = 'snmpclient'
253
+ help = 'poll snmp oids'
254
+ tags = False
103
255
 
104
- def add_subparser_defaults(
105
- parser: argparse._SubParsersAction,
106
- name: str, call, help: str, epilog: str):
107
- """Add arguments common to all subparsers."""
108
- s = parser.add_parser(name, help=help, epilog=epilog)
109
- s.set_defaults(get_module=call, module=name)
110
- s.add_argument('--config', metavar='file', default=None,
111
- help=f"Config file, default is '{name}.yaml'")
112
- s.add_argument('--tags', metavar='file', default=None,
113
- help="Tags file, default is 'tags.yaml'")
114
- s.add_argument('--verbose', action='store_true',
115
- help="Set level to logging.INFO")
116
- return s
256
+ def run_once(self, options):
257
+ """Create the module."""
258
+ config = Config(options.config)
259
+ self.module = SnmpClient(**config)
117
260
 
118
261
 
119
262
  def args(_version: str):
@@ -123,41 +266,20 @@ def args(_version: str):
123
266
  description='Connect IO, logic, applications, and webpage UI',
124
267
  epilog=f'Python Mobile SCADA {_version}'
125
268
  )
126
- subparsers = parser.add_subparsers(title='module')
127
- for module, func, help, epilog in [
128
- ['bus', bus, 'run the message bus', None],
129
- ['wwwserver', wwwserver, 'serve web pages', None],
130
- ['history', history, 'collect and serve history', None],
131
- ['files', files, 'receive and send files', None],
132
- ['console', console, 'interactive bus console', None],
133
- ['checkout', _checkout, 'create example config files', """
134
- To add to systemd `f="pymscada-bus" && cp config/$f.service
135
- /lib/systemd/system && systemctl enable $f && systemctl start
136
- $f`"""],
137
- ['validate', _validate, 'validate config files', None],
138
- ['modbusserver', modbusserver, 'receive modbus messages', """
139
- Needs `setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/python3.nn` to
140
- bind to port 502"""],
141
- ['modbusclient', modbusclient, 'poll/write to modbus devices', None],
142
- ['ping', ping, 'ping a list of addresses, return time', """
143
- Needs `setcap CAP_NET_RAW+ep /usr/bin/python3.nn` to open SOCK_RAW
144
- """],
145
- ['logixclient', logixclient, 'poll/write to logix devices', None],
146
- ['snmpclient', snmpclient, 'poll snmp oids', None],
147
- ]:
148
- modparser = add_subparser_defaults(subparsers, module, func, help,
149
- epilog)
150
- if module == 'checkout':
151
- modparser.add_argument(
152
- '--overwrite', action='store_true', default=False,
153
- help='checkout may overwrite files, CARE!')
154
- modparser.add_argument(
155
- '--diff', action='store_true', default=False,
156
- help='compare default with existing')
157
- elif module == 'validate':
158
- modparser.add_argument(
159
- '--path', metavar='file',
160
- help='default is current working directory')
269
+ s = parser.add_subparsers(title='module')
270
+ _Bus(s)
271
+ _WwwServer(s)
272
+ _History(s)
273
+ _Files(s)
274
+ _OpNotes(s)
275
+ _Console(s)
276
+ _checkout(s)
277
+ _validate(s)
278
+ _LogixClient(s)
279
+ _ModbusServer(s)
280
+ _ModbusClient(s)
281
+ _PingClient(s)
282
+ _SnmpClient(s)
161
283
  return parser.parse_args()
162
284
 
163
285
 
@@ -167,13 +289,9 @@ async def run():
167
289
  logging.warning(f'pymscada {_version} starting')
168
290
  options = args(_version)
169
291
  if options.verbose:
170
- logging.basicConfig(level=logging.INFO)
171
- if options.config is None:
172
- options.config = f'{options.module}.yaml'
173
- if options.tags is None:
174
- options.tags = 'tags.yaml'
175
- module = await options.get_module(options)
176
- if module is None:
177
- return
178
- await module.start()
179
- await asyncio.get_event_loop().create_future()
292
+ logging.getLogger().setLevel(logging.INFO)
293
+ options.app.run_once(options)
294
+ if options.app.module is not None:
295
+ await options.app.module.start()
296
+ if options.app.await_future:
297
+ await asyncio.get_event_loop().create_future()
pymscada/opnotes.py ADDED
@@ -0,0 +1,108 @@
1
+ """Operator Notes."""
2
+ import logging
3
+ import sqlite3 # note that sqlite3 has blocking calls
4
+ from pymscada.bus_client import BusClient
5
+ from pymscada.tag import Tag
6
+
7
+
8
+ class OpNotes:
9
+ """Connect to bus_ip:bus_port, store and provide Operator Notes."""
10
+
11
+ def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
12
+ db: str = None, rta_tag: str = '__opnotes__',
13
+ table: str = 'opnotes') -> None:
14
+ """
15
+ Connect to bus_ip:bus_port, serve and update operator notes database.
16
+
17
+ TODO
18
+ Write something.
19
+
20
+ Event loop must be running.
21
+ """
22
+ if db is None:
23
+ raise SystemExit('OpNotes db must be defined')
24
+ logging.warning(f'OpNotes {bus_ip} {bus_port} {db} {rta_tag}')
25
+ self.connection = sqlite3.connect(db)
26
+ self.table = table
27
+ self.cursor = self.connection.cursor()
28
+ query = (
29
+ 'CREATE TABLE IF NOT EXISTS ' + self.table +
30
+ '(oid INTEGER PRIMARY KEY ASC, '
31
+ 'datetime INTEGER, '
32
+ 'site TEXT, '
33
+ 'operator TEXT, '
34
+ 'note TEXT)'
35
+ )
36
+ self.cursor.execute(query)
37
+ self.busclient = BusClient(bus_ip, bus_port, module='OpNotes')
38
+ self.rta = Tag(rta_tag, dict)
39
+ self.busclient.add_callback_rta(rta_tag, self.rta_cb)
40
+
41
+ def rta_cb(self, request):
42
+ """Respond to Request to Author and publish on rta_tag as needed."""
43
+ if 'action' not in request:
44
+ logging.warning(f'rta_cb malformed {request}')
45
+ elif request['action'] == 'ADD':
46
+ try:
47
+ with self.connection:
48
+ self.cursor.execute(
49
+ f'INSERT INTO {self.table} (datetime, site, operator, '
50
+ 'note) VALUES(:datetime, :site, :operator, :note) '
51
+ 'RETURNING *;',
52
+ request)
53
+ res = self.cursor.fetchone()
54
+ self.rta.value = {
55
+ 'oid': res[0],
56
+ 'datetime': res[1],
57
+ 'site': res[2],
58
+ 'operator': res[3],
59
+ 'note': res[4]
60
+ }
61
+ except sqlite3.IntegrityError as error:
62
+ logging.warning(f'OpNotes rta_cb {error}')
63
+ elif request['action'] == 'MODIFY':
64
+ try:
65
+ with self.connection:
66
+ self.cursor.execute(
67
+ f'REPLACE INTO {self.table} VALUES(:oid, :datetime, '
68
+ ':site, :operator, :note) RETURNING *;', request)
69
+ res = self.cursor.fetchone()
70
+ self.rta.value = {
71
+ 'oid': res[0],
72
+ 'datetime': res[1],
73
+ 'site': res[2],
74
+ 'operator': res[3],
75
+ 'note': res[4]
76
+ }
77
+ except sqlite3.IntegrityError as error:
78
+ logging.warning(f'OpNotes rta_cb {error}')
79
+ elif request['action'] == 'DELETE':
80
+ try:
81
+ with self.connection:
82
+ self.cursor.execute(
83
+ f'DELETE FROM {self.table} WHERE oid = :oid;', request)
84
+ self.rta.value = {'oid': request['oid']}
85
+ except sqlite3.IntegrityError as error:
86
+ logging.warning(f'OpNotes rta_cb {error}')
87
+ elif request['action'] == 'HISTORY':
88
+ tag = Tag(request['reply_tag'], dict)
89
+ try:
90
+ with self.connection:
91
+ self.cursor.execute(
92
+ f'SELECT * FROM {self.table} WHERE datetime < '
93
+ ':datetime ORDER BY ABS(datetime - :datetime) '
94
+ 'LIMIT 2;', request)
95
+ for res in self.cursor.fetchall():
96
+ tag.value = {
97
+ 'oid': res[0],
98
+ 'datetime': res[1],
99
+ 'site': res[2],
100
+ 'operator': res[3],
101
+ 'note': res[4]
102
+ }
103
+ except sqlite3.IntegrityError as error:
104
+ logging.warning(f'OpNotes rta_cb {error}')
105
+
106
+ async def start(self):
107
+ """Async startup."""
108
+ await self.busclient.start()
@@ -3,12 +3,12 @@ Protocol description and protocol constants.
3
3
 
4
4
  Bus holds a tag forever, assigns a tag id forever, holds a tag value with len
5
5
  < 1000 forever, otherwise wipes periodically. So assign tagnames once, at the
6
- start of your program; use RQS as an update messenger, share whole structures
6
+ start of your program; use RTA as an update messenger, share whole structures
7
7
  rarely.
8
8
 
9
9
  - version 16-bit unsigned int == 0x01
10
10
  - command 16-bit unsigned int
11
- - tag_id 32-bit unsigned int
11
+ - tag_id 32-bit unsigned int 0 is not a valid tag_id
12
12
  - size 32-bit unsigned int
13
13
  - if size == 0xff continuation mandatory
14
14
  - size 0x00 completes an empty continuation
@@ -23,7 +23,7 @@ command
23
23
  - CMD_UNSUB id
24
24
  - no reply
25
25
  - CMD_GET id
26
- - CMD_RQS id, data is request to tag creator
26
+ - CMD_RTA id, data is request to author
27
27
  - CMD_SUB id
28
28
  - reply: SET id and value, value may be None
29
29
  - CMD_LIST
@@ -34,6 +34,7 @@ command
34
34
  - text$ matches start of tagname
35
35
  - text matches anywhere in tagname
36
36
  - reply: LIST data as space separated tagnames
37
+ - CMD_LOG data to logging.warning
37
38
  """
38
39
 
39
40
  # Tuning constants
@@ -43,25 +44,27 @@ MAX_LEN = 65535 - 14 # TODO fix server(?) when 3
43
44
  CMD_ID = 1 # query / inform tag ID - data is tagname bytes string
44
45
  CMD_SET = 2 # set a tag
45
46
  CMD_GET = 3 # get a tag
46
- CMD_RQS = 4 # request set - request passed to last tag setter
47
+ CMD_RTA = 4 # request to author
47
48
  CMD_SUB = 5 # subscribe to a tag
48
49
  CMD_UNSUB = 6 # unsubscribe from a tag
49
50
  CMD_LIST = 7 # bus list tags
50
51
  CMD_ERR = 8 # action failed
52
+ CMD_LOG = 9 # bus print a logging message
51
53
 
52
54
  CMD_TEXT = {
53
55
  1: 'CMD_ID',
54
56
  2: 'CMD_SET',
55
57
  3: 'CMD_GET',
56
- 4: 'CMD_RQS',
58
+ 4: 'CMD_RTA',
57
59
  5: 'CMD_SUB',
58
60
  6: 'CMD_UNSUB',
59
61
  7: 'CMD_LIST',
60
- 8: 'CMD_ERR'
62
+ 8: 'CMD_ERR',
63
+ 9: 'CMD_LOG'
61
64
  }
62
65
 
63
- COMMANDS = [CMD_ID, CMD_SET, CMD_GET, CMD_RQS, CMD_SUB, CMD_UNSUB, CMD_LIST,
64
- CMD_ERR]
66
+ COMMANDS = [CMD_ID, CMD_SET, CMD_GET, CMD_RTA, CMD_SUB, CMD_UNSUB, CMD_LIST,
67
+ CMD_ERR, CMD_LOG]
65
68
 
66
69
  # data types
67
70
  TYPE_INT = 1 # 64 bit signed integer
pymscada/tag.py CHANGED
@@ -18,6 +18,28 @@ TYPES = {
18
18
  }
19
19
 
20
20
 
21
+ def tag_for_web(tagname: str, tag: dict):
22
+ """Correct tag dictionary in place to be suitable for web client."""
23
+ tag['name'] = tagname
24
+ tag['id'] = None
25
+ if 'desc' not in tag:
26
+ tag['desc'] = tag.name
27
+ if 'multi' in tag:
28
+ tag['type'] = 'int'
29
+ else:
30
+ if 'type' not in tag:
31
+ tag['type'] = 'float'
32
+ else:
33
+ if tag['type'] not in TYPES:
34
+ tag['type'] = 'str'
35
+ if tag['type'] == 'int':
36
+ tag['dp'] = 0
37
+ elif tag['type'] == 'float' and 'dp' not in tag:
38
+ tag['dp'] = 2
39
+ elif tag['type'] == 'str' and 'dp' in tag:
40
+ del tag['dp']
41
+
42
+
21
43
  class UniqueTag(type):
22
44
  """Super Tag class only create unique tags for unique tag names."""
23
45