pymscada 0.0.15__py3-none-any.whl → 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.

Potentially problematic release.


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

@@ -139,7 +139,10 @@ class ModbusMaps():
139
139
  """Make the maps."""
140
140
  for tagname, v in self.tags.items():
141
141
  dtype = v['type']
142
- addr = v['addr']
142
+ try:
143
+ addr = v['read']
144
+ except KeyError:
145
+ addr = v['addr']
143
146
  map = ModbusMap(tagname, dtype, addr, self.data, self.value_chg)
144
147
  size = DTYPES[dtype][3]
145
148
  name, unit, file, word = addr.split(':')
@@ -37,80 +37,65 @@ class ModbusServerProtocol:
37
37
  transport.set_write_buffer_limits(high=0)
38
38
  self.transport = transport
39
39
 
40
- def data_received(self, recv):
41
- """Received."""
42
- # logging.info(f'received: {recv}')
40
+ def unpack_mb(self):
41
+ """Return complete modbus packets and trim the buffer."""
43
42
  start = 0
44
- self.buffer += recv
45
43
  while True:
46
44
  buf_len = len(self.buffer)
47
45
  if buf_len < 6 + start: # enough to unpack length
48
- self.buffer = self.buffer[start:]
49
46
  break
50
- (_mbap_tr, _mbap_pr, mbap_len) = unpack_from(
51
- ">3H", self.buffer, start
52
- )
53
- if buf_len < 6 + mbap_len: # there is a complete message
54
- self.buffer = self.buffer[start:]
47
+ mbap_tr, mbap_pr, mbap_len = unpack_from(">3H", self.buffer, start)
48
+ if buf_len < start + 6 + mbap_len: # there is a complete message
55
49
  break
56
50
  end = start + 6 + mbap_len
57
- self.msg = self.buffer[start:end]
51
+ yield self.buffer[start:end]
58
52
  start = end
59
- self.process()
60
- if self.msg is not None:
61
- self.transport.write(self.msg)
62
- self.msg = b""
53
+ self.buffer = self.buffer[end:]
54
+
55
+ def data_received(self, recv):
56
+ """Received."""
57
+ # logging.info(f'received: {recv}')
58
+ self.buffer += recv
59
+ for msg in self.unpack_mb():
60
+ reply = self.process(msg)
61
+ self.transport.write(reply)
63
62
 
64
63
  def datagram_received(self, recv, addr):
65
64
  """Received."""
66
- start = 0
67
- buffer = recv
68
- while True:
69
- buf_len = len(buffer)
70
- if buf_len < 6 + start: # enough to unpack length
71
- buffer = buffer[start:]
72
- break
73
- (_mbap_tr, _mbap_pr, mbap_len) = unpack_from(">3H", buffer, start)
74
- if buf_len < 6 + mbap_len: # there is a complete message
75
- buffer = buffer[start:]
76
- break
77
- end = start + 6 + mbap_len
78
- self.msg = buffer[start:end]
79
- start = end
80
- self.process()
81
- if self.msg is not None:
82
- self.transport.sendto(self.msg, addr)
83
- self.msg = b""
65
+ self.buffer = recv
66
+ for msg in self.unpack_mb():
67
+ reply = self.process(msg)
68
+ self.transport.sendto(reply, addr)
84
69
 
85
- def process(self):
70
+ def process(self, msg):
86
71
  """Process."""
87
72
  mbap_tr, mbap_pr, _mbap_len, mbap_unit, pdu_fc = unpack_from(
88
- ">3H2B", self.msg, 0)
73
+ ">3H2B", msg, 0)
89
74
  if pdu_fc == 3: # Read Holding Registers
90
75
  # Return 0 for missing addresses
91
- pdu_start, pdu_count = unpack_from(">2H", self.msg, 8)
76
+ pdu_start, pdu_count = unpack_from(">2H", msg, 8)
92
77
  data = self.mapping.get_data(self.name, mbap_unit, '4x', pdu_start,
93
78
  pdu_count)
94
79
  data_len = len(data)
95
80
  msg_len = 3 + data_len
96
- self.msg = pack('>3H3B', mbap_tr, mbap_pr, msg_len, mbap_unit,
97
- pdu_fc, data_len) + data
81
+ reply = pack('>3H3B', mbap_tr, mbap_pr, msg_len, mbap_unit,
82
+ pdu_fc, data_len) + data
98
83
  elif pdu_fc == 6: # Set Single Register 4x
