slowpy 0.4.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.
- slowpy/__init__.py +11 -0
- slowpy/basetypes.py +118 -0
- slowpy/control/__init__.py +13 -0
- slowpy/control/control_AsyncDripline.py +492 -0
- slowpy/control/control_AsyncHTTP.py +153 -0
- slowpy/control/control_AsyncLocalPubsub.py +213 -0
- slowpy/control/control_AsyncMQTT.py +370 -0
- slowpy/control/control_AsyncModbus.py +208 -0
- slowpy/control/control_AsyncNATS.py +275 -0
- slowpy/control/control_AsyncRabbitMQ.py +594 -0
- slowpy/control/control_AsyncRedis.py +500 -0
- slowpy/control/control_AsyncSlowMQ.py +321 -0
- slowpy/control/control_AsyncSlowdash.py +43 -0
- slowpy/control/control_CAMAC.py +497 -0
- slowpy/control/control_DataStore.py +53 -0
- slowpy/control/control_Dripline.py +229 -0
- slowpy/control/control_DriplineInterface.py +61 -0
- slowpy/control/control_DummyDevice.py +178 -0
- slowpy/control/control_Ethernet.py +398 -0
- slowpy/control/control_HTTP.py +156 -0
- slowpy/control/control_LabJackU.py +466 -0
- slowpy/control/control_MQTT.py +246 -0
- slowpy/control/control_Microphone.py +198 -0
- slowpy/control/control_Modbus.py +172 -0
- slowpy/control/control_NanotechMotor.py +528 -0
- slowpy/control/control_RabbitMQ.py +596 -0
- slowpy/control/control_Redis.py +499 -0
- slowpy/control/control_Serial.py +104 -0
- slowpy/control/control_Shell.py +95 -0
- slowpy/control/control_Slowdash.py +42 -0
- slowpy/control/control_UDP.py +119 -0
- slowpy/control/control_VISA.py +108 -0
- slowpy/control/dummy_device.py +132 -0
- slowpy/control/hdl/__init__.py +3 -0
- slowpy/control/hdl/hdl.py +177 -0
- slowpy/control/netutils.py +127 -0
- slowpy/control/node.py +819 -0
- slowpy/control/scpi_server.py +344 -0
- slowpy/control/system.py +372 -0
- slowpy/graphs.py +121 -0
- slowpy/histograms.py +316 -0
- slowpy/mesh/__init__.py +5 -0
- slowpy/mesh/dash.py +81 -0
- slowpy/mesh/mesh.py +645 -0
- slowpy/mesh/packet.py +107 -0
- slowpy/mesh/stdio.py +403 -0
- slowpy/mesh/tasklet.py +755 -0
- slowpy/mpldata.py +348 -0
- slowpy/slowfetch.py +184 -0
- slowpy/slowplot.py +540 -0
- slowpy/store/__init__.py +9 -0
- slowpy/store/blob_storage.py +155 -0
- slowpy/store/factory.py +32 -0
- slowpy/store/store.py +156 -0
- slowpy/store/store_CSV.py +95 -0
- slowpy/store/store_HDF5.py +239 -0
- slowpy/store/store_InfluxDB2.py +143 -0
- slowpy/store/store_Redis.py +163 -0
- slowpy/store/store_SQL.py +489 -0
- slowpy/treetable.py +71 -0
- slowpy/trend.py +200 -0
- slowpy-0.4.0.dist-info/METADATA +27 -0
- slowpy-0.4.0.dist-info/RECORD +65 -0
- slowpy-0.4.0.dist-info/WHEEL +5 -0
- slowpy-0.4.0.dist-info/top_level.txt +1 -0
slowpy/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
from .basetypes import DataElement, TimeSeries
|
|
3
|
+
from .histograms import Histogram, Histogram2d, HistogramBasicStat, HistogramCountStat, Histogram2dBasicStat
|
|
4
|
+
from .graphs import Graph, GraphYStat
|
|
5
|
+
from .trend import Trend, RateTrend
|
|
6
|
+
from .treetable import Tree, Table
|
|
7
|
+
|
|
8
|
+
from .slowfetch import SlowFetch
|
|
9
|
+
|
|
10
|
+
from .mpldata import slowdashify
|
|
11
|
+
from .slowplot import slowplot
|
slowpy/basetypes.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Created by Sanshiro Enomoto on 17 July 2024 #
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import time, json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DataElement:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self.attr_values = {}
|
|
10
|
+
self.stat_values = {}
|
|
11
|
+
self.stat_functors = []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_attr(self, name, value):
|
|
15
|
+
self.attr_values[name] = value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_stat(self, arg0, arg1=None):
|
|
19
|
+
# add_stat(key, value) or add_stat(functor) where functor takes an DataElement and returns a dict
|
|
20
|
+
if callable(arg0):
|
|
21
|
+
self.stat_functors.append(arg0)
|
|
22
|
+
else:
|
|
23
|
+
self.stat_values[arg0] = arg1
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def clear(self):
|
|
27
|
+
self.stat_values = {}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_json(self):
|
|
31
|
+
for f in self.stat_functors:
|
|
32
|
+
self.stat_values.update(f(self))
|
|
33
|
+
|
|
34
|
+
record = {}
|
|
35
|
+
if any(self.attr_values):
|
|
36
|
+
record.update({ '_attr': self.attr_values })
|
|
37
|
+
if any(self.stat_values):
|
|
38
|
+
record.update({ '_stat': self.stat_values })
|
|
39
|
+
return record
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def from_json(obj):
|
|
44
|
+
if type(obj) is not dict:
|
|
45
|
+
return obj
|
|
46
|
+
|
|
47
|
+
from .graphs import Graph
|
|
48
|
+
from .histograms import Histogram, Histogram2d
|
|
49
|
+
|
|
50
|
+
if 'bins' in obj:
|
|
51
|
+
return Histogram.from_json(obj)
|
|
52
|
+
elif 'ybins' in obj:
|
|
53
|
+
return Histogram2d.from_json(obj)
|
|
54
|
+
elif 'y' in obj:
|
|
55
|
+
return Graph.from_json(obj)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __str__(self):
|
|
60
|
+
return json.dumps(self.to_json())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TimeSeries:
|
|
65
|
+
def __init__(self, *, fields:list[str]|None=None, start:float=0, length:float|None=None):
|
|
66
|
+
self.start = start
|
|
67
|
+
self.length = length
|
|
68
|
+
self.fields = fields if fields is not None and len(fields) > 0 else ['x']
|
|
69
|
+
self.t = []
|
|
70
|
+
self.values = [ [] for _ in self.fields ] # self.values[field] is a time-series (array) for the field
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def write(self, values, t=None):
|
|
74
|
+
'''
|
|
75
|
+
# add one point to the time-series #
|
|
76
|
+
- values: dict for field name-value pairs, or list for field values, or a value
|
|
77
|
+
where a value can be a number, string, or data-element.
|
|
78
|
+
- time: UNIX time-stamp, if None if given, the current time will be used.
|
|
79
|
+
'''
|
|
80
|
+
if t is None:
|
|
81
|
+
t = time.time()
|
|
82
|
+
|
|
83
|
+
record = [None] * len(self.fields)
|
|
84
|
+
if isinstance(values, dict):
|
|
85
|
+
for i in range(len(record)):
|
|
86
|
+
record[i] = values.get(self.fields[i], None)
|
|
87
|
+
elif isinstance(values, list):
|
|
88
|
+
for i in range(min(len(values), len(record))):
|
|
89
|
+
record[i] = values[i]
|
|
90
|
+
else:
|
|
91
|
+
record[0] = values
|
|
92
|
+
|
|
93
|
+
self.t.append(t - self.start)
|
|
94
|
+
for k in range(len(self.fields)):
|
|
95
|
+
self.values[k].append(record[k])
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def to_json(self):
|
|
99
|
+
length = self.length
|
|
100
|
+
if length is None:
|
|
101
|
+
if len(self.t) < 1:
|
|
102
|
+
length = 1
|
|
103
|
+
else:
|
|
104
|
+
length = self.t[-1] + 1
|
|
105
|
+
|
|
106
|
+
data = {
|
|
107
|
+
'start': self.start,
|
|
108
|
+
'length': length,
|
|
109
|
+
't': self.t,
|
|
110
|
+
}
|
|
111
|
+
for k in range(len(self.fields)):
|
|
112
|
+
data[self.fields[k]] = self.values[k]
|
|
113
|
+
|
|
114
|
+
return data
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def __str__(self):
|
|
118
|
+
return json.dumps(self.to_json())
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
from .node import ControlNode, ControlVariableNode, ControlThreadNode, ControlException
|
|
3
|
+
from .system import ControlSystem, ValueNode, control_system
|
|
4
|
+
from .control_Ethernet import EthernetNode, ScpiNode, ScpiCommandNode
|
|
5
|
+
from .control_UDP import UdpSocketNode
|
|
6
|
+
from .control_HTTP import HttpNode
|
|
7
|
+
from .control_Shell import ShellNode
|
|
8
|
+
from .control_DataStore import DataStoreNode
|
|
9
|
+
|
|
10
|
+
from .scpi_server import ScpiServer, ScpiAdapter
|
|
11
|
+
from .netutils import find_ip
|
|
12
|
+
|
|
13
|
+
from .dummy_device import RandomWalkDevice, RandomHitDevice, RandomChargeDevice, RandomTimeDevice
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Created by Sanshiro Enomoto on 3 October 2025 #
|
|
2
|
+
|
|
3
|
+
import os, time, datetime, socket, copy, getpass, uuid, json, inspect, asyncio, logging
|
|
4
|
+
|
|
5
|
+
from slowpy.control import ControlNode, ControlVariableNode, control_system as ctrl
|
|
6
|
+
ctrl.import_control_module('AsyncRabbitMQ')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
sender_info = {
|
|
11
|
+
'exe': __file__,
|
|
12
|
+
'hostname': socket.gethostname(),
|
|
13
|
+
'service_name': 'slowdrip',
|
|
14
|
+
'username': getpass.getuser(),
|
|
15
|
+
'versions': {
|
|
16
|
+
'slowdash': {
|
|
17
|
+
'package': 'slowdash',
|
|
18
|
+
'version': '0',
|
|
19
|
+
'commit': '0',
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AsyncDriplineNode(ControlNode):
|
|
26
|
+
_seq = 0
|
|
27
|
+
|
|
28
|
+
def __init__(self, rabbitmq_url:str, name:str=None):
|
|
29
|
+
self.rmq = ctrl.async_rabbitmq(rabbitmq_url)
|
|
30
|
+
self.name = name or f'AsyncSlowDrip_{socket.gethostname()}_{os.getpid()}_{AsyncDriplineNode._seq}'
|
|
31
|
+
self.sender_id = str(uuid.uuid4())
|
|
32
|
+
AsyncDriplineNode._seq += 1
|
|
33
|
+
|
|
34
|
+
self.alerts_exchange = self.rmq.topic_exchange('alerts')
|
|
35
|
+
self.requests_exchange = self.rmq.topic_exchange('requests')
|
|
36
|
+
|
|
37
|
+
self.reply_queue_node = None # to be set by endpoint()
|
|
38
|
+
self.request_queue_node = None # to be set by request()
|
|
39
|
+
self.sensor_value_queue_node = None # to be set by self.sensors_value_queue()
|
|
40
|
+
self.heartbeat_queue_node = None # to be set by self.heartbeat_queue()
|
|
41
|
+
self.status_message_queue_node = None # to be set by self.status_message_queue()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def aio_close(self):
|
|
45
|
+
if self.rmq is not None:
|
|
46
|
+
await self.rmq.aio_close()
|
|
47
|
+
self.rmq = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## child nodes ##
|
|
51
|
+
# dripline().endpoint(name): set/get to other dripline endpoints
|
|
52
|
+
def endpoint(self, name:str, *, specifier:str=None, lockout_key:str=None, timeout=None):
|
|
53
|
+
return EndpointNode(self, name, specifier=specifier, lockout_key=lockout_key, timeout=timeout)
|
|
54
|
+
|
|
55
|
+
# dripline().request(): handles set/get/cmd requests from other dripline services
|
|
56
|
+
def request(self, handler):
|
|
57
|
+
return RequestNode(self, handler)
|
|
58
|
+
|
|
59
|
+
# dripline().sensor_value_alert(): sends sensor_value alert; use aio_set(value)
|
|
60
|
+
def sensor_value_alert(self, name:str=None):
|
|
61
|
+
return SensorValueAlertNode(self, name)
|
|
62
|
+
|
|
63
|
+
# dripline().heartbeat_alert(): sends heartbeat alerts; use aio_set(value)
|
|
64
|
+
def heartbeat_alert(self):
|
|
65
|
+
return HeartbeatAlertNode(self)
|
|
66
|
+
|
|
67
|
+
# dripline().status_message_alert(): sends status_message alerts; use aio_set(value)
|
|
68
|
+
def status_message_alert(self, message_type='notice'):
|
|
69
|
+
return StatusMessageAlertNode(self, message_type)
|
|
70
|
+
|
|
71
|
+
# dripline().sensor_value_queue(): receives sensor_value; use aio_get()
|
|
72
|
+
def sensor_value_queue(self):
|
|
73
|
+
return SensorValuesQueueNode(self)
|
|
74
|
+
|
|
75
|
+
# dripline().heartbeat_queue(): receives heartbeat; use aio_get()
|
|
76
|
+
def heartbeat_queue(self):
|
|
77
|
+
return HeartbeatQueueNode(self)
|
|
78
|
+
|
|
79
|
+
# dripline().status_message_queue(): receives status_messages; use aio_get()
|
|
80
|
+
def status_message_queue(self):
|
|
81
|
+
return StatusMessageQueueNode(self)
|
|
82
|
+
|
|
83
|
+
# dripline().service(server):
|
|
84
|
+
def service(self, server, *, endpoints:list[str]|None=None):
|
|
85
|
+
return ServiceNode(self, server, endpoints=endpoints)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def _node_creator_method(cls):
|
|
90
|
+
def async_dripline(self, *args, **kwargs):
|
|
91
|
+
if True:
|
|
92
|
+
return AsyncDriplineNode(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
self.dripline_node
|
|
96
|
+
except:
|
|
97
|
+
self.dripline_node = AsyncDriplineNode(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
return self.dripline_node
|
|
100
|
+
|
|
101
|
+
return async_dripline
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class EndpointNode(ControlNode):
|
|
106
|
+
def __init__(self, dripline:AsyncDriplineNode, name:str, *, specifier:str=None, lockout_key:str=None, timeout=None):
|
|
107
|
+
self.specifier = specifier or ''
|
|
108
|
+
self.lockout_key = lockout_key or '00000000-0000-0000-0000-000000000000'
|
|
109
|
+
|
|
110
|
+
if dripline.reply_queue_node is None:
|
|
111
|
+
dripline.reply_queue_node = (
|
|
112
|
+
dripline.requests_exchange.queue(f'{dripline.name}_reply', timeout=(timeout or 5), exclusive=True)
|
|
113
|
+
)
|
|
114
|
+
self.reply_queue_node = dripline.reply_queue_node
|
|
115
|
+
self.routing_key = name
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def aio_set(self, value):
|
|
119
|
+
operation_code = 0 # 0: Set, 1: Get, 9: Command
|
|
120
|
+
if type(value) is dict:
|
|
121
|
+
body = copy.deepcopy(value)
|
|
122
|
+
elif type(value) is list:
|
|
123
|
+
body = { 'values': value }
|
|
124
|
+
else:
|
|
125
|
+
body = { 'values': [value] }
|
|
126
|
+
return await self.aio_do_send_request(operation_code, body)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def aio_get(self):
|
|
130
|
+
operation_code = 1 # 0: Set, 1: Get, 9: Command
|
|
131
|
+
body = {}
|
|
132
|
+
return await self.aio_do_send_request(operation_code, body)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
## child nodes ##
|
|
136
|
+
# dripline().endpoint(name).value_raw(): aio_get() returns 'value_raw'
|
|
137
|
+
def value_raw(self):
|
|
138
|
+
return RawValueNode(self)
|
|
139
|
+
|
|
140
|
+
# dripline().endpoint(name).value_cal(): aio_get() returns 'value_cal'
|
|
141
|
+
def value_cal(self):
|
|
142
|
+
return CalibratedValueNode(self)
|
|
143
|
+
|
|
144
|
+
# dripline().endpoint(name).command(*args,**kwargs): aio_get() sends a command to other endpoints
|
|
145
|
+
def command(self, *args, **kwargs):
|
|
146
|
+
return EndpointCommandNode(self, *args, **kwargs)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def aio_do_send_request(self, operation_code:int, body):
|
|
150
|
+
now = datetime.datetime.fromtimestamp(time.time(), tz=datetime.timezone.utc)
|
|
151
|
+
|
|
152
|
+
headers = {
|
|
153
|
+
'message_operation': operation_code, # 0: Set, 1: Get, 9: Command
|
|
154
|
+
'message_type': 3, # 2: Reply, 3: Request, 4: Alert
|
|
155
|
+
'lockout_key': self.lockout_key,
|
|
156
|
+
'specifier': self.specifier,
|
|
157
|
+
'timestamp': now.isoformat().replace('+00:00', 'Z'),
|
|
158
|
+
'sender_info': sender_info,
|
|
159
|
+
}
|
|
160
|
+
parameters = {
|
|
161
|
+
'message_id': f'{uuid.uuid4()}/0/1',
|
|
162
|
+
'content_encoding': 'application/json',
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
message = await self.reply_queue_node.rpc_call(self.routing_key, headers, body, parameters).aio_get()
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logging.error(f'SlowDrip.Endpoint[{self.routing_key}]: RPC error: {e}')
|
|
169
|
+
return None
|
|
170
|
+
if message is None:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
if type(message.body) is dict:
|
|
174
|
+
return message.body
|
|
175
|
+
else:
|
|
176
|
+
return json.loads(message.body or '{}')
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class EndpointCommandNode(ControlVariableNode):
|
|
181
|
+
def __init__(self, endpoint:EndpointNode, *args, **kwargs):
|
|
182
|
+
self.endpoint_node = endpoint
|
|
183
|
+
self.body = {'values': [ arg for arg in args ] }
|
|
184
|
+
self.body.update({ k:v for k,v in kwargs.items() })
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
async def aio_get(self):
|
|
188
|
+
operation_code = 9 # 0: Set, 1: Get, 9: Command
|
|
189
|
+
return await self.endpoint_node.aio_do_send_request(operation_code, self.body)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class RawValueNode(ControlVariableNode):
|
|
194
|
+
def __init__(self, endpoint:EndpointNode):
|
|
195
|
+
self.endpoint = endpoint
|
|
196
|
+
|
|
197
|
+
async def aio_set(self, value):
|
|
198
|
+
return await self.endpoint.aio_set(value)
|
|
199
|
+
|
|
200
|
+
async def aio_get(self):
|
|
201
|
+
return ((await self.endpoint.aio_get()) or {}).get('value_raw', None)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class CalibratedValueNode(ControlNode):
|
|
206
|
+
def __init__(self, endpoint:EndpointNode):
|
|
207
|
+
self.endpoint = endpoint
|
|
208
|
+
|
|
209
|
+
async def aio_get(self):
|
|
210
|
+
return ((await self.endpoint.aio_get()) or {}).get('value_cal', None)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class RequestNode(ControlNode):
|
|
215
|
+
def __init__(self, dripline:AsyncDriplineNode, handler):
|
|
216
|
+
self.handler = handler
|
|
217
|
+
|
|
218
|
+
if dripline.request_queue_node is None:
|
|
219
|
+
dripline.request_queue_node = (
|
|
220
|
+
dripline.requests_exchange.queue(f'{dripline.name}', exclusive=True)
|
|
221
|
+
)
|
|
222
|
+
self.request_queue_node = dripline.request_queue_node
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def aio_get(self):
|
|
226
|
+
try:
|
|
227
|
+
return await self.request_queue_node.rpc_function(self.handler).aio_get()
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logging(e)
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class SensorValueAlertNode(ControlNode):
|
|
235
|
+
def __init__(self, dripline:AsyncDriplineNode, name:str=None):
|
|
236
|
+
self.name = name or dripline.name
|
|
237
|
+
self.sender_id = dripline.sender_id
|
|
238
|
+
self.publisher_node = dripline.alerts_exchange.publisher(f'sensor_value.{self.name}')
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def aio_set(self, value):
|
|
242
|
+
now = datetime.datetime.fromtimestamp(time.time(), tz=datetime.timezone.utc)
|
|
243
|
+
|
|
244
|
+
if type(value) is tuple and len(value) >= 2:
|
|
245
|
+
body = { 'value_raw': value[0], 'value_cal': value[1] }
|
|
246
|
+
elif type(value) is dict:
|
|
247
|
+
body = value
|
|
248
|
+
elif type(value) in [ bool, int, float, str ]:
|
|
249
|
+
body = { 'value_raw': value }
|
|
250
|
+
else:
|
|
251
|
+
# throw an error?
|
|
252
|
+
body = value
|
|
253
|
+
|
|
254
|
+
if True:
|
|
255
|
+
if (type(body) is not dict) or ('value_raw' not in body):
|
|
256
|
+
# throw an error?
|
|
257
|
+
return
|
|
258
|
+
if body['value_raw'] is None:
|
|
259
|
+
# Dripline logger does not like a None value
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
headers = {
|
|
263
|
+
'message_type': 4, # 2: Reply, 3: Request, 4: Alert
|
|
264
|
+
'timestamp': now.isoformat().replace('+00:00', 'Z'),
|
|
265
|
+
'sender_info': sender_info,
|
|
266
|
+
}
|
|
267
|
+
parameters = {
|
|
268
|
+
'message_id': f'{uuid.uuid4()}/0/1',
|
|
269
|
+
'content_encoding': 'application/json',
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return await self.publisher_node.aio_set((headers, body, parameters))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class HeartbeatAlertNode(ControlNode):
|
|
277
|
+
def __init__(self, dripline:AsyncDriplineNode):
|
|
278
|
+
self.name = dripline.name
|
|
279
|
+
self.sender_id = dripline.sender_id
|
|
280
|
+
self.publisher_node = dripline.alerts_exchange.publisher(f'heartbeat.{self.name}')
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
async def aio_set(self, value):
|
|
284
|
+
now = datetime.datetime.fromtimestamp(value or time.time(), tz=datetime.timezone.utc)
|
|
285
|
+
|
|
286
|
+
body = {
|
|
287
|
+
'id': self.sender_id,
|
|
288
|
+
'name': self.name,
|
|
289
|
+
}
|
|
290
|
+
headers = {
|
|
291
|
+
'message_type': 4, # 2: Reply, 3: Request, 4: Alert
|
|
292
|
+
'timestamp': now.isoformat().replace('+00:00', 'Z'),
|
|
293
|
+
'sender_info': sender_info,
|
|
294
|
+
}
|
|
295
|
+
parameters = {
|
|
296
|
+
'message_id': f'{uuid.uuid4()}/0/1',
|
|
297
|
+
'content_encoding': 'application/json',
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return await self.publisher_node.aio_set((headers, body, parameters))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class StatusMessageAlertNode(ControlNode):
|
|
305
|
+
def __init__(self, dripline:AsyncDriplineNode, message_type='notice'):
|
|
306
|
+
self.name = dripline.name
|
|
307
|
+
self.sender_id = dripline.sender_id
|
|
308
|
+
self.publisher_node = dripline.alerts_exchange.publisher(f'status_message.{self.name}.{message_type}')
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
async def aio_set(self, value):
|
|
312
|
+
now = datetime.datetime.fromtimestamp(time.time(), tz=datetime.timezone.utc)
|
|
313
|
+
|
|
314
|
+
body = {
|
|
315
|
+
'message': value,
|
|
316
|
+
}
|
|
317
|
+
headers = {
|
|
318
|
+
'message_type': 4, # 2: Reply, 3: Request, 4: Alert
|
|
319
|
+
'timestamp': now.isoformat().replace('+00:00', 'Z'),
|
|
320
|
+
'sender_info': sender_info,
|
|
321
|
+
}
|
|
322
|
+
parameters = {
|
|
323
|
+
'message_id': f'{uuid.uuid4()}/0/1',
|
|
324
|
+
'content_encoding': 'application/json',
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return await self.publisher_node.aio_set((headers, body, parameters))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class SensorValuesQueueNode(ControlNode):
|
|
332
|
+
def __init__(self, dripline:AsyncDriplineNode):
|
|
333
|
+
if dripline.sensor_value_queue_node is None:
|
|
334
|
+
dripline.sensor_value_queue_node = (
|
|
335
|
+
dripline.alerts_exchange.queue(f'{dripline.name}_sensor_value', routing_key='sensor_value.*', exclusive=True)
|
|
336
|
+
)
|
|
337
|
+
self.queue_node = dripline.sensor_value_queue_node
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
async def aio_get(self):
|
|
341
|
+
try:
|
|
342
|
+
message = await self.queue_node.aio_get()
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logging.error(e)
|
|
345
|
+
return None
|
|
346
|
+
if message is None:
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
if message.body is None or type(message.body) is dict:
|
|
350
|
+
return message
|
|
351
|
+
else:
|
|
352
|
+
# Dripline puts content_type in the content_encoding fields, causing unparsed results
|
|
353
|
+
return type(message)(message.headers, json.loads(message.body or '{}'), message.parameters)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class HeartbeatQueueNode(ControlNode):
|
|
358
|
+
def __init__(self, dripline:AsyncDriplineNode):
|
|
359
|
+
if dripline.heartbeat_queue_node is None:
|
|
360
|
+
dripline.heartbeat_queue_node = (
|
|
361
|
+
dripline.alerts_exchange.queue(f'{dripline.name}_heartbeat', routing_key='heartbeat.*', exclusive=True)
|
|
362
|
+
)
|
|
363
|
+
self.queue_node = dripline.heartbeat_queue_node
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
async def aio_get(self):
|
|
367
|
+
try:
|
|
368
|
+
message = await self.queue_node.aio_get()
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logging.error(e)
|
|
371
|
+
return None
|
|
372
|
+
if message is None:
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
if message.body is None or type(message.body) is dict:
|
|
376
|
+
return message
|
|
377
|
+
else:
|
|
378
|
+
# Dripline puts content_type in the content_encoding fields, causing unparsed results
|
|
379
|
+
return type(message)(message.headers, json.loads(message.body or '{}'), message.parameters)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class StatusMessageQueueNode(ControlNode):
|
|
384
|
+
def __init__(self, dripline:AsyncDriplineNode):
|
|
385
|
+
if dripline.status_message_queue_node is None:
|
|
386
|
+
dripline.status_message_queue_node = (
|
|
387
|
+
dripline.alerts_exchange.queue(f'{dripline.name}_status_message', routing_key='status_message.*.*', exclusive=True)
|
|
388
|
+
)
|
|
389
|
+
self.queue_node = dripline.status_message_queue_node
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
async def aio_get(self):
|
|
393
|
+
try:
|
|
394
|
+
message = await self.queue_node.aio_get()
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logging.error(e)
|
|
397
|
+
return None
|
|
398
|
+
if message is None:
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
if message.body is None or type(message.body) is dict:
|
|
402
|
+
return message
|
|
403
|
+
else:
|
|
404
|
+
# Dripline puts content_type in the content_encoding fields, causing unparsed results
|
|
405
|
+
return type(message)(message.headers, json.loads(message.body or '{}'), message.parameters)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class ServiceNode(ControlNode):
|
|
410
|
+
def __init__(self, dripline:AsyncDriplineNode, server, *, endpoints:list[str]|str|None=None):
|
|
411
|
+
self.dripline_node = dripline
|
|
412
|
+
self.server = server
|
|
413
|
+
|
|
414
|
+
if type(endpoints) is list:
|
|
415
|
+
self.endpoints = list(endpoints)
|
|
416
|
+
else:
|
|
417
|
+
self.endpoints = [endpoints or '*']
|
|
418
|
+
|
|
419
|
+
routing_keys = list(self.endpoints) or '*'
|
|
420
|
+
|
|
421
|
+
self.request_queue_node = dripline.requests_exchange.queue(
|
|
422
|
+
name = dripline.name,
|
|
423
|
+
routing_key = routing_keys,
|
|
424
|
+
exclusive = True,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
self.heartbeat_alert = dripline.heartbeat_alert()
|
|
428
|
+
self.status_message_alert = dripline.status_message_alert()
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
async def aio_start(self):
|
|
432
|
+
await asyncio.gather(
|
|
433
|
+
self._handle_requests(),
|
|
434
|
+
self._send_heartbeats()
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
async def _send_heartbeats(self):
|
|
439
|
+
while not ctrl.is_stop_requested():
|
|
440
|
+
await self.heartbeat_alert.aio_set(time.time())
|
|
441
|
+
await self.status_message_alert.aio_set('I am working')
|
|
442
|
+
await ctrl.aio_sleep(30)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
async def _handle_requests(self):
|
|
446
|
+
while not ctrl.is_stop_requested():
|
|
447
|
+
try:
|
|
448
|
+
await self.request_queue_node.rpc_function(self._handle_message).aio_get()
|
|
449
|
+
except Exception as e:
|
|
450
|
+
logging.error(e)
|
|
451
|
+
logging.info('AsyncDripline.aio_start(): resuming in 3 seconds')
|
|
452
|
+
await ctrl.aio_sleep(3)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
async def _handle_message(self, message):
|
|
456
|
+
routing_key = message.parameters.get('routing_key')
|
|
457
|
+
operation = message.headers.get('message_operation', -1) # 0: Set, 1: Get, 9: Command
|
|
458
|
+
if operation < 0: # reply message
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
logging.debug(f'REQUEST: key={routing_key}, op={operation}, body={message.body}')
|
|
462
|
+
|
|
463
|
+
reply = None
|
|
464
|
+
if operation == 0:
|
|
465
|
+
if hasattr(self.server, 'on_set') and callable(getattr(self.server, 'on_set')):
|
|
466
|
+
reply = self.server.on_set(message)
|
|
467
|
+
elif operation == 1:
|
|
468
|
+
if hasattr(self.server, 'on_get') and callable(getattr(self.server, 'on_get')):
|
|
469
|
+
reply = self.server.on_get(message)
|
|
470
|
+
elif operation == 9:
|
|
471
|
+
if hasattr(self.server, 'on_command') and callable(getattr(self.server, 'on_command')):
|
|
472
|
+
reply = self.server.on_command(message)
|
|
473
|
+
else:
|
|
474
|
+
logging.warning(f'Dripline: Unknown operation code: {operation}')
|
|
475
|
+
|
|
476
|
+
if inspect.isawaitable(reply):
|
|
477
|
+
reply = await reply
|
|
478
|
+
|
|
479
|
+
if reply is None:
|
|
480
|
+
reply = {'status': 'ERROR: invalid request'}
|
|
481
|
+
logging.warning(f'Dripline: request not handled: key={routing_key}, op={operation}, body={message.body}')
|
|
482
|
+
elif type(reply) is dict:
|
|
483
|
+
pass
|
|
484
|
+
elif type(reply) is tuple and len(reply) >= 2:
|
|
485
|
+
reply = { 'value_raw': reply[0], 'value_cal': reply[1] }
|
|
486
|
+
elif type(reply) in [ bool, int, float, str ]:
|
|
487
|
+
reply = { 'value_raw': reply }
|
|
488
|
+
else:
|
|
489
|
+
# throw an error?
|
|
490
|
+
pass
|
|
491
|
+
|
|
492
|
+
return reply
|