pymscada 0.0.14__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.
- pymscada/checkout.py +38 -5
- pymscada/demo/logixclient.yaml +24 -23
- pymscada/demo/modbusclient.yaml +22 -18
- pymscada/demo/modbusserver.yaml +10 -2
- pymscada/demo/ping.yaml +12 -0
- pymscada/demo/pymscada-io-ping.service +15 -0
- pymscada/demo/snmpclient.yaml +17 -17
- pymscada/demo/tags.yaml +50 -1
- pymscada/demo/wwwserver.yaml +447 -1
- pymscada/iodrivers/logix_client.py +9 -9
- pymscada/iodrivers/logix_map.py +74 -62
- pymscada/iodrivers/modbus_client.py +20 -34
- pymscada/iodrivers/modbus_map.py +4 -1
- pymscada/iodrivers/modbus_server.py +34 -48
- pymscada/iodrivers/ping_client.py +120 -0
- pymscada/iodrivers/ping_map.py +43 -0
- pymscada/iodrivers/snmp_client.py +4 -2
- pymscada/iodrivers/snmp_map.py +1 -1
- pymscada/main.py +239 -71
- pymscada/validate.py +127 -35
- pymscada-0.1.0.dist-info/METADATA +88 -0
- {pymscada-0.0.14.dist-info → pymscada-0.1.0.dist-info}/RECORD +28 -26
- pymscada/demo/simulate.yaml +0 -21
- pymscada/simulate.py +0 -66
- pymscada-0.0.14.dist-info/METADATA +0 -251
- /pymscada/demo/{pymscada-modbusclient.service → pymscada-io-modbusclient.service} +0 -0
- /pymscada/demo/{pymscada-modbusserver.service → pymscada-io-modbusserver.service} +0 -0
- /pymscada/{iodrivers → tools}/snmp_client2.py +0 -0
- {pymscada-0.0.14.dist-info → pymscada-0.1.0.dist-info}/WHEEL +0 -0
- {pymscada-0.0.14.dist-info → pymscada-0.1.0.dist-info}/entry_points.txt +0 -0
- {pymscada-0.0.14.dist-info → pymscada-0.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Modbus Client."""
|
|
2
2
|
import asyncio
|
|
3
|
-
from itertools import chain
|
|
4
3
|
import logging
|
|
5
4
|
from struct import pack, unpack_from
|
|
6
5
|
from pymscada.bus_client import BusClient
|
|
@@ -43,53 +42,40 @@ class ModbusClientProtocol(asyncio.Protocol):
|
|
|
43
42
|
transport.set_write_buffer_limits(high=0)
|
|
44
43
|
self.transport = transport
|
|
45
44
|
|
|
46
|
-
def
|
|
47
|
-
"""
|
|
48
|
-
# logging.info("data_received")
|
|
49
|
-
# logging.info(f'tcp echo server received: {recv}')
|
|
45
|
+
def unpack_mb(self):
|
|
46
|
+
"""Return complete modbus packets and trim the buffer."""
|
|
50
47
|
start = 0
|
|
51
|
-
self.buffer += recv
|
|
52
48
|
while True:
|
|
53
49
|
buf_len = len(self.buffer)
|
|
54
|
-
if buf_len < 6 + start: #
|
|
55
|
-
self.buffer = self.buffer[start:]
|
|
50
|
+
if buf_len < 6 + start: # enough to unpack length
|
|
56
51
|
break
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
if buf_len < 6 + mbap_len:
|
|
61
|
-
self.buffer = self.buffer[start:]
|
|
52
|
+
mbap_tr, mbap_pr, mbap_len = unpack_from(">3H", self.buffer, start)
|
|
53
|
+
if buf_len < start + 6 + mbap_len: # there is a complete message
|
|
62
54
|
break
|
|
63
55
|
end = start + 6 + mbap_len
|
|
64
|
-
|
|
65
|
-
self.process(self.buffer[start:end])
|
|
56
|
+
yield self.buffer[start:end]
|
|
66
57
|
start = end
|
|
58
|
+
self.buffer = self.buffer[end:]
|
|
59
|
+
|
|
60
|
+
def data_received(self, recv):
|
|
61
|
+
"""Received TCP data, see if there is a full modbus packet."""
|
|
62
|
+
self.buffer += recv
|
|
63
|
+
for msg in self.unpack_mb():
|
|
64
|
+
self.process(msg)
|
|
67
65
|
|
|
68
66
|
def datagram_received(self, recv, _addr):
|
|
69
67
|
"""Received a UDP packet, discard any partial packets."""
|
|
70
68
|
# logging.info("datagram_received")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
buf_len = len(buffer)
|
|
75
|
-
if buf_len < 6 + start: # assumes possible mbap_len of 0
|
|
76
|
-
buffer = buffer[start:]
|
|
77
|
-
break
|
|
78
|
-
(_mbap_tr, _mbap_pr, mbap_len) = unpack_from(">3H", buffer, start)
|
|
79
|
-
if buf_len < 6 + mbap_len:
|
|
80
|
-
buffer = buffer[start:]
|
|
81
|
-
break
|
|
82
|
-
end = start + 6 + mbap_len
|
|
83
|
-
# got a complete message, set start to end for buffer prune
|
|
84
|
-
self.process(buffer[start:end])
|
|
85
|
-
start = end
|
|
69
|
+
self.buffer = recv
|
|
70
|
+
for msg in self.unpack_mb():
|
|
71
|
+
self.process(msg)
|
|
86
72
|
|
|
87
73
|
|
|
88
74
|
class ModbusClientConnector:
|
|
89
75
|
"""Poll Modbus device, write on change in write range."""
|
|
90
76
|
|
|
91
77
|
def __init__(self, name: str, ip: str, port: int, rate: int, tcp_udp: str,
|
|
92
|
-
|
|
78
|
+
poll: list, mapping: ModbusMaps):
|
|
93
79
|
"""
|
|
94
80
|
Set up polling client.
|
|
95
81
|
|
|
@@ -101,13 +87,13 @@ class ModbusClientConnector:
|
|
|
101
87
|
self.tcp_udp = tcp_udp
|
|
102
88
|
self.transport = None
|
|
103
89
|
self.protocol = None
|
|
104
|
-
self.read =
|
|
105
|
-
self.writeok =
|
|
90
|
+
self.read = poll
|
|
91
|
+
self.writeok = None
|
|
106
92
|
self.periodic = Periodic(self.poll, rate)
|
|
107
93
|
self.mapping = mapping
|
|
108
94
|
self.sent = {}
|
|
109
95
|
tables = {}
|
|
110
|
-
for file_range in chain(read, writeok):
|
|
96
|
+
for file_range in poll: # chain(read, writeok):
|
|
111
97
|
unit = file_range['unit']
|
|
112
98
|
file = file_range['file']
|
|
113
99
|
table = f'{name}:{unit}:{file}'
|
pymscada/iodrivers/modbus_map.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
41
|
-
"""
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
51
|
+
yield self.buffer[start:end]
|
|
58
52
|
start = end
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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",
|
|
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",
|
|
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
|
-
|
|
97
|
-
|
|
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",
|
|
100
|
-
data = bytearray(
|
|
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
|
-
|
|
105
|
-
|
|
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",
|
|
108
|
-
data = bytearray(
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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,
|
|
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
|
|
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:
|
pymscada/iodrivers/snmp_map.py
CHANGED
|
@@ -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['
|
|
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] = {}
|