99
- pdu_start = unpack_from(">H", self.msg, 8)[0]
100
- data = bytearray(self.msg[10:12])
84
+ pdu_start = unpack_from(">H", msg, 8)[0]
85
+ data = bytearray(msg[10:12])
101
86
  self.mapping.set_data(self.name, mbap_unit, '4x', pdu_start,
102
87
  1, data)
103
88
  msg_len = 6
104
- self.msg = pack(">3H2BH", mbap_tr, mbap_pr, msg_len, mbap_unit,
105
- pdu_fc, pdu_start) + data
89
+ reply = pack(">3H2BH", mbap_tr, mbap_pr, msg_len, mbap_unit,
90
+ pdu_fc, pdu_start) + data
106
91
  elif pdu_fc == 16: # Set Multiple Registers 4x
107
- pdu_start, pdu_count, pdu_bytes = unpack_from(">2HB", self.msg, 8)
108
- data = bytearray(self.msg[13:13 + pdu_bytes])
92
+ pdu_start, pdu_count, pdu_bytes = unpack_from(">2HB", msg, 8)
93
+ data = bytearray(msg[13:13 + pdu_bytes])
109
94
  self.mapping.set_data(self.name, mbap_unit, '4x', pdu_start,
110
95
  pdu_count, data)
111
96
  msg_len = 6
112
- self.msg = pack(">3H2B2H", mbap_tr, mbap_pr, msg_len, mbap_unit,
113
- pdu_fc, pdu_start, pdu_count)
97
+ reply = pack(">3H2B2H", mbap_tr, mbap_pr, msg_len, mbap_unit,
98
+ pdu_fc, pdu_start, pdu_count)
114
99
  else:
115
100
  # Unsupported, send the standard Modbus exception
116
101
  logging.warn(
@@ -118,8 +103,9 @@ class ModbusServerProtocol:
118
103
  f" attempted FC {pdu_fc}"
119
104
  )
120
105
  msg_len = 3
121
- self.msg = pack(">3H2BB", mbap_tr, mbap_pr, msg_len, mbap_unit,
122
- pdu_fc + 128, 1)
106
+ reply = pack(">3H2BB", mbap_tr, mbap_pr, msg_len, mbap_unit,
107
+ pdu_fc + 128, 1)
108
+ return reply
123
109
 
124
110
 
125
111
  class ModbusServerConnector:
