pymscada 0.1.11b10__py3-none-any.whl → 0.2.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.
- pymscada/alarms.py +353 -0
- pymscada/bus_client.py +6 -5
- pymscada/bus_server.py +14 -2
- pymscada/callout.py +206 -0
- pymscada/checkout.py +87 -89
- pymscada/console.py +4 -3
- pymscada/demo/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/demo/alarms.yaml +5 -0
- pymscada/demo/callout.yaml +17 -0
- pymscada/demo/openweather.yaml +1 -1
- pymscada/demo/pymscada-alarms.service +16 -0
- pymscada/demo/pymscada-callout.service +16 -0
- pymscada/demo/pymscada-io-openweather.service +15 -0
- pymscada/demo/{pymscada-io-accuweather.service → pymscada-io-witsapi.service} +2 -2
- pymscada/demo/tags.yaml +4 -0
- pymscada/demo/witsapi.yaml +17 -0
- pymscada/files.py +3 -3
- pymscada/history.py +64 -8
- pymscada/iodrivers/openweather.py +131 -50
- pymscada/iodrivers/witsapi.py +217 -0
- pymscada/iodrivers/witsapi_POC.py +246 -0
- pymscada/main.py +1 -1
- pymscada/module_config.py +40 -14
- pymscada/opnotes.py +81 -16
- pymscada/pdf/__pycache__/__init__.cpython-311.pyc +0 -0
- pymscada/protocol_constants.py +51 -33
- pymscada/tag.py +0 -22
- pymscada/www_server.py +72 -17
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/METADATA +9 -7
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/RECORD +38 -25
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/WHEEL +2 -1
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/entry_points.txt +0 -3
- pymscada-0.2.0.dist-info/top_level.txt +1 -0
- {pymscada-0.1.11b10.dist-info → pymscada-0.2.0.dist-info}/licenses/LICENSE +0 -0
pymscada/checkout.py
CHANGED
|
@@ -6,100 +6,98 @@ import sys
|
|
|
6
6
|
from pymscada.config import get_demo_files, get_pdf_files
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
9
|
+
class Checkout:
|
|
10
|
+
"""Create and manage configuration files."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, **kwargs):
|
|
13
|
+
"""Initialize paths and settings."""
|
|
14
|
+
self.path = {
|
|
15
|
+
'__PYTHON__': Path(f'{sys.exec_prefix}/bin/python').absolute(),
|
|
16
|
+
'__PYMSCADA__': Path(sys.argv[0]).absolute(),
|
|
17
|
+
'__DIR__': Path('.').absolute(),
|
|
18
|
+
'__HOME__': Path.home().absolute(),
|
|
19
|
+
'__USER__': getpass.getuser()
|
|
20
|
+
}
|
|
21
|
+
if sys.platform == "win32":
|
|
22
|
+
self.path['__PYTHON__'] = Path(f'{sys.exec_prefix}/python.exe').absolute()
|
|
23
|
+
|
|
24
|
+
self.overwrite = kwargs.get('overwrite', False)
|
|
25
|
+
self.diff = kwargs.get('diff', False)
|
|
24
26
|
|
|
25
|
-
def make_history():
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
def make_history(self):
|
|
28
|
+
"""Make the history folder if missing."""
|
|
29
|
+
history_dir = self.path['__DIR__'].joinpath('history')
|
|
30
|
+
if not history_dir.exists():
|
|
31
|
+
print("making 'history' folder")
|
|
32
|
+
history_dir.mkdir()
|
|
31
33
|
|
|
34
|
+
def make_pdf(self):
|
|
35
|
+
"""Make the pdf folder if missing."""
|
|
36
|
+
pdf_dir = self.path['__DIR__'].joinpath('pdf')
|
|
37
|
+
if not pdf_dir.exists():
|
|
38
|
+
print('making pdf dir')
|
|
39
|
+
pdf_dir.mkdir()
|
|
40
|
+
for pdf_file in get_pdf_files():
|
|
41
|
+
target = pdf_dir.joinpath(pdf_file.name)
|
|
42
|
+
target.write_bytes(pdf_file.read_bytes())
|
|
32
43
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
def make_config(self):
|
|
45
|
+
"""Make the config folder, if missing, and copy files in."""
|
|
46
|
+
config_dir = self.path['__DIR__'].joinpath('config')
|
|
47
|
+
if not config_dir.exists():
|
|
48
|
+
print('making config dir')
|
|
49
|
+
config_dir.mkdir()
|
|
50
|
+
for config_file in get_demo_files():
|
|
51
|
+
target = config_dir.joinpath(config_file.name)
|
|
52
|
+
rt = 'Creating '
|
|
53
|
+
if target.exists():
|
|
54
|
+
if self.overwrite:
|
|
55
|
+
rt = 'Replacing '
|
|
56
|
+
target.unlink()
|
|
57
|
+
else:
|
|
58
|
+
continue
|
|
59
|
+
print(f'{rt} {target}')
|
|
60
|
+
rd_bytes = config_file.read_bytes()
|
|
61
|
+
if target.name.lower() != 'readme.md':
|
|
62
|
+
for k, v in self.path.items():
|
|
63
|
+
rd_bytes = rd_bytes.replace(k.encode(), str(v).encode())
|
|
64
|
+
target.write_bytes(rd_bytes)
|
|
42
65
|
|
|
66
|
+
def read_with_subst(self, file: Path):
|
|
67
|
+
"""Read the file and replace DIR markers."""
|
|
68
|
+
rd = file.read_bytes().decode()
|
|
69
|
+
for k, v in self.path.items():
|
|
70
|
+
rd = rd.replace(k, str(v))
|
|
71
|
+
lines = rd.splitlines()
|
|
72
|
+
return lines
|
|
43
73
|
|
|
44
|
-
def
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
def compare_config(self):
|
|
75
|
+
"""Compare old and new config."""
|
|
76
|
+
config_dir = self.path['__DIR__'].joinpath('config')
|
|
77
|
+
if not config_dir.exists():
|
|
78
|
+
print('No config dir, are you in the right directory')
|
|
79
|
+
return
|
|
80
|
+
for config_file in get_demo_files():
|
|
81
|
+
target = config_dir.joinpath(config_file.name)
|
|
82
|
+
if target.exists():
|
|
83
|
+
new_lines = self.read_with_subst(config_file)
|
|
84
|
+
old_lines = self.read_with_subst(target)
|
|
85
|
+
diff = list(difflib.unified_diff(old_lines, new_lines,
|
|
86
|
+
fromfile=str(target), tofile=str(config_file)))
|
|
87
|
+
if len(diff):
|
|
88
|
+
print('\n'.join(diff), '\n')
|
|
57
89
|
else:
|
|
58
|
-
|
|
59
|
-
print(f'{rt} {target}')
|
|
60
|
-
rd_bytes = config_file.read_bytes()
|
|
61
|
-
if target.name.lower() != 'readme.md':
|
|
62
|
-
for k, v in PATH.items():
|
|
63
|
-
rd_bytes = rd_bytes.replace(k.encode(), str(v).encode())
|
|
64
|
-
target.write_bytes(rd_bytes)
|
|
90
|
+
print(f'\n--- MISSING FILE\n\n+++ {config_file}')
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def compare_config():
|
|
77
|
-
"""Compare old and new config."""
|
|
78
|
-
config_dir = PATH['__DIR__'].joinpath('config')
|
|
79
|
-
if not config_dir.exists():
|
|
80
|
-
print('No config dir, are you in the right directory')
|
|
81
|
-
return
|
|
82
|
-
for config_file in get_demo_files():
|
|
83
|
-
target = config_dir.joinpath(config_file.name)
|
|
84
|
-
if target.exists():
|
|
85
|
-
new_lines = read_with_subst(config_file)
|
|
86
|
-
old_lines = read_with_subst(target)
|
|
87
|
-
diff = list(difflib.unified_diff(old_lines, new_lines,
|
|
88
|
-
fromfile=str(target), tofile=str(config_file)))
|
|
89
|
-
if len(diff):
|
|
90
|
-
print('\n'.join(diff), '\n')
|
|
92
|
+
async def start(self):
|
|
93
|
+
"""Execute checkout process."""
|
|
94
|
+
for name in ['__PYTHON__', '__PYMSCADA__', '__DIR__', '__HOME__']:
|
|
95
|
+
if not self.path[name].exists():
|
|
96
|
+
raise SystemExit(f'{self.path[name]} is missing')
|
|
97
|
+
|
|
98
|
+
if self.diff:
|
|
99
|
+
self.compare_config()
|
|
91
100
|
else:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def checkout(overwrite=False, diff=False):
|
|
96
|
-
"""Do it."""
|
|
97
|
-
for name in ['__PYTHON__', '__PYMSCADA__', '__DIR__', '__HOME__']:
|
|
98
|
-
if not PATH[name].exists():
|
|
99
|
-
raise SystemExit(f'{PATH[name]} is missing')
|
|
100
|
-
if diff:
|
|
101
|
-
compare_config()
|
|
102
|
-
else:
|
|
103
|
-
make_history()
|
|
104
|
-
make_pdf()
|
|
105
|
-
make_config(overwrite)
|
|
101
|
+
self.make_history()
|
|
102
|
+
self.make_pdf()
|
|
103
|
+
self.make_config()
|
pymscada/console.py
CHANGED
|
@@ -3,7 +3,8 @@ import asyncio
|
|
|
3
3
|
import logging
|
|
4
4
|
import sys
|
|
5
5
|
from pymscada.bus_client import BusClient
|
|
6
|
-
from pymscada.tag import Tag
|
|
6
|
+
from pymscada.tag import Tag
|
|
7
|
+
from pymscada.www_server import standardise_tag_info
|
|
7
8
|
try:
|
|
8
9
|
import termios
|
|
9
10
|
import tty
|
|
@@ -153,7 +154,7 @@ class Console:
|
|
|
153
154
|
"""Provide a text console to interact with a Bus."""
|
|
154
155
|
|
|
155
156
|
def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
|
|
156
|
-
tag_info: dict = {}):
|
|
157
|
+
tag_info: dict = {}) -> None:
|
|
157
158
|
"""
|
|
158
159
|
Connect to bus_ip:bus_port and provide console interaction with a Bus.
|
|
159
160
|
|
|
@@ -177,7 +178,7 @@ class Console:
|
|
|
177
178
|
self.busclient = BusClient(bus_ip, bus_port, module='Console')
|
|
178
179
|
self.tags: dict[str, Tag] = {}
|
|
179
180
|
for tagname, tag in tag_info.items():
|
|
180
|
-
|
|
181
|
+
standardise_tag_info(tagname, tag)
|
|
181
182
|
self.tags[tagname] = Tag(tagname, tag['type'])
|
|
182
183
|
|
|
183
184
|
def write_tag(self, tag: Tag):
|
|
Binary file
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
bus_ip: 127.0.0.1
|
|
2
|
+
bus_port: 1324
|
|
3
|
+
rta_tag: __callout__
|
|
4
|
+
alarms_tag: __alarms__
|
|
5
|
+
ack_tag: SI_Alarm_Ack
|
|
6
|
+
status_tag: SO_Alarm_Status
|
|
7
|
+
callees:
|
|
8
|
+
- name: A name
|
|
9
|
+
sms: A number
|
|
10
|
+
group: System
|
|
11
|
+
- name: B name
|
|
12
|
+
sms: B number
|
|
13
|
+
groups:
|
|
14
|
+
- name: Aniwhenua Station
|
|
15
|
+
group:
|
|
16
|
+
- name: Aniwhenua System
|
|
17
|
+
group: System
|
pymscada/demo/openweather.yaml
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=pymscada - alarms
|
|
3
|
+
BindsTo=pymscada-bus.service
|
|
4
|
+
After=pymscada-bus.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
WorkingDirectory=__DIR__
|
|
8
|
+
ExecStart=__PYMSCADA__ alarms --config __DIR__/config/alarms.yaml --tags __DIR__/config/tags.yaml
|
|
9
|
+
Restart=always
|
|
10
|
+
RestartSec=5
|
|
11
|
+
User=__USER__
|
|
12
|
+
Group=__USER__
|
|
13
|
+
KillSignal=SIGINT
|
|
14
|
+
|
|
15
|
+
[Install]
|
|
16
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=pymscada - callout
|
|
3
|
+
BindsTo=pymscada-bus.service
|
|
4
|
+
After=pymscada-bus.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
WorkingDirectory=__DIR__
|
|
8
|
+
ExecStart=__PYMSCADA__ callout --config __DIR__/config/callout.yaml --tags __DIR__/config/tags.yaml
|
|
9
|
+
Restart=always
|
|
10
|
+
RestartSec=5
|
|
11
|
+
User=__USER__
|
|
12
|
+
Group=__USER__
|
|
13
|
+
KillSignal=SIGINT
|
|
14
|
+
|
|
15
|
+
[Install]
|
|
16
|
+
WantedBy=multi-user.target
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=pymscada - Open Weather client
|
|
3
|
+
BindsTo=pymscada-bus.service
|
|
4
|
+
After=pymscada-bus.service
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
WorkingDirectory=__DIR__
|
|
8
|
+
ExecStart=__PYMSCADA__ openweatherclient --config __DIR__/config/openweather.yaml
|
|
9
|
+
Restart=always
|
|
10
|
+
RestartSec=5
|
|
11
|
+
User=__USER__
|
|
12
|
+
Group=__USER__
|
|
13
|
+
|
|
14
|
+
[Install]
|
|
15
|
+
WantedBy=multi-user.target
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[Unit]
|
|
2
|
-
Description=pymscada -
|
|
2
|
+
Description=pymscada - WITS client
|
|
3
3
|
BindsTo=pymscada-bus.service
|
|
4
4
|
After=pymscada-bus.service
|
|
5
5
|
|
|
6
6
|
[Service]
|
|
7
7
|
WorkingDirectory=__DIR__
|
|
8
|
-
ExecStart=__PYMSCADA__
|
|
8
|
+
ExecStart=__PYMSCADA__ witsapiclient --config __DIR__/config/witsapi.yaml
|
|
9
9
|
Restart=always
|
|
10
10
|
RestartSec=5
|
|
11
11
|
User=__USER__
|
pymscada/demo/tags.yaml
CHANGED
|
@@ -113,10 +113,14 @@ localhost_ping:
|
|
|
113
113
|
desc: Ping time to localhost
|
|
114
114
|
type: float
|
|
115
115
|
units: ms
|
|
116
|
+
alarm: '> 500 for 300'
|
|
117
|
+
group: System
|
|
116
118
|
google_ping:
|
|
117
119
|
desc: Ping time to google
|
|
118
120
|
type: float
|
|
119
121
|
units: ms
|
|
122
|
+
alarm: '> 100 for 30'
|
|
123
|
+
group: System
|
|
120
124
|
Murupara_Temp:
|
|
121
125
|
desc: Murupara Temperature
|
|
122
126
|
type: float
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
bus_ip: 127.0.0.1
|
|
2
|
+
bus_port: 1324
|
|
3
|
+
proxy:
|
|
4
|
+
api:
|
|
5
|
+
url: 'https://api.electricityinfo.co.nz'
|
|
6
|
+
client_id: ${WITS_CLIENT_ID}
|
|
7
|
+
client_secret: ${WITS_CLIENT_SECRET}
|
|
8
|
+
gxp_list:
|
|
9
|
+
- MAT1101
|
|
10
|
+
- CYD2201
|
|
11
|
+
- BEN2201
|
|
12
|
+
back: 1
|
|
13
|
+
forward: 12
|
|
14
|
+
tags:
|
|
15
|
+
- MAT1101_RTD
|
|
16
|
+
- CYD2201_RTD
|
|
17
|
+
- BEN2201_RTD
|
pymscada/files.py
CHANGED
|
@@ -10,13 +10,13 @@ class Files():
|
|
|
10
10
|
"""Connect to bus_ip:bus_port, store and provide a value history."""
|
|
11
11
|
|
|
12
12
|
def __init__(self, bus_ip: str = '127.0.0.1', bus_port: int = 1324,
|
|
13
|
-
path: str = '', files: list =
|
|
13
|
+
path: str = '', files: list[dict] = [],
|
|
14
14
|
rta_tag: str = '__files__') -> None:
|
|
15
15
|
"""
|
|
16
16
|
Connect to bus_ip:bus_port, serve and update files.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
Scans paths in files at the root defined by path. These should
|
|
19
|
+
be readable by wwwserver so that download links work.
|
|
20
20
|
|
|
21
21
|
Event loop must be running.
|
|
22
22
|
"""
|
pymscada/history.py
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
|
-
"""Store and provide history.
|
|
1
|
+
"""Store and provide history.
|
|
2
|
+
|
|
3
|
+
History File Structure
|
|
4
|
+
---------------------
|
|
5
|
+
History files are binary files stored as <tagname>_<time_us>.dat where time_us
|
|
6
|
+
is the microsecond timestamp of the first entry in that file.
|
|
7
|
+
|
|
8
|
+
Each file contains a series of fixed-size records (16 bytes each):
|
|
9
|
+
- For integer tags: 8 bytes timestamp (uint64) + 8 bytes value (int64)
|
|
10
|
+
- For float tags: 8 bytes timestamp (uint64) + 8 bytes value (double)
|
|
11
|
+
|
|
12
|
+
Files are organized in chunks:
|
|
13
|
+
- Each chunk is 1024 records (16KB)
|
|
14
|
+
- Each file contains up to 64 chunks (1MB)
|
|
15
|
+
- New files are created when:
|
|
16
|
+
1. Current file reaches max size (64 chunks)
|
|
17
|
+
2. Manual flush() is called
|
|
18
|
+
3. Application shutdown
|
|
19
|
+
|
|
20
|
+
Timestamps are stored as microseconds since epoch in network byte order (big-endian).
|
|
21
|
+
Values are also stored in network byte order.
|
|
22
|
+
"""
|
|
2
23
|
import atexit
|
|
3
24
|
import logging
|
|
4
25
|
from pathlib import Path
|
|
5
26
|
from struct import pack, pack_into, unpack_from, error
|
|
6
27
|
import time
|
|
28
|
+
import socket
|
|
29
|
+
from typing import TypedDict, Optional
|
|
7
30
|
from pymscada.bus_client import BusClient
|
|
8
31
|
from pymscada.tag import Tag, TYPES
|
|
9
32
|
|
|
@@ -14,11 +37,12 @@ CHUNK_SIZE = ITEM_COUNT * ITEM_SIZE
|
|
|
14
37
|
FILE_CHUNKS = 64
|
|
15
38
|
|
|
16
39
|
|
|
17
|
-
def
|
|
18
|
-
"""Correct tag dictionary in place to be suitable for
|
|
40
|
+
def standardise_tag_info(tagname: str, tag: dict):
|
|
41
|
+
"""Correct tag dictionary in place to be suitable for modules."""
|
|
19
42
|
tag['name'] = tagname
|
|
20
43
|
tag['id'] = None
|
|
21
44
|
if 'desc' not in tag:
|
|
45
|
+
logging.warning(f"Tag {tagname} has no description, using name")
|
|
22
46
|
tag['desc'] = tag['name']
|
|
23
47
|
if 'multi' in tag:
|
|
24
48
|
tag['type'] = int
|
|
@@ -38,6 +62,16 @@ def tag_for_history(tagname: str, tag: dict):
|
|
|
38
62
|
tag['deadband'] = None
|
|
39
63
|
|
|
40
64
|
|
|
65
|
+
class Request(TypedDict, total=False):
|
|
66
|
+
"""Type definition for request dictionary."""
|
|
67
|
+
tagname: str
|
|
68
|
+
start_ms: Optional[int] # Allow web client to use native ms
|
|
69
|
+
start_us: Optional[int] # Native for pymscada server
|
|
70
|
+
end_ms: Optional[int]
|
|
71
|
+
end_us: Optional[int]
|
|
72
|
+
__rta_id__: Optional[int] # Empty for a change that must be broadcast
|
|
73
|
+
|
|
74
|
+
|
|
41
75
|
def get_tag_hist_files(path: Path, tagname: str) -> dict[int, Path]:
|
|
42
76
|
"""Parse path for history files matching tagname."""
|
|
43
77
|
files_us = {}
|
|
@@ -188,9 +222,14 @@ class TagHistory():
|
|
|
188
222
|
class History():
|
|
189
223
|
"""Connect to bus_ip:bus_port, store and provide a value history."""
|
|
190
224
|
|
|
191
|
-
def __init__(
|
|
192
|
-
|
|
193
|
-
|
|
225
|
+
def __init__(
|
|
226
|
+
self,
|
|
227
|
+
bus_ip: str = '127.0.0.1',
|
|
228
|
+
bus_port: int = 1324,
|
|
229
|
+
path: str = 'history',
|
|
230
|
+
tag_info: dict[str, dict] = {},
|
|
231
|
+
rta_tag: str | None = '__history__'
|
|
232
|
+
) -> None:
|
|
194
233
|
"""
|
|
195
234
|
Connect to bus_ip:bus_port, store and provide a value history.
|
|
196
235
|
|
|
@@ -200,12 +239,29 @@ class History():
|
|
|
200
239
|
|
|
201
240
|
Event loop must be running.
|
|
202
241
|
"""
|
|
242
|
+
if not isinstance(bus_ip, str):
|
|
243
|
+
raise ValueError("bus_ip must be a string")
|
|
244
|
+
try:
|
|
245
|
+
socket.gethostbyname(bus_ip)
|
|
246
|
+
except socket.gaierror:
|
|
247
|
+
raise ValueError(f"Invalid bus_ip: {bus_ip}")
|
|
248
|
+
if not isinstance(bus_port, int):
|
|
249
|
+
raise ValueError("bus_port must be an integer")
|
|
250
|
+
if not 1024 <= bus_port <= 65535:
|
|
251
|
+
raise ValueError(f"bus_port must be between 1024 and 65535")
|
|
252
|
+
if not isinstance(path, str):
|
|
253
|
+
raise ValueError("path must be a string")
|
|
254
|
+
if not isinstance(tag_info, dict):
|
|
255
|
+
raise ValueError("tag_info must be a dictionary")
|
|
256
|
+
if not isinstance(rta_tag, (str, type(None))):
|
|
257
|
+
raise ValueError("rta_tag must be a string or None")
|
|
258
|
+
|
|
203
259
|
self.busclient = BusClient(bus_ip, bus_port, module='History')
|
|
204
260
|
self.path = path
|
|
205
261
|
self.tags: dict[str, Tag] = {}
|
|
206
262
|
self.hist_tags: dict[str, TagHistory] = {}
|
|
207
263
|
for tagname, tag in tag_info.items():
|
|
208
|
-
|
|
264
|
+
standardise_tag_info(tagname, tag)
|
|
209
265
|
if tag['type'] not in [float, int]:
|
|
210
266
|
continue
|
|
211
267
|
self.hist_tags[tagname] = TagHistory(
|
|
@@ -217,7 +273,7 @@ class History():
|
|
|
217
273
|
self.rta.value = b'\x00\x00\x00\x00\x00\x00'
|
|
218
274
|
self.busclient.add_callback_rta(rta_tag, self.rta_cb)
|
|
219
275
|
|
|
220
|
-
def rta_cb(self, request):
|
|
276
|
+
def rta_cb(self, request: Request):
|
|
221
277
|
"""Respond to bus requests for data to publish on rta."""
|
|
222
278
|
if 'start_ms' in request:
|
|
223
279
|
request['start_us'] = request['start_ms'] * 1000
|