@@ -0,0 +1,120 @@
1
+ """Network monitoring / scanning."""
2
+ import asyncio
3
+ import logging
4
+ import socket
5
+ import struct
6
+ import time
7
+ from pymscada.bus_client import BusClient
8
+ from pymscada.periodic import Periodic
9
+ from pymscada.iodrivers.ping_map import PingMaps
10
+
11
+
12
+ ICMP_REQUEST = 8
13
+ ICMP_REPLY = 0
14
+ RATE = 60
15
+
16
+
17
+ def checksum_chat(data):
18
+ """Calculate ICMP Checksum."""
19
+ if len(data) & 1: # If the packet length is odd
20
+ data += b'\0'
21
+ res = 0
22
+ # Process two bytes at a time
23
+ for i in range(0, len(data), 2):
24
+ res += (data[i] << 8) + data[i + 1]
25
+ res = (res >> 16) + (res & 0xffff)
26
+ res += res >> 16
27
+ return (~res) & 0xffff # Return ones' complement of the result
28
+
29
+
30
+ class PingClientConnector:
31
+ """Ping a list of addresses."""
32
+
33
+ def __init__(self, mapping: PingMaps):
34
+ """Accept list of addresses, ip or name."""
35
+ self.mapping = mapping
36
+ self.dns = {}
37
+ self.addr_info = {}
38
+ self.socket = None
39
+ self.ping_id = 0
40
+ self.ping_dict = {}
41
+
42
+ async def poll(self):
43
+ """Do pings."""
44
+ self.reply_dict = {}
45
+ if self.socket is None:
46
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_RAW,
47
+ socket.IPPROTO_ICMP)
48
+ asyncio.get_event_loop().add_reader(self.socket,
49
+ self.read_response)
50
+ for ping_id, address in list(self.ping_dict.items()):
51
+ logging.info(f'failed {self.dns[address]} {ping_id}')
52
+ self.mapping.polled_data(self.dns[address], float('NAN'))
53
+ del self.ping_dict[ping_id]
54
+ self.send_ping()
55
+
56
+ def send_ping(self):
57
+ """Build and send ping messages."""
58
+ for address in self.dns.keys():
59
+ self.ping_id = (self.ping_id + 1) & 0xffff
60
+ self.ping_dict[self.ping_id] = address
61
+ logging.info(f'ping {address} id {self.ping_id}')
62
+ header = struct.pack("!BbHHh", ICMP_REQUEST, 0, 0, self.ping_id, 1)
63
+ data = struct.pack("!d", time.perf_counter()) + (60 * b'\0')
64
+ checksum = checksum_chat(header + data)
65
+ packet = struct.pack("!BbHHh", ICMP_REQUEST, 0, checksum,
66
+ self.ping_id, 1) + data
67
+ self.socket.sendto(packet, (address, 0))
68
+
69
+ def read_response(self):
70
+ """Match ping response."""
71
+ data, address = self.socket.recvfrom(1024)
72
+ msgtype, _, _, ping_id, _ = struct.unpack('!BBHHH', data[20:28])
73
+ if msgtype != ICMP_REPLY:
74
+ return
75
+ if ping_id in self.ping_dict:
76
+ latency = 1000 * (time.perf_counter() -
77
+ struct.unpack('!d', data[28:36])[0])
78
+ name = self.dns[address[0]]
79
+ logging.info(f'success {name} {ping_id} {latency}ms')
80
+ self.mapping.polled_data(name, latency)
81
+ del self.ping_dict[ping_id]
82
+
83
+ async def start(self):
84
+ """Start pinging."""
85
+ loop = asyncio.get_event_loop()
86
+ for address in self.mapping.var_map.keys():
87
+ info = await loop.getaddrinfo(address, None, family=socket.AF_INET,
88
+ type=socket.SOCK_STREAM)
89
+ ip = info[0][4][0]
90
+ self.dns[ip] = address
91
+ self.periodic = Periodic(self.poll, RATE)
92
+ await self.periodic.start()
93
+
94
+
95
+ class PingClient:
96
+ """Ping client."""
97
+
98
+ def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
99
+ tags: dict = {}) -> None:
100
+ """
101
+ Connect to bus on bus_ip:bus_port, ping a list.
102
+
103
+ Event loop must be running.
104
+ """
105
+ self.busclient = None
106
+ if bus_ip is not None:
107
+ self.busclient = BusClient(bus_ip, bus_port)
108
+ self.mapping = PingMaps(tags)
109
+ self.pinger = PingClientConnector(mapping=self.mapping)
110
+
111
+ async def _poll(self):
112
+ """For testing."""
113
+ for connection in self.connections:
114
+ await connection.poll()
115
+
116
+ async def start(self):
117
+ """Start bus connection and PLC polling."""
118
+ if self.busclient is not None:
119
+ await self.busclient.start()
120
+ await self.pinger.start()
@@ -0,0 +1,43 @@
1
+ """Map between snmp MIB and Tag."""
2
+ from time import time
3
+ from pymscada.tag import Tag
4
+
5
+
6
+ class PingMap:
7
+ """Do value updates for each tag."""
8
+
9
+ def __init__(self, tagname: str, addr: str):
10
+ """Initialise MIB map and Tag."""
11
+ self.last_value = None
12
+ self.tag = Tag(tagname, float)
13
+ self.addr = addr
14
+ self.map_bus = id(self)
15
+
16
+ def set_tag_value(self, value, time_us):
17
+ """Pass update from IO driver to tag value."""
18
+ if self.last_value is None:
19
+ self.last_value = value
20
+ return
21
+ if self.last_value != value:
22
+ self.tag.value = value, time_us, self.map_bus
23
+
24
+
25
+ class PingMaps:
26
+ """Link tags with protocol connector."""
27
+
28
+ def __init__(self, tags: dict):
29
+ """Collect maps based on a tag dictionary."""
30
+ # use the tagname to access the map.
31
+ self.tag_map: dict[str, PingMap] = {}
32
+ # use the plc_name then variable name to access a list of maps.
33
+ self.var_map: dict[str, PingMap] = {}
34
+ for tagname, v in tags.items():
35
+ addr = v['addr']
36
+ map = PingMap(tagname, addr)
37
+ self.var_map[addr] = map
38
+ self.tag_map[tagname] = map
39
+
40
+ def polled_data(self, address, latency):
41
+ """Pass updates read from the PLC to the tags."""
42
+ time_us = int(time() * 1e6)
43
+ self.var_map[address].set_tag_value(latency, time_us)
@@ -1,3 +1,4 @@
1
+ """Poll SNMP OIDs from devices."""
1
2
  import logging
2
3
  import pysnmp.hlapi.asyncio as snmp
3
4
  from pymscada.bus_client import BusClient
@@ -8,14 +9,14 @@ from pymscada.iodrivers.snmp_map import SnmpMaps
8
9
  class SnmpClientConnector:
9
10
  """Poll snmp devices, write and traps are not implemented."""
10
11
 
11
- def __init__(self, name: str, ip:str, rate: float, read: list,
12
+ def __init__(self, name: str, ip: str, rate: float, poll: list,
12
13
  community: str, mapping: SnmpMaps):
13
14
  """Set up polling client."""
14
15
  self.snmp_name = name
15
16
  self.ip = ip
16
17
  self.community = community
17
18
  self.read_oids = [snmp.ObjectType(snmp.ObjectIdentity(x))
18
- for x in read]
19
+ for x in poll]
19
20
  self.mapping = mapping
20
21
  self.periodic = Periodic(self.poll, rate)
21
22
  self.snmp_engine = snmp.SnmpEngine()
@@ -45,6 +46,7 @@ class SnmpClientConnector:
45
46
 
46
47
 
47
48
  class SnmpClient:
49
+ """SNMP client."""
48
50
 
49
51
  def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
50
52
  rtus: dict = {}, tags: dict = {}) -> None:
@@ -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
@@ -1,7 +1,9 @@
1
1
  """Main server entry point."""
2
2
  import argparse
3
3
  import asyncio
4
+ from importlib.metadata import version
4
5
  import logging
6
+ import sys
5
7
  from pymscada.bus_server import BusServer
6
8
  from pymscada.checkout import checkout
7
9
  from pymscada.config import Config
@@ -11,92 +13,258 @@ from pymscada.history import History
11
13
  from pymscada.iodrivers.logix_client import LogixClient
12
14
  from pymscada.iodrivers.modbus_client import ModbusClient
13
15
  from pymscada.iodrivers.modbus_server import ModbusServer
16
+ from pymscada.iodrivers.ping_client import PingClient
14
17
  from pymscada.iodrivers.snmp_client import SnmpClient
15
- from pymscada.simulate import Simulate
16
18
  from pymscada.www_server import WwwServer
17
19
  from pymscada.validate import validate
18
20
 
19
21
 
20
- def args():
21
- """Read commandline arguments."""
22
- parser = argparse.ArgumentParser(
23
- prog='pymscada',
24
- description='Connect IO, logic, applications and webpage UI.',
25
- epilog='Python MobileSCADA.'
26
- )
27
- commands = ['bus', 'console', 'wwwserver', 'history', 'files',
28
- 'logixclient',
29
- 'modbusserver', 'modbusclient',
30
- 'snmpclient',
31
- 'simulate', 'checkout',
32
- 'validate']
33
- parser.add_argument('module', type=str, choices=commands, metavar='action',
34
- help=f'select one of: {", ".join(commands)}')
35
- parser.add_argument('--config', metavar='file',
36
- help='Config file, default is "[module].yaml".')
37
- parser.add_argument('--tags', metavar='file',
38
- help='Tags file, default is "tags.yaml".')
39
- parser.add_argument('--verbose', action='store_true',
40
- help="Set level to logging.INFO.")
41
- parser.add_argument('--path', metavar='folder',
42
- help="Working folder, used for history and validate.")
43
- parser.add_argument('--overwrite', action='store_true', default=False,
44
- help='checkout may overwrite files, CARE!')
45
- return parser.parse_args()
22
+ class Module():
23
+ """Default Module."""
46
24
 
25
+ name = None
26
+ help = None
27
+ epilog = None
28
+ module = None
29
+ config = True
30
+ tags = True
31
+ sub = None
47
32
 
48
- async def run():
49
- """Run bus and wwwserver."""
50
- options = args()
51
- if options.verbose:
52
- logging.basicConfig(level=logging.INFO)
53
- if options.config is None:
54
- options.config = f'{options.module}.yaml'
55
- if options.tags is None:
56
- options.tags = 'tags.yaml'
57
- if options.module == 'bus':
33
+ def __init__(self, subparser: argparse._SubParsersAction):
34
+ """Add arguments common to all subparsers."""
35
+ self.sub = subparser.add_parser(
36
+ self.name, help=self.help, epilog=self.epilog)
37
+ self.sub.set_defaults(app=self)
38
+ if self.config:
39
+ self.sub.add_argument(
40
+ '--config', metavar='file', default=f'{self.name}.yaml',
41
+ help=f"Config file, default is '{self.name}.yaml'")
42
+ if self.tags:
43
+ self.sub.add_argument(
44
+ '--tags', metavar='file', default='tags.yaml',
45
+ help="Tags file, default is 'tags.yaml'")
46
+ self.sub.add_argument('--verbose', action='store_true',
47
+ help="Set level to logging.INFO")
48
+
49
+
50
+ class _Bus(Module):
51
+ """Bus Server."""
52
+
53
+ name = 'bus'
54
+ help = 'run the message bus'
55
+ tags = False
56
+
57
+ def run_once(self, options):
58
+ """Create the module."""
58
59
  config = Config(options.config)
59
- module = BusServer(**config)
60
- elif options.module == 'console':
61
- module = Console()
62
- elif options.module == 'wwwserver':
60
+ self.module = BusServer(**config)
61
+
62
+
63
+ class _WwwServer(Module):
64
+ """WWW Server Module."""
65
+
66
+ name = 'wwwserver'
67
+ help = 'serve web pages'
68
+
69
+ def run_once(self, options):
70
+ """Create the module."""
63
71
  config = Config(options.config)
64
72
  tag_info = dict(Config(options.tags))
65
- module = WwwServer(tag_info=tag_info, **config)
66
- elif options.module == 'history':
73
+ self.module = WwwServer(tag_info=tag_info, **config)
74
+
75
+
76
+ class _History(Module):
77
+ """History Module."""
78
+
79
+ name = 'history'
80
+ help = 'collect and serve history'
81
+
82
+ def run_once(self, options):
83
+ """Create the module."""
67
84
  config = Config(options.config)
68
85
  tag_info = dict(Config(options.tags))
69
- module = History(tag_info=tag_info, **config)
70
- elif options.module == 'files':
86
+ self.module = History(tag_info=tag_info, **config)
87
+
88
+
89
+ class _Files(Module):
90
+ """Bus Module."""
91
+
92
+ name = 'files'
93
+ help = 'receive and send files'
94
+ tags = False
95
+
96
+ def run_once(self, options):
97
+ """Create the module."""
71
98
  config = Config(options.config)
72
- module = Files(**config)
73
- elif options.module == 'logixclient':
99
+ self.module = Files(**config)
100
+
101
+
102
+ class _Console(Module):
103
+ """Bus Module."""
104
+
105
+ name = 'console'
106
+ help = 'interactive bus console'
107
+ config = False
108
+ tags = False
109
+
110
+ def run_once(self, _options):
111
+ """Create the module."""
112
+ self.module = Console()
113
+
114
+
115
+ class _checkout(Module):
116
+ """Bus Module."""
117
+
118
+ name = 'checkout'
119
+ help = 'create example config files'
120
+ epilog = """
121
+ To add to systemd `f="pymscada-bus" && cp config/$f.service
122
+ /lib/systemd/system && systemctl enable $f && systemctl start
123
+ $f`"""
124
+ config = False
125
+ tags = False
126
+
127
+ def __init__(self, subparser: argparse._SubParsersAction):
128
+ super().__init__(subparser)
129
+ self.sub.add_argument(
130
+ '--overwrite', action='store_true', default=False,
131
+ help='checkout may overwrite files, CARE!')
132
+ self.sub.add_argument(
133
+ '--diff', action='store_true', default=False,
134
+ help='compare default with existing')
135
+
136
+ def run_once(self, options):
137
+ """Create the module."""
138
+ checkout(overwrite=options.overwrite, diff=options.diff)
139
+
140
+
141
+ class _validate(Module):
142
+ """Bus Module."""
143
+
144
+ name = 'validate'
145
+ help = 'validate config files'
146
+ config = False
147
+ tags = False
148
+
149
+ def __init__(self, subparser: argparse._SubParsersAction):
150
+ super().__init__(subparser)
151
+ self.sub.add_argument(
152
+ '--path', metavar='file',
153
+ help='default is current working directory')
154
+
155
+ def run_once(self, options):
156
+ """Create the module."""
157
+ r, e, p = validate(options.path)
158
+ if r:
159
+ print(f'Config files in {p} valid.')
160
+ else:
161
+ print(e)
162
+
163
+
164
+ class _ModbusServer(Module):
165
+ """Bus Module."""
166
+
167
+ name = 'modbusserver'
168
+ help = 'receive modbus messages'
169
+ epilog = """
170
+ Needs `setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/python3.nn` to
171
+ bind to port 502."""
172
+ tags = False
173
+
174
+ def run_once(self, options):
175
+ """Create the module."""
74
176
  config = Config(options.config)
75
- module = LogixClient(**config)
76
- elif options.module == 'modbusclient':
177
+ self.module = ModbusServer(**config)
178
+
179
+
180
+ class _ModbusClient(Module):
181
+ """Bus Module."""
182
+
183
+ name = 'modbusclient'
184
+ help = 'poll/write to modbus devices'
185
+ tags = False
186
+
187
+ def run_once(self, options):
188
+ """Create the module."""
77
189
  config = Config(options.config)
78
- module = ModbusClient(**config)
79
- elif options.module == 'modbusserver':
190
+ self.module = ModbusClient(**config)
191
+
192
+
193
+ class _LogixClient(Module):
194
+ """Bus Module."""
195
+
196
+ name = 'logixclient'
197
+ help = 'poll/write to logix devices'
198
+
199
+ def run_once(self, options):
200
+ """Create the module."""
80
201
  config = Config(options.config)
81
- module = ModbusServer(**config)
82
- elif options.module == 'snmpclient':
202
+ self.module = LogixClient(**config)
203
+
204
+
205
+ class _PingClient(Module):
206
+ """Bus Module."""
207
+
208
+ name = 'ping'
209
+ help = 'ping a list of addresses, return time'
210
+ epilog = """
211
+ Needs `setcap CAP_NET_RAW+ep /usr/bin/python3.nn` to open SOCK_RAW
212
+ """
213
+ tags = False
214
+
215
+ def run_once(self, options):
216
+ """Create the module."""
217
+ if sys.platform.startswith("win"):
218
+ asyncio.set_event_loop_policy(
219
+ asyncio.WindowsSelectorEventLoopPolicy())
83
220
  config = Config(options.config)
84
- module = SnmpClient(**config)
85
- elif options.module == 'simulate':
221
+ self.module = PingClient(**config)
222
+
223
+
224
+ class _SnmpClient(Module):
225
+ """Bus Module."""
226
+
227
+ name = 'snmpclient'
228
+ help = 'poll snmp oids'
229
+ tags = False
230
+
231
+ def run_once(self, options):
232
+ """Create the module."""
86
233
  config = Config(options.config)
87
- tag_info = dict(Config(options.tags))
88
- module = Simulate(tag_info=tag_info, **config)
89
- elif options.module == 'checkout':
90
- checkout(overwrite=options.overwrite)
91
- return
92
- elif options.module == 'validate':
93
- r, e = validate(options.path)
94
- if r == True:
95
- print(f'Config files in {options.path} valid.')
96
- else:
97
- print(e)
98
- return
99
- else:
100
- logging.warning(f'no {options.module}')
101
- await module.start()
102
- await asyncio.get_event_loop().create_future()
234
+ self.module = SnmpClient(**config)
235
+
236
+
237
+ def args(_version: str):
238
+ """Read commandline arguments."""
239
+ parser = argparse.ArgumentParser(
240
+ prog='pymscada',
241
+ description='Connect IO, logic, applications, and webpage UI',
242
+ epilog=f'Python Mobile SCADA {_version}'
243
+ )
244
+ s = parser.add_subparsers(title='module')
245
+ _Bus(s)
246
+ _WwwServer(s)
247
+ _History(s)
248
+ _Files(s)
249
+ _Console(s)
250
+ _checkout(s)
251
+ _validate(s)
252
+ _ModbusServer(s)
253
+ _ModbusClient(s)
254
+ _LogixClient(s)
255
+ _SnmpClient(s)
256
+ _PingClient(s)
257
+ return parser.parse_args()
258
+
259
+
260
+ async def run():
261
+ """Run bus and wwwserver."""
262
+ _version = version("pymscada")
263
+ logging.warning(f'pymscada {_version} starting')
264
+ options = args(_version)
265
+ if options.verbose:
266
+ logging.getLogger().setLevel(logging.INFO)
267
+ options.app.run_once(options)
268
+ if options.app.module is not None:
269
+ await options.app.module.start()
270
+ await asyncio.get_event_loop().create_future()