wizata-dsapi 2.2.0.dev20__py3-none-any.whl → 2.2.0.dev21__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.
- wizata_dsapi/__init__.py +7 -4
- wizata_dsapi/connection.py +154 -44
- wizata_dsapi/connection_discovery.py +196 -0
- wizata_dsapi/connection_policy.py +194 -0
- wizata_dsapi/connection_types.py +199 -125
- wizata_dsapi/version.py +1 -1
- wizata_dsapi/wizata_dsapi_client.py +31 -0
- {wizata_dsapi-2.2.0.dev20.dist-info → wizata_dsapi-2.2.0.dev21.dist-info}/METADATA +1 -1
- {wizata_dsapi-2.2.0.dev20.dist-info → wizata_dsapi-2.2.0.dev21.dist-info}/RECORD +12 -10
- {wizata_dsapi-2.2.0.dev20.dist-info → wizata_dsapi-2.2.0.dev21.dist-info}/WHEEL +0 -0
- {wizata_dsapi-2.2.0.dev20.dist-info → wizata_dsapi-2.2.0.dev21.dist-info}/licenses/LICENSE.txt +0 -0
- {wizata_dsapi-2.2.0.dev20.dist-info → wizata_dsapi-2.2.0.dev21.dist-info}/top_level.txt +0 -0
wizata_dsapi/__init__.py
CHANGED
|
@@ -65,10 +65,13 @@ from .edge_device import EdgeDevice, EdgeDeviceStatus, EdgeDeviceType, EdgeDevic
|
|
|
65
65
|
from .edge_module import EdgeModule, EdgeModuleType, WIZATA_MODULES
|
|
66
66
|
from .edge_config import EdgeConsumer, EdgeWriter, EdgeTrigger, EdgeFile, EdgeConfig, EdgeCommand
|
|
67
67
|
from .edge_package import EdgePackage, EdgePackageOperation, EdgeRelease
|
|
68
|
-
from .connection import Connection,
|
|
69
|
-
EdgeDeviceKey, EdgeDeviceKeyStatus
|
|
70
|
-
from .connection_types import ConnectionType,
|
|
71
|
-
list_connection_types
|
|
68
|
+
from .connection import Connection, ConnectionChannel, ConnectionSignal, ConnectionDirection, \
|
|
69
|
+
TransformKind, EdgeDeviceKey, EdgeDeviceKeyStatus
|
|
70
|
+
from .connection_types import ConnectionType, ChannelType, CONNECTION_TYPES, SYSTEM_TYPES, \
|
|
71
|
+
TRANSFORM_SCHEMAS, get_connection_type, list_connection_types
|
|
72
|
+
from .connection_policy import DatapointPolicy, PolicyError, canonical_node_id
|
|
73
|
+
from .connection_discovery import ConnectionSeenName, ConnectionSeenReport, ConnectionBrowse, \
|
|
74
|
+
SEEN_OUTCOMES, BROWSE_REQUESTED, BROWSE_STATUSES, BROWSE_EXPIRES_SECONDS
|
|
72
75
|
from .product import ProductKind, Product, ProductComponent, BUILTIN_PRODUCT_KINDS, is_valid_product_key
|
|
73
76
|
from .inventory import (InventoryBalance, InventoryStorage, InventoryStorageDatapoint, inventory_mode,
|
|
74
77
|
InventoryStock, InventoryStockProduct, InventoryStockLine, InventoryStockCount, INVENTORY_OPTION, INVENTORY_MODES,
|
wizata_dsapi/connection.py
CHANGED
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
Edge connection DTOs (DEV-6013).
|
|
3
3
|
|
|
4
4
|
An edge connector used to be one untyped dict in the device's ``consumers.json``,
|
|
5
|
-
credentials inline. It is now
|
|
5
|
+
credentials inline. It is now (12.2 redesign) three entities, the same for every type:
|
|
6
6
|
|
|
7
7
|
Connection — how to reach a source or a target (endpoint, credentials, certificates),
|
|
8
|
-
bound to one edge device. ``config`` is clear; ``secrets`` holds one
|
|
9
|
-
per secret field, readable by the owning edge only.
|
|
8
|
+
bound to one edge device and entered once. ``config`` is clear; ``secrets`` holds one
|
|
9
|
+
sealed envelope per secret field, readable by the owning edge only. ``cloud`` (hub) and
|
|
10
|
+
``edge`` (tsdb) are the device's built-in connections.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
ConnectionChannel — a reader or a writer of a connection: what it reads or writes
|
|
13
|
+
(``spec``, shaped by the type) and its datapoint policy (all / list / mapping).
|
|
14
|
+
|
|
15
|
+
ConnectionSignal — one row of a policy: a reader's name and the datapoint it goes on as,
|
|
16
|
+
or a datapoint and where a writer sends it.
|
|
15
17
|
|
|
16
18
|
EdgeDeviceKey — the public half of the key pair an edge generated for itself. Secrets
|
|
17
19
|
are sealed for the edge's active key; rotating adds a row and retires the previous.
|
|
@@ -74,6 +76,17 @@ class Connection:
|
|
|
74
76
|
self.secrets: dict = {}
|
|
75
77
|
self.created_at: Optional[datetime] = None
|
|
76
78
|
self.updated_at: Optional[datetime] = None
|
|
79
|
+
# read-only summaries a search adds: [ConnectionChannel], without their rows
|
|
80
|
+
self.readers: Optional[list] = None
|
|
81
|
+
self.writers: Optional[list] = None
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def system(self) -> bool:
|
|
85
|
+
"""A device's built-in ``cloud`` / ``edge`` connection (DEV-6013): created by the
|
|
86
|
+
platform, never deleted, only its writer's policy edited."""
|
|
87
|
+
from .connection_types import get_connection_type
|
|
88
|
+
connection_type = get_connection_type(self.connection_type)
|
|
89
|
+
return bool(connection_type and connection_type.system)
|
|
77
90
|
|
|
78
91
|
def secrets_set(self) -> list:
|
|
79
92
|
"""Names of the secret fields holding a value."""
|
|
@@ -99,6 +112,7 @@ class Connection:
|
|
|
99
112
|
obj["connectionType"] = self.connection_type
|
|
100
113
|
if self.edge_device_id is not None:
|
|
101
114
|
obj["edgeDeviceId"] = self.edge_device_id
|
|
115
|
+
obj["system"] = self.system
|
|
102
116
|
obj["config"] = dict(self.config or {})
|
|
103
117
|
if include_secrets:
|
|
104
118
|
obj["secrets"] = dict(self.secrets or {})
|
|
@@ -113,6 +127,10 @@ class Connection:
|
|
|
113
127
|
obj["createdAt"] = _ts_out(self.created_at)
|
|
114
128
|
if self.updated_at is not None:
|
|
115
129
|
obj["updatedAt"] = _ts_out(self.updated_at)
|
|
130
|
+
if self.readers is not None:
|
|
131
|
+
obj["readers"] = [c.to_dict() for c in self.readers]
|
|
132
|
+
if self.writers is not None:
|
|
133
|
+
obj["writers"] = [c.to_dict() for c in self.writers]
|
|
116
134
|
return obj
|
|
117
135
|
|
|
118
136
|
@classmethod
|
|
@@ -144,50 +162,70 @@ class Connection:
|
|
|
144
162
|
f"type={self.connection_type!r}, edge={self.edge_device_id!r})")
|
|
145
163
|
|
|
146
164
|
|
|
147
|
-
class
|
|
165
|
+
class ConnectionChannel:
|
|
148
166
|
"""
|
|
149
|
-
|
|
150
|
-
PK ``id`` (UUID)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- ``
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- ``
|
|
158
|
-
``
|
|
159
|
-
- ``
|
|
167
|
+
A reader or a writer of a connection (DEV-6013, 12.2 redesign), stored in PostgreSQL
|
|
168
|
+
``"ConnectionChannel"``. PK ``id`` (UUID); FK ``connection_id`` → ``Connection.id``,
|
|
169
|
+
cascade on delete.
|
|
170
|
+
|
|
171
|
+
- ``direction``: READ (a reader, a consumers.json entry) or WRITE (a writer, a
|
|
172
|
+
writers.json entry).
|
|
173
|
+
- ``key``: unique per device and direction; it is the consumer or writer ``id`` on the
|
|
174
|
+
edge, so it is what the edge's logs and seen names call it.
|
|
175
|
+
- ``spec``: what it reads or writes, shaped by the connection type
|
|
176
|
+
(``connection_types``): OPC nodes and cadence, MQTT topics and function, a queue...
|
|
177
|
+
- ``policy_mode`` / ``prefix``: its datapoint policy (``connection_policy``); the rows
|
|
178
|
+
of the policy are its ``ConnectionSignal`` s.
|
|
179
|
+
- ``enabled``: a disabled reader or writer is not rendered.
|
|
180
|
+
- ``signals``: on create only, the policy rows to write with it (read back from search).
|
|
181
|
+
- ``signal_count`` / ``mapped_count``: read-only, how many rows and how many name a
|
|
182
|
+
datapoint.
|
|
160
183
|
"""
|
|
161
184
|
|
|
162
185
|
def __init__(self):
|
|
163
186
|
self.id: Optional[str] = None
|
|
164
187
|
self.connection_id: Optional[str] = None
|
|
188
|
+
self.edge_device_id: Optional[str] = None
|
|
165
189
|
self.direction: ConnectionDirection = ConnectionDirection.READ
|
|
166
|
-
self.
|
|
167
|
-
self.
|
|
168
|
-
self.
|
|
169
|
-
self.
|
|
170
|
-
self.
|
|
190
|
+
self.key: Optional[str] = None
|
|
191
|
+
self.name: Optional[str] = None
|
|
192
|
+
self.enabled: bool = True
|
|
193
|
+
self.spec: dict = {}
|
|
194
|
+
self.policy_mode: Optional[str] = None
|
|
195
|
+
self.prefix: Optional[str] = None
|
|
196
|
+
self.signals: Optional[list] = None
|
|
197
|
+
self.signal_count: Optional[int] = None
|
|
198
|
+
self.mapped_count: Optional[int] = None
|
|
171
199
|
self.created_at: Optional[datetime] = None
|
|
172
200
|
self.updated_at: Optional[datetime] = None
|
|
173
201
|
|
|
202
|
+
@property
|
|
203
|
+
def is_reader(self) -> bool:
|
|
204
|
+
return ConnectionDirection(self.direction) == ConnectionDirection.READ
|
|
205
|
+
|
|
174
206
|
def to_dict(self) -> dict:
|
|
175
207
|
obj = {}
|
|
176
208
|
if self.id is not None:
|
|
177
209
|
obj["id"] = str(self.id)
|
|
178
210
|
if self.connection_id is not None:
|
|
179
211
|
obj["connectionId"] = str(self.connection_id)
|
|
180
|
-
|
|
181
|
-
|
|
212
|
+
if self.edge_device_id is not None:
|
|
213
|
+
obj["edgeDeviceId"] = self.edge_device_id
|
|
214
|
+
obj["direction"] = (self.direction.value if isinstance(self.direction, ConnectionDirection)
|
|
182
215
|
else self.direction)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
216
|
+
if self.key is not None:
|
|
217
|
+
obj["key"] = self.key
|
|
218
|
+
if self.name is not None:
|
|
219
|
+
obj["name"] = self.name
|
|
220
|
+
obj["enabled"] = bool(self.enabled)
|
|
221
|
+
obj["spec"] = dict(self.spec or {})
|
|
222
|
+
obj["policy"] = {"mode": self.policy_mode, "prefix": self.prefix or ""}
|
|
223
|
+
if self.signals is not None:
|
|
224
|
+
obj["signals"] = [s.to_dict() if hasattr(s, "to_dict") else dict(s) for s in self.signals]
|
|
225
|
+
if self.signal_count is not None:
|
|
226
|
+
obj["signalCount"] = self.signal_count
|
|
227
|
+
if self.mapped_count is not None:
|
|
228
|
+
obj["mappedCount"] = self.mapped_count
|
|
191
229
|
if self.created_at is not None:
|
|
192
230
|
obj["createdAt"] = _ts_out(self.created_at)
|
|
193
231
|
if self.updated_at is not None:
|
|
@@ -195,19 +233,32 @@ class ConnectionMapping:
|
|
|
195
233
|
return obj
|
|
196
234
|
|
|
197
235
|
@classmethod
|
|
198
|
-
def from_dict(cls, data: dict) -> "
|
|
236
|
+
def from_dict(cls, data: dict) -> "ConnectionChannel":
|
|
199
237
|
obj = cls()
|
|
200
238
|
obj.id = data.get("id")
|
|
201
239
|
obj.connection_id = data.get("connectionId") or data.get("connection_id")
|
|
240
|
+
obj.edge_device_id = data.get("edgeDeviceId") or data.get("edge_device_id")
|
|
202
241
|
raw_direction = data.get("direction")
|
|
203
242
|
if raw_direction is not None:
|
|
204
243
|
obj.direction = ConnectionDirection(str(raw_direction).lower())
|
|
205
|
-
obj.
|
|
206
|
-
obj.
|
|
207
|
-
|
|
208
|
-
obj.
|
|
209
|
-
obj.
|
|
210
|
-
|
|
244
|
+
obj.key = data.get("key")
|
|
245
|
+
obj.name = data.get("name")
|
|
246
|
+
enabled = data.get("enabled")
|
|
247
|
+
obj.enabled = True if enabled is None else bool(enabled)
|
|
248
|
+
obj.spec = data.get("spec") or {}
|
|
249
|
+
policy = data.get("policy")
|
|
250
|
+
if isinstance(policy, dict):
|
|
251
|
+
obj.policy_mode = policy.get("mode")
|
|
252
|
+
obj.prefix = policy.get("prefix")
|
|
253
|
+
else:
|
|
254
|
+
obj.policy_mode = data.get("policyMode") or data.get("policy_mode")
|
|
255
|
+
obj.prefix = data.get("prefix")
|
|
256
|
+
signals = data.get("signals")
|
|
257
|
+
if signals is not None:
|
|
258
|
+
obj.signals = [s if isinstance(s, ConnectionSignal) else ConnectionSignal.from_dict(s)
|
|
259
|
+
for s in signals]
|
|
260
|
+
obj.signal_count = data.get("signalCount")
|
|
261
|
+
obj.mapped_count = data.get("mappedCount")
|
|
211
262
|
obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
|
|
212
263
|
obj.updated_at = _ts_in(data.get("updatedAt") or data.get("updated_at"))
|
|
213
264
|
return obj
|
|
@@ -216,14 +267,73 @@ class ConnectionMapping:
|
|
|
216
267
|
return str(self.id) if self.id else None
|
|
217
268
|
|
|
218
269
|
def api_key(self) -> str:
|
|
219
|
-
return self.
|
|
270
|
+
return self.key
|
|
271
|
+
|
|
272
|
+
def set_id(self, value):
|
|
273
|
+
self.id = str(value)
|
|
274
|
+
|
|
275
|
+
def __repr__(self) -> str:
|
|
276
|
+
return (f"ConnectionChannel(id={self.id!r}, key={self.key!r}, direction={self.direction!r}, "
|
|
277
|
+
f"connection={self.connection_id!r}, policy={self.policy_mode!r})")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ConnectionSignal:
|
|
281
|
+
"""
|
|
282
|
+
One row of a reader's or writer's datapoint policy (DEV-6013, 12.2 redesign), stored in
|
|
283
|
+
PostgreSQL ``"ConnectionSignal"``. PK ``id`` (UUID); FK ``channel_id`` →
|
|
284
|
+
``ConnectionChannel.id``, cascade on delete. Unique per channel: ``tag``, and
|
|
285
|
+
``hardware_id`` when set.
|
|
286
|
+
|
|
287
|
+
- For a **reader**, ``tag`` is the reader's own name for what it read (an OPC node id,
|
|
288
|
+
a point name, a function's output) and ``hardware_id`` the datapoint it goes on as.
|
|
289
|
+
In a ``list`` policy a row with no ``hardware_id`` lets the tag through (prefixed).
|
|
290
|
+
- For a **writer**, ``hardware_id`` is the datapoint and ``tag`` where it goes (the OPC
|
|
291
|
+
node, the name written). In a ``list`` policy a row with no ``tag`` lets the
|
|
292
|
+
datapoint through.
|
|
293
|
+
- A row with both always renames, whatever the mode.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
def __init__(self, tag: str = None, hardware_id: str = None):
|
|
297
|
+
self.id: Optional[str] = None
|
|
298
|
+
self.channel_id: Optional[str] = None
|
|
299
|
+
self.tag: Optional[str] = tag
|
|
300
|
+
self.hardware_id: Optional[str] = hardware_id
|
|
301
|
+
self.created_at: Optional[datetime] = None
|
|
302
|
+
self.updated_at: Optional[datetime] = None
|
|
303
|
+
|
|
304
|
+
def to_dict(self) -> dict:
|
|
305
|
+
obj = {}
|
|
306
|
+
if self.id is not None:
|
|
307
|
+
obj["id"] = str(self.id)
|
|
308
|
+
if self.channel_id is not None:
|
|
309
|
+
obj["channelId"] = str(self.channel_id)
|
|
310
|
+
obj["tag"] = self.tag
|
|
311
|
+
obj["hardwareId"] = self.hardware_id
|
|
312
|
+
if self.created_at is not None:
|
|
313
|
+
obj["createdAt"] = _ts_out(self.created_at)
|
|
314
|
+
if self.updated_at is not None:
|
|
315
|
+
obj["updatedAt"] = _ts_out(self.updated_at)
|
|
316
|
+
return obj
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def from_dict(cls, data: dict) -> "ConnectionSignal":
|
|
320
|
+
obj = cls()
|
|
321
|
+
obj.id = data.get("id")
|
|
322
|
+
obj.channel_id = data.get("channelId") or data.get("channel_id")
|
|
323
|
+
obj.tag = data.get("tag")
|
|
324
|
+
obj.hardware_id = data.get("hardwareId") or data.get("hardware_id")
|
|
325
|
+
obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
|
|
326
|
+
obj.updated_at = _ts_in(data.get("updatedAt") or data.get("updated_at"))
|
|
327
|
+
return obj
|
|
328
|
+
|
|
329
|
+
def api_id(self) -> str:
|
|
330
|
+
return str(self.id) if self.id else None
|
|
220
331
|
|
|
221
332
|
def set_id(self, value):
|
|
222
333
|
self.id = str(value)
|
|
223
334
|
|
|
224
335
|
def __repr__(self) -> str:
|
|
225
|
-
return
|
|
226
|
-
f"direction={self.direction!r}, hardware_id={self.hardware_id!r})")
|
|
336
|
+
return f"ConnectionSignal(tag={self.tag!r}, hardware_id={self.hardware_id!r})"
|
|
227
337
|
|
|
228
338
|
|
|
229
339
|
class EdgeDeviceKey:
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
What an edge tells the platform about its connections (DEV-6013, release 12.2):
|
|
3
|
+
|
|
4
|
+
- **seen names**: every name a reader produced, and what its policy did with it. A new
|
|
5
|
+
reader lets nothing through (an empty list), so this is how a user sees what to allow.
|
|
6
|
+
**Names, counts and times, never values**: what a policy dropped must not leave the edge
|
|
7
|
+
through the report.
|
|
8
|
+
- **browse results**: the nodes of an OPC-UA server, browsed by the edge on a platform
|
|
9
|
+
request (``browse_opc`` command) or by hand on a manual edge (``browse.py``).
|
|
10
|
+
|
|
11
|
+
Stored in PostgreSQL (alembic 0029): ``"ConnectionSeenName"``, ``"ConnectionSeenReport"``,
|
|
12
|
+
``"ConnectionBrowse"``.
|
|
13
|
+
"""
|
|
14
|
+
import uuid
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from .connection import _ts_in, _ts_out
|
|
19
|
+
|
|
20
|
+
SEEN_OUTCOMES = ("passed", "renamed", "dropped")
|
|
21
|
+
|
|
22
|
+
BROWSE_REQUESTED = "requested"
|
|
23
|
+
BROWSE_STATUSES = (BROWSE_REQUESTED, "ok", "partial", "failed", "refused")
|
|
24
|
+
#: A request the edge never answered reads as expired after this (the command is served
|
|
25
|
+
#: 15 minutes, a browse runs up to 5, and its upload is retried up to 15).
|
|
26
|
+
BROWSE_EXPIRES_SECONDS = 3600
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConnectionSeenName:
|
|
30
|
+
"""
|
|
31
|
+
One name a reader produced (PK ``channel_id`` + ``name``), as the edge last reported it.
|
|
32
|
+
|
|
33
|
+
- ``name``: the reader's native name (OPC node id or display name, a point name, what a
|
|
34
|
+
function emitted), before its policy.
|
|
35
|
+
- ``hardware_id`` / ``outcome``: what the policy the edge ran made of it (``passed``,
|
|
36
|
+
``renamed``, ``dropped``); ``hardware_id`` is None when dropped.
|
|
37
|
+
- ``count``: messages since the edge's reader started; ``first_seen`` / ``last_seen``.
|
|
38
|
+
- ``value_type``: ``number``, ``text``, ``bool``... never the value.
|
|
39
|
+
- ``current_hardware_id`` / ``current_outcome``: read-only, what the policy stored now
|
|
40
|
+
makes of the name (the edge applies it on its next sync).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, channel_id: str = None, name: str = None):
|
|
44
|
+
self.channel_id: Optional[str] = channel_id
|
|
45
|
+
self.name: Optional[str] = name
|
|
46
|
+
self.hardware_id: Optional[str] = None
|
|
47
|
+
self.outcome: Optional[str] = None
|
|
48
|
+
self.count: int = 0
|
|
49
|
+
self.first_seen: Optional[datetime] = None
|
|
50
|
+
self.last_seen: Optional[datetime] = None
|
|
51
|
+
self.value_type: Optional[str] = None
|
|
52
|
+
self.reported_at: Optional[datetime] = None
|
|
53
|
+
self.current_hardware_id: Optional[str] = None
|
|
54
|
+
self.current_outcome: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict:
|
|
57
|
+
obj = {"channelId": str(self.channel_id) if self.channel_id else None, "name": self.name,
|
|
58
|
+
"hardwareId": self.hardware_id, "outcome": self.outcome, "count": self.count,
|
|
59
|
+
"firstSeen": _ts_out(self.first_seen), "lastSeen": _ts_out(self.last_seen),
|
|
60
|
+
"valueType": self.value_type, "reportedAt": _ts_out(self.reported_at)}
|
|
61
|
+
if self.current_outcome is not None:
|
|
62
|
+
obj["current"] = {"hardwareId": self.current_hardware_id, "outcome": self.current_outcome}
|
|
63
|
+
return obj
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_dict(cls, data: dict) -> "ConnectionSeenName":
|
|
67
|
+
obj = cls(channel_id=data.get("channelId"), name=data.get("name"))
|
|
68
|
+
obj.hardware_id = data.get("hardwareId")
|
|
69
|
+
obj.outcome = data.get("outcome")
|
|
70
|
+
obj.count = int(data.get("count") or 0)
|
|
71
|
+
obj.first_seen = _ts_in(data.get("firstSeen"))
|
|
72
|
+
obj.last_seen = _ts_in(data.get("lastSeen"))
|
|
73
|
+
obj.value_type = data.get("valueType")
|
|
74
|
+
obj.reported_at = _ts_in(data.get("reportedAt"))
|
|
75
|
+
return obj
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
return f"ConnectionSeenName(name={self.name!r}, outcome={self.outcome!r})"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ConnectionSeenReport:
|
|
82
|
+
"""
|
|
83
|
+
The last report of one reader (PK ``channel_id``): the digest of the policy the edge
|
|
84
|
+
ran, so a user sees whether the edge applies the stored one yet; names beyond the
|
|
85
|
+
edge's cap (``overflow``) and messages it could not name (``invalid``).
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(self, channel_id: str = None):
|
|
89
|
+
self.channel_id: Optional[str] = channel_id
|
|
90
|
+
self.policy_digest: Optional[str] = None
|
|
91
|
+
self.overflow: int = 0
|
|
92
|
+
self.invalid: int = 0
|
|
93
|
+
self.reported_at: Optional[datetime] = None
|
|
94
|
+
|
|
95
|
+
def to_dict(self) -> dict:
|
|
96
|
+
return {"channelId": str(self.channel_id) if self.channel_id else None,
|
|
97
|
+
"policyDigest": self.policy_digest, "overflow": self.overflow,
|
|
98
|
+
"invalid": self.invalid, "reportedAt": _ts_out(self.reported_at)}
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_dict(cls, data: dict) -> "ConnectionSeenReport":
|
|
102
|
+
obj = cls(channel_id=data.get("channelId"))
|
|
103
|
+
obj.policy_digest = data.get("policyDigest")
|
|
104
|
+
obj.overflow = int(data.get("overflow") or 0)
|
|
105
|
+
obj.invalid = int(data.get("invalid") or 0)
|
|
106
|
+
obj.reported_at = _ts_in(data.get("reportedAt"))
|
|
107
|
+
return obj
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ConnectionBrowse:
|
|
111
|
+
"""
|
|
112
|
+
One OPC-UA browse of a connection (PK ``id``, the command id the edge answers with).
|
|
113
|
+
|
|
114
|
+
- ``source``: ``platform`` (requested here, sent as a ``browse_opc`` command) or
|
|
115
|
+
``edge`` (run by hand on a manual edge and uploaded).
|
|
116
|
+
- ``status``: ``requested`` until the edge answers, then the edge's ``ok``,
|
|
117
|
+
``partial`` (deadline or node cap reached), ``failed`` or ``refused``. Read as
|
|
118
|
+
``expired`` when a request got no answer within ``BROWSE_EXPIRES_SECONDS``.
|
|
119
|
+
- ``roots``, ``depth``, ``max_nodes``, ``timeout_seconds``: what was asked (the edge
|
|
120
|
+
clamps them).
|
|
121
|
+
- ``server``: ``{url, namespaces}``; ``nodes``: what the browser returned (node id,
|
|
122
|
+
names, class, data type, access, ``readable``, ``hasChildren``, parent, depth).
|
|
123
|
+
- ``pages`` / ``complete``: the edge uploads at most 1000 nodes per page; ``complete``
|
|
124
|
+
once its last page arrived.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, id: str = None, connection_id: str = None, edge_device_id: str = None):
|
|
128
|
+
self.id: Optional[str] = id
|
|
129
|
+
self.connection_id: Optional[str] = connection_id
|
|
130
|
+
self.edge_device_id: Optional[str] = edge_device_id
|
|
131
|
+
self.source: str = "platform"
|
|
132
|
+
self.status: str = BROWSE_REQUESTED
|
|
133
|
+
self.roots: Optional[list] = None
|
|
134
|
+
self.depth: Optional[int] = None
|
|
135
|
+
self.max_nodes: Optional[int] = None
|
|
136
|
+
self.timeout_seconds: Optional[int] = None
|
|
137
|
+
self.requested_by: Optional[str] = None
|
|
138
|
+
self.requested_at: Optional[datetime] = None
|
|
139
|
+
self.started_at: Optional[str] = None
|
|
140
|
+
self.finished_at: Optional[str] = None
|
|
141
|
+
self.error: Optional[str] = None
|
|
142
|
+
self.truncated: bool = False
|
|
143
|
+
self.server: Optional[dict] = None
|
|
144
|
+
self.nodes: list = []
|
|
145
|
+
self.node_count: int = 0
|
|
146
|
+
self.pages: int = 0
|
|
147
|
+
self.complete: bool = False
|
|
148
|
+
|
|
149
|
+
def api_id(self) -> str:
|
|
150
|
+
return str(self.id) if self.id else None
|
|
151
|
+
|
|
152
|
+
def set_id(self, value):
|
|
153
|
+
self.id = str(value)
|
|
154
|
+
|
|
155
|
+
def get_id_type(self) -> type:
|
|
156
|
+
return uuid.UUID
|
|
157
|
+
|
|
158
|
+
def to_dict(self, include_nodes: bool = True) -> dict:
|
|
159
|
+
obj = {"id": str(self.id) if self.id else None,
|
|
160
|
+
"connectionId": str(self.connection_id) if self.connection_id else None,
|
|
161
|
+
"edgeDeviceId": self.edge_device_id, "source": self.source, "status": self.status,
|
|
162
|
+
"roots": self.roots, "depth": self.depth, "maxNodes": self.max_nodes,
|
|
163
|
+
"timeoutSeconds": self.timeout_seconds, "requestedBy": self.requested_by,
|
|
164
|
+
"requestedAt": _ts_out(self.requested_at), "startedAt": self.started_at,
|
|
165
|
+
"finishedAt": self.finished_at, "error": self.error, "truncated": bool(self.truncated),
|
|
166
|
+
"server": self.server, "nodeCount": self.node_count, "pages": self.pages,
|
|
167
|
+
"complete": bool(self.complete)}
|
|
168
|
+
if include_nodes:
|
|
169
|
+
obj["nodes"] = list(self.nodes or [])
|
|
170
|
+
return obj
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_dict(cls, data: dict) -> "ConnectionBrowse":
|
|
174
|
+
obj = cls(id=data.get("id"), connection_id=data.get("connectionId"),
|
|
175
|
+
edge_device_id=data.get("edgeDeviceId"))
|
|
176
|
+
obj.source = data.get("source") or "platform"
|
|
177
|
+
obj.status = data.get("status") or BROWSE_REQUESTED
|
|
178
|
+
obj.roots = data.get("roots")
|
|
179
|
+
obj.depth = data.get("depth")
|
|
180
|
+
obj.max_nodes = data.get("maxNodes")
|
|
181
|
+
obj.timeout_seconds = data.get("timeoutSeconds")
|
|
182
|
+
obj.requested_by = data.get("requestedBy")
|
|
183
|
+
obj.requested_at = _ts_in(data.get("requestedAt"))
|
|
184
|
+
obj.started_at = data.get("startedAt")
|
|
185
|
+
obj.finished_at = data.get("finishedAt")
|
|
186
|
+
obj.error = data.get("error")
|
|
187
|
+
obj.truncated = bool(data.get("truncated"))
|
|
188
|
+
obj.server = data.get("server")
|
|
189
|
+
obj.nodes = list(data.get("nodes") or [])
|
|
190
|
+
obj.node_count = int(data.get("nodeCount") or len(obj.nodes))
|
|
191
|
+
obj.pages = int(data.get("pages") or 0)
|
|
192
|
+
obj.complete = bool(data.get("complete"))
|
|
193
|
+
return obj
|
|
194
|
+
|
|
195
|
+
def __repr__(self) -> str:
|
|
196
|
+
return f"ConnectionBrowse(id={self.id!r}, status={self.status!r}, nodes={self.node_count})"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DEV-6013 (12.2 redesign): the datapoint policy of an edge reader or writer, and the one
|
|
3
|
+
resolution of it that both the platform and the edge run.
|
|
4
|
+
|
|
5
|
+
A reader names what it reads with its own **tag**: an OPC node id or name (per its
|
|
6
|
+
``name_source``), a modbus or S7 point name, or the ``HardwareId`` an MQTT or RabbitMQ
|
|
7
|
+
function emits. A writer receives datapoints (``HardwareId``). The policy decides what
|
|
8
|
+
goes on, and under which name:
|
|
9
|
+
|
|
10
|
+
| Name ``n`` | Result |
|
|
11
|
+
|---|---|
|
|
12
|
+
| in ``mapping`` | ``mapping[n]``, verbatim, whatever the mode |
|
|
13
|
+
| mode ``all`` | ``prefix + n`` |
|
|
14
|
+
| mode ``list`` and ``n`` in ``names`` | ``prefix + n`` |
|
|
15
|
+
| otherwise | dropped (``None``) |
|
|
16
|
+
|
|
17
|
+
So ``mapping`` mode is strict: a name with no mapping is dropped. ``list`` matches the
|
|
18
|
+
native name, so changing the prefix never empties a list. ``all`` with mapping entries is
|
|
19
|
+
how a 12.1 OPC consumer named its nodes (its ``mapping``, else ``prefix + name``). For a
|
|
20
|
+
writer, the mapping is ``{datapoint: target}``: the OPC node id, or the name written to
|
|
21
|
+
the cloud or to the store.
|
|
22
|
+
|
|
23
|
+
The platform renders a policy into every ``consumers.json`` / ``writers.json`` entry as
|
|
24
|
+
``{"mode", "prefix", "names", "mapping"}``. message_client reads it back with
|
|
25
|
+
``DatapointPolicy.from_dict`` and applies ``resolve``. A policy that cannot be read is a
|
|
26
|
+
``PolicyError``; the edge then drops everything for that reader or writer (fail closed),
|
|
27
|
+
because a policy is what keeps data from leaving.
|
|
28
|
+
"""
|
|
29
|
+
import hashlib
|
|
30
|
+
import json
|
|
31
|
+
|
|
32
|
+
MODE_ALL = "all"
|
|
33
|
+
MODE_LIST = "list"
|
|
34
|
+
MODE_MAPPING = "mapping"
|
|
35
|
+
MODES = (MODE_ALL, MODE_LIST, MODE_MAPPING)
|
|
36
|
+
|
|
37
|
+
OUTCOME_PASSED = "passed"
|
|
38
|
+
OUTCOME_RENAMED = "renamed"
|
|
39
|
+
OUTCOME_DROPPED = "dropped"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PolicyError(ValueError):
|
|
43
|
+
"""The policy cannot be read: a mode it does not know, or names that are not text."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DatapointPolicy:
|
|
47
|
+
"""
|
|
48
|
+
An immutable datapoint policy. Build it with ``from_dict`` (the rendered form),
|
|
49
|
+
``from_datapoints`` (a writers.json entry without one), ``deny`` or ``allow_all``.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
__slots__ = ("mode", "prefix", "names", "mapping", "_digest")
|
|
53
|
+
|
|
54
|
+
def __init__(self, mode: str = MODE_LIST, prefix: str = None, names=None, mapping: dict = None):
|
|
55
|
+
if mode not in MODES:
|
|
56
|
+
raise PolicyError(f"unknown policy mode {mode!r}: use one of {', '.join(MODES)}")
|
|
57
|
+
if prefix is None:
|
|
58
|
+
prefix = ""
|
|
59
|
+
if not isinstance(prefix, str):
|
|
60
|
+
raise PolicyError("the policy prefix must be text")
|
|
61
|
+
listed = set()
|
|
62
|
+
for name in names or ():
|
|
63
|
+
if not isinstance(name, str) or not name:
|
|
64
|
+
raise PolicyError(f"a listed name must be non-empty text, not {name!r}")
|
|
65
|
+
listed.add(name)
|
|
66
|
+
renames = {}
|
|
67
|
+
if mapping is not None and not isinstance(mapping, dict):
|
|
68
|
+
raise PolicyError("the policy mapping must be an object {name: name}")
|
|
69
|
+
for source, target in (mapping or {}).items():
|
|
70
|
+
if not isinstance(source, str) or not source:
|
|
71
|
+
raise PolicyError(f"a mapped name must be non-empty text, not {source!r}")
|
|
72
|
+
if not isinstance(target, str) or not target:
|
|
73
|
+
raise PolicyError(f"{source!r} must map to a non-empty name, not {target!r}")
|
|
74
|
+
renames[source] = target
|
|
75
|
+
object.__setattr__(self, "mode", mode)
|
|
76
|
+
object.__setattr__(self, "prefix", prefix)
|
|
77
|
+
object.__setattr__(self, "names", frozenset(listed))
|
|
78
|
+
object.__setattr__(self, "mapping", renames)
|
|
79
|
+
object.__setattr__(self, "_digest", None)
|
|
80
|
+
|
|
81
|
+
def __setattr__(self, key, value):
|
|
82
|
+
raise AttributeError("a DatapointPolicy is immutable")
|
|
83
|
+
|
|
84
|
+
def __eq__(self, other):
|
|
85
|
+
return isinstance(other, DatapointPolicy) and self.to_dict() == other.to_dict()
|
|
86
|
+
|
|
87
|
+
def __hash__(self):
|
|
88
|
+
return hash(self.digest())
|
|
89
|
+
|
|
90
|
+
def __repr__(self):
|
|
91
|
+
return (f"DatapointPolicy(mode={self.mode!r}, prefix={self.prefix!r}, "
|
|
92
|
+
f"names={len(self.names)}, mapping={len(self.mapping)})")
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------ builders
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_dict(cls, data: dict) -> "DatapointPolicy":
|
|
98
|
+
"""The rendered form ``{"mode", "prefix", "names", "mapping"}``. A missing mode is
|
|
99
|
+
``list``, so a policy that says nothing lets nothing through. Unknown keys are
|
|
100
|
+
ignored, so a newer platform can add some."""
|
|
101
|
+
if not isinstance(data, dict):
|
|
102
|
+
raise PolicyError(f"a policy must be an object, not {type(data).__name__}")
|
|
103
|
+
names = data.get("names")
|
|
104
|
+
if names is not None and not isinstance(names, (list, tuple, set, frozenset)):
|
|
105
|
+
raise PolicyError("the policy names must be a list")
|
|
106
|
+
return cls(mode=data.get("mode", MODE_LIST), prefix=data.get("prefix"),
|
|
107
|
+
names=names, mapping=data.get("mapping"))
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def from_datapoints(cls, datapoints) -> "DatapointPolicy":
|
|
111
|
+
"""
|
|
112
|
+
The policy a writers.json entry without one has always meant: ``"*"`` is all, a
|
|
113
|
+
list keeps those datapoints, and a dict (an OPC writer's ``{datapoint: node id}``)
|
|
114
|
+
keeps its keys and does not rename them (the writer maps them itself). Anything
|
|
115
|
+
else, including no ``datapoints`` at all, keeps nothing.
|
|
116
|
+
"""
|
|
117
|
+
if datapoints == "*":
|
|
118
|
+
return cls.allow_all()
|
|
119
|
+
if isinstance(datapoints, dict):
|
|
120
|
+
return cls(mode=MODE_LIST, names=[k for k in datapoints if isinstance(k, str) and k])
|
|
121
|
+
if isinstance(datapoints, (list, tuple, set, frozenset)):
|
|
122
|
+
return cls(mode=MODE_LIST, names=[k for k in datapoints if isinstance(k, str) and k])
|
|
123
|
+
return cls.deny()
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def deny(cls) -> "DatapointPolicy":
|
|
127
|
+
"""An empty list: nothing goes on. What a new reader or cloud/edge writer starts with."""
|
|
128
|
+
return cls(mode=MODE_LIST)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def allow_all(cls, prefix: str = None) -> "DatapointPolicy":
|
|
132
|
+
return cls(mode=MODE_ALL, prefix=prefix)
|
|
133
|
+
|
|
134
|
+
# ------------------------------------------------------------------ resolution
|
|
135
|
+
|
|
136
|
+
def resolve(self, name):
|
|
137
|
+
"""The name ``name`` goes on as, or ``None`` when the policy drops it."""
|
|
138
|
+
if not isinstance(name, str):
|
|
139
|
+
return None
|
|
140
|
+
mapped = self.mapping.get(name)
|
|
141
|
+
if mapped is not None:
|
|
142
|
+
return mapped
|
|
143
|
+
if self.mode == MODE_ALL or (self.mode == MODE_LIST and name in self.names):
|
|
144
|
+
return self.prefix + name
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def allows(self, name) -> bool:
|
|
148
|
+
return self.resolve(name) is not None
|
|
149
|
+
|
|
150
|
+
def outcome(self, name) -> str:
|
|
151
|
+
"""``passed`` (same name), ``renamed`` or ``dropped``: what the edge reports for a
|
|
152
|
+
name it saw."""
|
|
153
|
+
resolved = self.resolve(name)
|
|
154
|
+
if resolved is None:
|
|
155
|
+
return OUTCOME_DROPPED
|
|
156
|
+
return OUTCOME_PASSED if resolved == name else OUTCOME_RENAMED
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def is_identity(self) -> bool:
|
|
160
|
+
"""Every name goes on unchanged: nothing to filter, nothing to copy."""
|
|
161
|
+
return self.mode == MODE_ALL and not self.prefix and not self.mapping
|
|
162
|
+
|
|
163
|
+
# ------------------------------------------------------------------ rendering
|
|
164
|
+
|
|
165
|
+
def to_dict(self) -> dict:
|
|
166
|
+
"""The rendered form, sorted, so the same policy always renders the same bytes."""
|
|
167
|
+
return {
|
|
168
|
+
"mode": self.mode,
|
|
169
|
+
"prefix": self.prefix,
|
|
170
|
+
"names": sorted(self.names),
|
|
171
|
+
"mapping": {k: self.mapping[k] for k in sorted(self.mapping)},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
def digest(self) -> str:
|
|
175
|
+
"""``sha256:<hex>`` of the rendered form. The edge reports the digest it applies, so
|
|
176
|
+
the platform can tell an edit the edge already runs from one still on its way."""
|
|
177
|
+
if self._digest is None:
|
|
178
|
+
text = json.dumps(self.to_dict(), sort_keys=True, separators=(",", ":"))
|
|
179
|
+
object.__setattr__(self, "_digest", "sha256:" + hashlib.sha256(text.encode()).hexdigest())
|
|
180
|
+
return self._digest
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def canonical_node_id(text: str) -> str:
|
|
184
|
+
"""
|
|
185
|
+
A light canonical form of an OPC node id, for the platform, which has no OPC stack:
|
|
186
|
+
surrounding spaces trimmed and the default namespace dropped (``ns=0;i=85`` and
|
|
187
|
+
``i=85`` are one node). The edge normalises with asyncua, which agrees on these forms.
|
|
188
|
+
"""
|
|
189
|
+
if not isinstance(text, str):
|
|
190
|
+
return text
|
|
191
|
+
text = text.strip()
|
|
192
|
+
if text.startswith("ns=0;"):
|
|
193
|
+
text = text[len("ns=0;"):]
|
|
194
|
+
return text
|
wizata_dsapi/connection_types.py
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Connection type registry (DEV-6013).
|
|
2
|
+
Connection type registry (DEV-6013; readers and writers since the 12.2 redesign).
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
A **connection** says how an edge reaches something: its endpoint and credentials, entered
|
|
5
|
+
once. A connection holds **readers** and **writers**, and each one has:
|
|
6
|
+
- a **spec**: what it reads or writes, and how (OPC nodes and cadence, MQTT topics and the
|
|
7
|
+
function parsing them, a queue, Modbus points, S7 blocks);
|
|
8
|
+
- a **datapoint policy** (``connection_policy``): ``all``, ``list`` or ``mapping``, over
|
|
9
|
+
the reader's own names (its *tag*) or the writer's datapoints.
|
|
10
|
+
|
|
11
|
+
One definition per type serves three things:
|
|
5
12
|
- the front-end lists the types and builds each form from its JSON Schemas;
|
|
6
|
-
- the platform validates every connection and
|
|
13
|
+
- the platform validates every connection, reader and writer against them on write — the
|
|
7
14
|
single place a secret-bearing key is refused outside ``secrets``;
|
|
8
15
|
- the platform renders the edge configuration from the entities.
|
|
9
16
|
|
|
10
|
-
Schemas are plain JSON Schema (draft 2020-12) dicts
|
|
11
|
-
|
|
17
|
+
Schemas are plain JSON Schema (draft 2020-12) dicts. Secret fields are ``writeOnly``:
|
|
18
|
+
accepted on write, never returned. Key names are the entity's, not the legacy
|
|
19
|
+
``consumers.json`` spelling; rendering maps them back.
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
``hub`` (key ``cloud``) and ``tsdb`` (key ``edge``) are **system** types: every device gets
|
|
22
|
+
them from the platform, with one writer each, and only their policy is edited. Their
|
|
23
|
+
endpoint is the edge's own environment.
|
|
16
24
|
"""
|
|
17
25
|
|
|
18
26
|
from typing import Optional
|
|
19
27
|
|
|
20
28
|
from .connection import ConnectionDirection, TransformKind
|
|
29
|
+
from .connection_policy import MODE_ALL, MODE_LIST, MODE_MAPPING
|
|
21
30
|
|
|
22
31
|
|
|
23
32
|
_READ = ConnectionDirection.READ.value
|
|
24
33
|
_WRITE = ConnectionDirection.WRITE.value
|
|
34
|
+
_ALL_MODES = [MODE_ALL, MODE_LIST, MODE_MAPPING]
|
|
25
35
|
|
|
26
36
|
|
|
27
37
|
def _obj(properties: dict, required: list = None) -> dict:
|
|
@@ -31,6 +41,13 @@ def _obj(properties: dict, required: list = None) -> dict:
|
|
|
31
41
|
return schema
|
|
32
42
|
|
|
33
43
|
|
|
44
|
+
def _array(items: dict, min_items: int = None) -> dict:
|
|
45
|
+
schema = {"type": "array", "items": items}
|
|
46
|
+
if min_items is not None:
|
|
47
|
+
schema["minItems"] = min_items
|
|
48
|
+
return schema
|
|
49
|
+
|
|
50
|
+
|
|
34
51
|
def _secret(title: str, pem: bool = False) -> dict:
|
|
35
52
|
field = {"type": "string", "title": title, "writeOnly": True}
|
|
36
53
|
if pem:
|
|
@@ -44,38 +61,74 @@ def _pem(title: str) -> dict:
|
|
|
44
61
|
|
|
45
62
|
_EMPTY = _obj({})
|
|
46
63
|
|
|
64
|
+
_FUNCTION = _obj({
|
|
65
|
+
"filepath": {"type": "string", "title": "Function file", "minLength": 1},
|
|
66
|
+
"function_name": {"type": "string", "title": "Function name", "minLength": 1},
|
|
67
|
+
}, required=["filepath", "function_name"])
|
|
68
|
+
|
|
69
|
+
_POLL = {"type": "integer", "title": "Poll interval (ms)", "minimum": 10, "default": 1000}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ChannelType:
|
|
73
|
+
"""
|
|
74
|
+
What a reader or writer of a type is.
|
|
75
|
+
|
|
76
|
+
:param spec: JSON Schema of its ``spec``.
|
|
77
|
+
:param policy_modes: the policy modes it accepts.
|
|
78
|
+
:param tag: what its native names are, for the form ("the OPC node id").
|
|
79
|
+
:param default_mode: the mode a new one starts with. Readers and the cloud/edge
|
|
80
|
+
writers start as an empty list: a wrong configuration must not flood the platform.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, spec: dict, policy_modes: list, tag: str, default_mode: str = MODE_LIST):
|
|
84
|
+
self.spec = spec
|
|
85
|
+
self.policy_modes = policy_modes
|
|
86
|
+
self.tag = tag
|
|
87
|
+
self.default_mode = default_mode
|
|
88
|
+
|
|
89
|
+
def describe(self) -> dict:
|
|
90
|
+
return {"spec": self.spec, "policyModes": list(self.policy_modes), "tag": self.tag,
|
|
91
|
+
"defaultMode": self.default_mode}
|
|
92
|
+
|
|
47
93
|
|
|
48
94
|
class ConnectionType:
|
|
49
95
|
"""
|
|
50
|
-
One supported
|
|
96
|
+
One supported connection type.
|
|
51
97
|
|
|
52
98
|
:param name: identifier stored on ``Connection.connection_type``.
|
|
53
|
-
:param directions: what mappings may do — read, write, or both.
|
|
54
|
-
:param transforms: transform kinds a read mapping may carry.
|
|
55
|
-
:param polled: mappings take a ``poll_interval_ms`` (else the source pushes).
|
|
56
|
-
:param hardware_id_required: per direction, whether a mapping must name its
|
|
57
|
-
datapoint (False where the source names it).
|
|
58
99
|
:param config: schema for ``Connection.config``.
|
|
59
100
|
:param secrets: schema for ``Connection.secrets`` (plaintext write form).
|
|
60
|
-
:param
|
|
101
|
+
:param reader: what its readers are, or None when it cannot read.
|
|
102
|
+
:param writer: what its writers are, or None when it cannot write.
|
|
103
|
+
:param system: created by the platform with the device (``cloud`` / ``edge``).
|
|
104
|
+
:param device_types: the device types it exists on (None: every type).
|
|
105
|
+
:param system_key: the key of the system connection, and of its writer.
|
|
61
106
|
"""
|
|
62
107
|
|
|
63
108
|
def __init__(self, name: str, label: str, description: str,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
109
|
+
config: dict, secrets: dict,
|
|
110
|
+
reader: ChannelType = None, writer: ChannelType = None,
|
|
111
|
+
supports_browse: bool = False, system: bool = False,
|
|
112
|
+
device_types: list = None, system_key: str = None):
|
|
68
113
|
self.name = name
|
|
69
114
|
self.label = label
|
|
70
115
|
self.description = description
|
|
71
|
-
self.directions = directions
|
|
72
|
-
self.transforms = transforms
|
|
73
|
-
self.polled = polled
|
|
74
|
-
self.hardware_id_required = hardware_id_required
|
|
75
116
|
self.config = config
|
|
76
117
|
self.secrets = secrets
|
|
77
|
-
self.
|
|
118
|
+
self.reader = reader
|
|
119
|
+
self.writer = writer
|
|
78
120
|
self.supports_browse = supports_browse
|
|
121
|
+
self.system = system
|
|
122
|
+
self.device_types = device_types
|
|
123
|
+
self.system_key = system_key
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def directions(self) -> list:
|
|
127
|
+
return ([_READ] if self.reader else []) + ([_WRITE] if self.writer else [])
|
|
128
|
+
|
|
129
|
+
def channel(self, direction) -> Optional[ChannelType]:
|
|
130
|
+
direction = getattr(direction, "value", direction)
|
|
131
|
+
return self.reader if direction == _READ else self.writer if direction == _WRITE else None
|
|
79
132
|
|
|
80
133
|
@property
|
|
81
134
|
def secret_fields(self) -> list:
|
|
@@ -87,37 +140,66 @@ class ConnectionType:
|
|
|
87
140
|
"type": self.name,
|
|
88
141
|
"label": self.label,
|
|
89
142
|
"description": self.description,
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
143
|
+
"system": self.system,
|
|
144
|
+
"deviceTypes": list(self.device_types) if self.device_types else None,
|
|
145
|
+
"directions": self.directions,
|
|
93
146
|
"supportsBrowse": self.supports_browse,
|
|
94
147
|
"secretFields": self.secret_fields,
|
|
95
148
|
}
|
|
96
149
|
|
|
97
150
|
def schema(self) -> dict:
|
|
98
|
-
"""Everything a form needs: the summary
|
|
151
|
+
"""Everything a form needs: the summary, the connection's schemas, and per direction
|
|
152
|
+
the reader's or writer's spec schema and policy modes."""
|
|
99
153
|
out = self.describe()
|
|
100
|
-
out["hardwareIdRequired"] = dict(self.hardware_id_required)
|
|
101
154
|
out["config"] = self.config
|
|
102
155
|
out["secrets"] = self.secrets
|
|
103
|
-
out["
|
|
156
|
+
out["reader"] = self.reader.describe() if self.reader else None
|
|
157
|
+
out["writer"] = self.writer.describe() if self.writer else None
|
|
104
158
|
return out
|
|
105
159
|
|
|
106
160
|
|
|
161
|
+
# ── Built-in: the cloud hub and the edge's own store ───────────────────────────
|
|
162
|
+
|
|
163
|
+
HUB = ConnectionType(
|
|
164
|
+
name="hub",
|
|
165
|
+
label="Cloud",
|
|
166
|
+
description="The platform's hub (Event Hub by default), reached with the edge's own "
|
|
167
|
+
"settings. Its writer chooses which datapoints leave the edge, and under which name.",
|
|
168
|
+
config=_EMPTY, secrets=_EMPTY,
|
|
169
|
+
writer=ChannelType(spec=_EMPTY, policy_modes=_ALL_MODES,
|
|
170
|
+
tag="the datapoint; in a mapping, the name it is sent to the cloud as"),
|
|
171
|
+
system=True, system_key="cloud",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
TSDB = ConnectionType(
|
|
175
|
+
name="tsdb",
|
|
176
|
+
label="Edge store",
|
|
177
|
+
description="The edge's own time-series database, which its pipelines read. Its writer "
|
|
178
|
+
"chooses which datapoints the edge keeps.",
|
|
179
|
+
config=_EMPTY, secrets=_EMPTY,
|
|
180
|
+
writer=ChannelType(spec=_EMPTY, policy_modes=_ALL_MODES,
|
|
181
|
+
tag="the datapoint; in a mapping, the name it is stored as"),
|
|
182
|
+
system=True, system_key="edge", device_types=["ConnectivityAndPipelines"],
|
|
183
|
+
)
|
|
184
|
+
|
|
107
185
|
# ── OPC-UA ─────────────────────────────────────────────────────────────────────
|
|
108
186
|
|
|
109
187
|
_OPC_SECURITY_POLICIES = ["None", "Basic128Rsa15", "Basic256", "Basic256Sha256",
|
|
110
188
|
"Aes128Sha256RsaOaep", "Aes256Sha256RsaPss"]
|
|
111
189
|
|
|
190
|
+
_OPC_GROUP_TUNING = {
|
|
191
|
+
"batch_size": {"type": "integer", "minimum": 1, "title": "Nodes per read"},
|
|
192
|
+
"max_concurrent_batches": {"type": "integer", "minimum": 1},
|
|
193
|
+
"publishing_interval": {"type": "integer", "minimum": 0, "default": 1000},
|
|
194
|
+
"sampling_interval": {"type": "integer", "minimum": 0, "default": 0},
|
|
195
|
+
"queue_size": {"type": "integer", "minimum": 1, "default": 1},
|
|
196
|
+
}
|
|
197
|
+
|
|
112
198
|
OPC_UA = ConnectionType(
|
|
113
199
|
name="opc-ua",
|
|
114
200
|
label="OPC-UA",
|
|
115
|
-
description="OPC-UA server —
|
|
116
|
-
directions=[_READ, _WRITE],
|
|
117
|
-
transforms=[],
|
|
118
|
-
polled=True,
|
|
201
|
+
description="OPC-UA server — readers poll or subscribe to nodes, writers write to nodes.",
|
|
119
202
|
supports_browse=True,
|
|
120
|
-
hardware_id_required={_READ: False, _WRITE: True},
|
|
121
203
|
config=_obj({
|
|
122
204
|
"server_url": {"type": "string", "title": "Server URL", "pattern": "^opc\\.tcp://"},
|
|
123
205
|
"username": {"type": "string", "title": "Username"},
|
|
@@ -130,39 +212,36 @@ OPC_UA = ConnectionType(
|
|
|
130
212
|
"server_certificate": _pem("Server certificate"),
|
|
131
213
|
"session_timeout_ms": {"type": "integer", "title": "Session timeout (ms)",
|
|
132
214
|
"minimum": 1000, "default": 60000},
|
|
133
|
-
"reconnect_interval_ms": {"type": "integer", "title": "Reconnect interval (ms)",
|
|
134
|
-
"minimum": 1000, "default": 5000},
|
|
135
|
-
"mode": {"type": "string", "title": "Read mode",
|
|
136
|
-
"enum": ["polling", "subscription"], "default": "polling"},
|
|
137
|
-
"name_source": {"type": "string", "title": "Name unmapped nodes by",
|
|
138
|
-
"enum": ["name", "identifier"], "default": "name"},
|
|
139
|
-
"prefix": {"type": "string", "title": "Prefix for unmapped nodes"},
|
|
140
215
|
}, required=["server_url"]),
|
|
141
216
|
secrets=_obj({
|
|
142
217
|
"password": _secret("Password"),
|
|
143
218
|
"client_private_key": _secret("Client private key", pem=True),
|
|
144
219
|
}),
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}, required=["
|
|
163
|
-
|
|
164
|
-
},
|
|
165
|
-
|
|
220
|
+
reader=ChannelType(
|
|
221
|
+
spec=_obj({
|
|
222
|
+
"mode": {"type": "string", "title": "Read mode",
|
|
223
|
+
"enum": ["polling", "subscription"], "default": "polling"},
|
|
224
|
+
"name_source": {"type": "string", "title": "Name nodes by",
|
|
225
|
+
"enum": ["nodeid", "name", "identifier"], "default": "nodeid"},
|
|
226
|
+
"reconnect_interval_ms": {"type": "integer", "title": "Reconnect interval (ms)",
|
|
227
|
+
"minimum": 1000, "default": 5000},
|
|
228
|
+
"poll_interval_ms": _POLL,
|
|
229
|
+
"nodes_from_policy": {"type": "boolean", "title": "Read the nodes listed in the policy",
|
|
230
|
+
"default": False},
|
|
231
|
+
"nodes": _array(_obj({
|
|
232
|
+
"node_ids": _array({"type": "string", "minLength": 1}, min_items=1),
|
|
233
|
+
"children": {"type": "boolean", "title": "And everything under them", "default": False},
|
|
234
|
+
"children_depth": {"type": "integer", "minimum": 1},
|
|
235
|
+
"poll_interval_ms": _POLL,
|
|
236
|
+
**_OPC_GROUP_TUNING,
|
|
237
|
+
}, required=["node_ids"])),
|
|
238
|
+
**{k: v for k, v in _OPC_GROUP_TUNING.items()},
|
|
239
|
+
}),
|
|
240
|
+
policy_modes=_ALL_MODES,
|
|
241
|
+
tag="the OPC node id (name_source nodeid), or its display name / identifier",
|
|
242
|
+
),
|
|
243
|
+
writer=ChannelType(spec=_EMPTY, policy_modes=[MODE_MAPPING],
|
|
244
|
+
tag="the node id each datapoint is written to", default_mode=MODE_MAPPING),
|
|
166
245
|
)
|
|
167
246
|
|
|
168
247
|
# ── MQTT ───────────────────────────────────────────────────────────────────────
|
|
@@ -170,17 +249,12 @@ OPC_UA = ConnectionType(
|
|
|
170
249
|
MQTT = ConnectionType(
|
|
171
250
|
name="mqtt",
|
|
172
251
|
label="MQTT",
|
|
173
|
-
description="MQTT broker — subscribe to topics,
|
|
174
|
-
directions=[_READ],
|
|
175
|
-
transforms=[TransformKind.FUNCTION.value],
|
|
176
|
-
polled=False,
|
|
177
|
-
hardware_id_required={_READ: False},
|
|
252
|
+
description="MQTT broker — readers subscribe to topics, a function parses the payloads.",
|
|
178
253
|
config=_obj({
|
|
179
254
|
"host": {"type": "string", "title": "Broker host"},
|
|
180
255
|
"port": {"type": "integer", "title": "Port", "default": 1883},
|
|
181
256
|
"tls": {"type": "boolean", "title": "TLS", "default": False},
|
|
182
257
|
"username": {"type": "string", "title": "Username"},
|
|
183
|
-
"client_id": {"type": "string", "title": "Client id"},
|
|
184
258
|
"keepalive": {"type": "integer", "title": "Keepalive (s)", "minimum": 1, "default": 60},
|
|
185
259
|
"payload_format": {"type": "string", "enum": ["json", "raw"], "default": "json"},
|
|
186
260
|
"reconnect_min": {"type": "number", "minimum": 0, "default": 1.0},
|
|
@@ -191,16 +265,18 @@ MQTT = ConnectionType(
|
|
|
191
265
|
secrets=_obj({
|
|
192
266
|
"password": _secret("Password"),
|
|
193
267
|
}),
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"
|
|
268
|
+
reader=ChannelType(
|
|
269
|
+
spec=_obj({
|
|
270
|
+
"topics": _array(_obj({
|
|
197
271
|
"topic": {"type": "string", "title": "Topic", "examples": ["plant/line1/#"]},
|
|
198
|
-
}, required=["topic"]),
|
|
199
|
-
"options": _obj({
|
|
200
272
|
"qos": {"type": "integer", "enum": [0, 1, 2], "default": 1},
|
|
201
|
-
}),
|
|
202
|
-
|
|
203
|
-
|
|
273
|
+
}, required=["topic"]), min_items=1),
|
|
274
|
+
"client_id": {"type": "string", "title": "Client id"},
|
|
275
|
+
"function": _FUNCTION,
|
|
276
|
+
}, required=["topics"]),
|
|
277
|
+
policy_modes=_ALL_MODES,
|
|
278
|
+
tag="the HardwareId the function (or the payload) gives each value",
|
|
279
|
+
),
|
|
204
280
|
)
|
|
205
281
|
|
|
206
282
|
# ── Modbus TCP ─────────────────────────────────────────────────────────────────
|
|
@@ -208,31 +284,29 @@ MQTT = ConnectionType(
|
|
|
208
284
|
MODBUS = ConnectionType(
|
|
209
285
|
name="modbus",
|
|
210
286
|
label="Modbus TCP",
|
|
211
|
-
description="Modbus TCP device — poll holding or input registers.",
|
|
212
|
-
directions=[_READ],
|
|
213
|
-
transforms=[TransformKind.EXPRESSION.value],
|
|
214
|
-
polled=True,
|
|
215
|
-
hardware_id_required={_READ: True},
|
|
287
|
+
description="Modbus TCP device — readers poll holding or input registers.",
|
|
216
288
|
config=_obj({
|
|
217
289
|
"ip_address": {"type": "string", "title": "IP address"},
|
|
218
290
|
"port": {"type": "integer", "title": "Port", "default": 502},
|
|
219
291
|
"unit_id": {"type": "integer", "title": "Unit id", "minimum": 0, "default": 1},
|
|
220
292
|
}, required=["ip_address"]),
|
|
221
293
|
secrets=_EMPTY,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"
|
|
294
|
+
reader=ChannelType(
|
|
295
|
+
spec=_obj({
|
|
296
|
+
"poll_interval_ms": _POLL,
|
|
297
|
+
"points": _array(_obj({
|
|
298
|
+
"name": {"type": "string", "title": "Point name", "minLength": 1},
|
|
225
299
|
"address": {"type": "integer", "title": "Register address", "minimum": 0},
|
|
226
300
|
"quantity": {"type": "integer", "title": "Register count", "minimum": 1},
|
|
227
|
-
"register_type": {"type": "string", "enum": ["holding", "input"],
|
|
228
|
-
"default": "holding"},
|
|
229
|
-
}, required=["address", "quantity"]),
|
|
230
|
-
"options": _obj({
|
|
301
|
+
"register_type": {"type": "string", "enum": ["holding", "input"], "default": "holding"},
|
|
231
302
|
"pack": {"type": "string", "title": "struct pack format", "examples": [">HH"]},
|
|
232
303
|
"unpack": {"type": "string", "title": "struct unpack format", "examples": [">f"]},
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
304
|
+
"transform": {"type": "string", "title": "Expression on x", "examples": ["x / 100"]},
|
|
305
|
+
}, required=["name", "address", "quantity", "pack", "unpack"]), min_items=1),
|
|
306
|
+
}, required=["points"]),
|
|
307
|
+
policy_modes=_ALL_MODES,
|
|
308
|
+
tag="the point name",
|
|
309
|
+
),
|
|
236
310
|
)
|
|
237
311
|
|
|
238
312
|
# ── Siemens S7 (snap7) ─────────────────────────────────────────────────────────
|
|
@@ -240,11 +314,7 @@ MODBUS = ConnectionType(
|
|
|
240
314
|
SNAP7 = ConnectionType(
|
|
241
315
|
name="snap7",
|
|
242
316
|
label="Siemens S7",
|
|
243
|
-
description="Siemens S7 PLC — poll data blocks.",
|
|
244
|
-
directions=[_READ],
|
|
245
|
-
transforms=[],
|
|
246
|
-
polled=True,
|
|
247
|
-
hardware_id_required={_READ: True},
|
|
317
|
+
description="Siemens S7 PLC — readers poll data blocks.",
|
|
248
318
|
config=_obj({
|
|
249
319
|
"address": {"type": "string", "title": "PLC address"},
|
|
250
320
|
"rack": {"type": "integer", "minimum": 0, "default": 0},
|
|
@@ -252,19 +322,24 @@ SNAP7 = ConnectionType(
|
|
|
252
322
|
"tcp_port": {"type": "integer", "title": "Port", "default": 102},
|
|
253
323
|
}, required=["address"]),
|
|
254
324
|
secrets=_EMPTY,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
"
|
|
325
|
+
reader=ChannelType(
|
|
326
|
+
spec=_obj({
|
|
327
|
+
"poll_interval_ms": _POLL,
|
|
328
|
+
"blocks": _array(_obj({
|
|
258
329
|
"db_number": {"type": "integer", "minimum": 0},
|
|
259
330
|
"start_offset": {"type": "integer", "minimum": 0},
|
|
260
331
|
"size": {"type": "integer", "minimum": 1},
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
332
|
+
"points": _array(_obj({
|
|
333
|
+
"name": {"type": "string", "title": "Point name", "minLength": 1},
|
|
334
|
+
"type": {"type": "string", "enum": ["dword", "word", "bool", "real", "int"]},
|
|
335
|
+
"index": {"type": "integer", "title": "Byte index in the block", "minimum": 0},
|
|
336
|
+
"bit_position": {"type": "integer", "minimum": 0, "maximum": 7},
|
|
337
|
+
}, required=["name", "type", "index"]), min_items=1),
|
|
338
|
+
}, required=["db_number", "start_offset", "size", "points"]), min_items=1),
|
|
339
|
+
}, required=["blocks"]),
|
|
340
|
+
policy_modes=_ALL_MODES,
|
|
341
|
+
tag="the point name",
|
|
342
|
+
),
|
|
268
343
|
)
|
|
269
344
|
|
|
270
345
|
# ── RabbitMQ (external queue) ──────────────────────────────────────────────────
|
|
@@ -272,11 +347,7 @@ SNAP7 = ConnectionType(
|
|
|
272
347
|
RABBITMQ = ConnectionType(
|
|
273
348
|
name="rabbitmq",
|
|
274
349
|
label="RabbitMQ",
|
|
275
|
-
description="External RabbitMQ broker —
|
|
276
|
-
directions=[_READ],
|
|
277
|
-
transforms=[TransformKind.FUNCTION.value],
|
|
278
|
-
polled=False,
|
|
279
|
-
hardware_id_required={_READ: False},
|
|
350
|
+
description="External RabbitMQ broker — each reader consumes a queue, a function parses it.",
|
|
280
351
|
config=_obj({
|
|
281
352
|
"host": {"type": "string", "title": "Host"},
|
|
282
353
|
"port": {"type": "integer", "title": "Port", "default": 5671},
|
|
@@ -287,23 +358,26 @@ RABBITMQ = ConnectionType(
|
|
|
287
358
|
secrets=_obj({
|
|
288
359
|
"password": _secret("Password"),
|
|
289
360
|
}),
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
"
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
"
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
},
|
|
361
|
+
reader=ChannelType(
|
|
362
|
+
spec=_obj({
|
|
363
|
+
"queue": {"type": "string", "title": "Queue", "default": "messages"},
|
|
364
|
+
"prefetch": {"type": "integer", "minimum": 1},
|
|
365
|
+
"error_queue": {"type": "string"},
|
|
366
|
+
"function": _FUNCTION,
|
|
367
|
+
}, required=["queue"]),
|
|
368
|
+
policy_modes=_ALL_MODES,
|
|
369
|
+
tag="the HardwareId the function gives each value",
|
|
370
|
+
),
|
|
301
371
|
)
|
|
302
372
|
|
|
303
373
|
|
|
304
|
-
CONNECTION_TYPES = {t.name: t for t in (OPC_UA, MQTT, MODBUS, SNAP7, RABBITMQ)}
|
|
374
|
+
CONNECTION_TYPES = {t.name: t for t in (HUB, TSDB, OPC_UA, MQTT, MODBUS, SNAP7, RABBITMQ)}
|
|
375
|
+
|
|
376
|
+
#: The types a device gets from the platform, by the key of their connection.
|
|
377
|
+
SYSTEM_TYPES = {t.system_key: t for t in CONNECTION_TYPES.values() if t.system}
|
|
305
378
|
|
|
306
|
-
#
|
|
379
|
+
# Kept for the forms that still name the two transform kinds; a Modbus point carries its
|
|
380
|
+
# expression, an MQTT or RabbitMQ reader its function.
|
|
307
381
|
TRANSFORM_SCHEMAS = {
|
|
308
382
|
TransformKind.EXPRESSION.value: _obj({
|
|
309
383
|
"kind": {"const": TransformKind.EXPRESSION.value},
|
wizata_dsapi/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.2.0.
|
|
1
|
+
__version__ = "2.2.0.dev21"
|
|
@@ -3104,6 +3104,37 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
|
|
|
3104
3104
|
body={"packages": list(packages or [])}, timeout=timeout)
|
|
3105
3105
|
return response.json()
|
|
3106
3106
|
|
|
3107
|
+
def send_edge_browse_result(self, edge_device_id: str, page: dict, timeout=(5, 60)) -> dict:
|
|
3108
|
+
"""
|
|
3109
|
+
One page of an OPC browse the edge ran (DEV-6013): on a platform request
|
|
3110
|
+
(``requestId``), or run by hand on a manual edge (``requestId`` null). Pages hold
|
|
3111
|
+
at most 1000 nodes; the last one says ``final: true``.
|
|
3112
|
+
|
|
3113
|
+
:param page: ``{requestId, connectionId, page, final, status, error, truncated,
|
|
3114
|
+
startedAt, finishedAt, server, roots, depth, nodes: [...]}``.
|
|
3115
|
+
:param timeout: (connect, read) seconds.
|
|
3116
|
+
:return: the platform's answer.
|
|
3117
|
+
"""
|
|
3118
|
+
response = self._edge_request("POST", self._edge_route(edge_device_id, "browse"),
|
|
3119
|
+
body=page, expected=(200, 201), timeout=timeout)
|
|
3120
|
+
return response.json()
|
|
3121
|
+
|
|
3122
|
+
def send_edge_seen_names(self, edge_device_id: str, report: dict, timeout=(5, 30)) -> dict:
|
|
3123
|
+
"""
|
|
3124
|
+
The names each reader of the edge produced, and what its policy did with them
|
|
3125
|
+
(DEV-6013): names, counts and times, never values. A policy keeps data from
|
|
3126
|
+
leaving, and the report must not carry what it dropped.
|
|
3127
|
+
|
|
3128
|
+
:param report: ``{writtenAt, full, readers: [{readerId, connectionId, type,
|
|
3129
|
+
policyMode, policyDigest, overflow, invalid, names: [{name, hardwareId,
|
|
3130
|
+
outcome, count, firstSeen, lastSeen, valueType}]}]}``.
|
|
3131
|
+
:param timeout: (connect, read) seconds.
|
|
3132
|
+
:return: the platform's answer.
|
|
3133
|
+
"""
|
|
3134
|
+
response = self._edge_request("POST", self._edge_route(edge_device_id, "seen"),
|
|
3135
|
+
body=report, expected=(200, 201), timeout=timeout)
|
|
3136
|
+
return response.json()
|
|
3137
|
+
|
|
3107
3138
|
|
|
3108
3139
|
def api() -> WizataDSAPIClient:
|
|
3109
3140
|
"""
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
wizata_dsapi/__init__.py,sha256=
|
|
1
|
+
wizata_dsapi/__init__.py,sha256=I6R8Q2VGjMTZvV5ZiM1hkpQi1DyP4I04e95ljSjZ9SE,4516
|
|
2
2
|
wizata_dsapi/api_config.py,sha256=zOgvpkB3FE2ny_2vAui7B1hcAMFCLvfb2ep3m-b7dIw,8270
|
|
3
3
|
wizata_dsapi/api_dto.py,sha256=F9HjNa_vqYhdEMZwwk8E3SmCxuMBcxFnMp1eE9dxLvg,2910
|
|
4
4
|
wizata_dsapi/api_interface.py,sha256=mCq0-rlwWL4GKB2QnxSPuyTecYOXXknmPOlQe_SEf2I,14670
|
|
5
5
|
wizata_dsapi/bucket.py,sha256=Zz9olv-pymikAutGitSuGWrAPiawOTW86JDDHG4ugTc,1150
|
|
6
6
|
wizata_dsapi/business_label.py,sha256=u0TVfUNfoR9qSv8lzpf6rNjlg3G9xTiz6itefcKfeak,4151
|
|
7
|
-
wizata_dsapi/connection.py,sha256=
|
|
8
|
-
wizata_dsapi/
|
|
7
|
+
wizata_dsapi/connection.py,sha256=sngpF2rfcR1N5IT9wqccv07IwIDUM85k_6vnNjzThGE,17027
|
|
8
|
+
wizata_dsapi/connection_discovery.py,sha256=1CHEFQNE_IGtbbnyAfToZuXmP77PIjJJq_ASRW2mcpg,9055
|
|
9
|
+
wizata_dsapi/connection_policy.py,sha256=oU1yukVMdCZGy4tvj0Ei3u00xBDJ_0ZEMQ7WVsocroE,8434
|
|
10
|
+
wizata_dsapi/connection_types.py,sha256=ffpauxzzRBg0k4j5Amy-I3zeBZ-Q6EnmyZ-6Jo-FQlI,18358
|
|
9
11
|
wizata_dsapi/context.py,sha256=1t0wCzRnBEzch7fx7jG4Oj3Ezrjv9getJtCrqEpn-pc,13188
|
|
10
12
|
wizata_dsapi/dashboard.py,sha256=NY4m-1EpMRoKDHem0EwsRF0QQzal3bvrm9gQv3BNizk,3841
|
|
11
13
|
wizata_dsapi/dataframe_toolkit.py,sha256=7D8JrhsFSGXeO3mRAd2e2cLmdjbXRjk04IuaMG8F974,9078
|
|
@@ -48,10 +50,10 @@ wizata_dsapi/trigger.py,sha256=waOX2AhrLvFzNFRQ535j6iemgg_QJP2QsWGL2ipljFg,8848
|
|
|
48
50
|
wizata_dsapi/twin.py,sha256=8AnrxSaL7dMrILXJ3-GGmmUz078j3qqwzDTe6i86xqA,12672
|
|
49
51
|
wizata_dsapi/twinregistration.py,sha256=iXt55AVGAQNl0jmxa3ZVynPqlSsIunFf4sLfp-MWTw0,13244
|
|
50
52
|
wizata_dsapi/user.py,sha256=-hchKAMZULu9P00RUHDqu_5AT4NI5BYOJd8XCBrt3eQ,1950
|
|
51
|
-
wizata_dsapi/version.py,sha256=
|
|
53
|
+
wizata_dsapi/version.py,sha256=YrgaspNwsrXytFOHsMVGn4pLI4bQSL62FdMaB7nvDBs,28
|
|
52
54
|
wizata_dsapi/wizard_function.py,sha256=RbM7W7Gf-6Rhp_1dU9DBYkHaciknGAGvuAndhAS_vyo,942
|
|
53
55
|
wizata_dsapi/wizard_request.py,sha256=v6BaqKLKvTWmUSo0_gda9FabAQz5x_-GOH1Av50GzFo,3762
|
|
54
|
-
wizata_dsapi/wizata_dsapi_client.py,sha256=
|
|
56
|
+
wizata_dsapi/wizata_dsapi_client.py,sha256=0dEdcHB0sUcIRktvvYe36DI8kEtqrvFnmikrIR4gWes,132510
|
|
55
57
|
wizata_dsapi/words.py,sha256=tV8CqzCqODZCV7PgBxBF5exBxeF_ya9t5DiUy-cg6Sg,1535
|
|
56
58
|
wizata_dsapi/models/__init__.py,sha256=O3Tusyhl8hFb-5QaEBY7DJISRB8Gf_Xbxxr-aFDR1_U,380
|
|
57
59
|
wizata_dsapi/models/common.py,sha256=5BC3Dc6kMvqPGxHgoCuGVOynu_F1MWh7EJSzXIICSSg,27196
|
|
@@ -60,8 +62,8 @@ wizata_dsapi/plots/common.py,sha256=7F4WMBaddubnCpdSafgaI9U9Sz0Sngg0VREP_vnIMno,
|
|
|
60
62
|
wizata_dsapi/plots/theme.py,sha256=D4sSgEwabXVxF3z1GV8DivXJbbY61KgxXAWfjUbNDAc,3594
|
|
61
63
|
wizata_dsapi/scripts/__init__.py,sha256=9fRdTMuuXcqMkIsUPf5dwFsTWwBzKteQdAMVkyy2acQ,212
|
|
62
64
|
wizata_dsapi/scripts/common.py,sha256=8LJK23Za0Yy4Dda1feKpKELav8806MH-hxFJYrklVOY,14758
|
|
63
|
-
wizata_dsapi-2.2.0.
|
|
64
|
-
wizata_dsapi-2.2.0.
|
|
65
|
-
wizata_dsapi-2.2.0.
|
|
66
|
-
wizata_dsapi-2.2.0.
|
|
67
|
-
wizata_dsapi-2.2.0.
|
|
65
|
+
wizata_dsapi-2.2.0.dev21.dist-info/licenses/LICENSE.txt,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
66
|
+
wizata_dsapi-2.2.0.dev21.dist-info/METADATA,sha256=rv_Ua4W6BNoghD_NujFiFQ39BEu0CnIfN6AA6b1vWEI,14038
|
|
67
|
+
wizata_dsapi-2.2.0.dev21.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
68
|
+
wizata_dsapi-2.2.0.dev21.dist-info/top_level.txt,sha256=-OeTJbEnh5DuWyTOHtvw0Dw3LRg3G27TNS6W4ZtfwPs,13
|
|
69
|
+
wizata_dsapi-2.2.0.dev21.dist-info/RECORD,,
|
|
File without changes
|
{wizata_dsapi-2.2.0.dev20.dist-info → wizata_dsapi-2.2.0.dev21.dist-info}/licenses/LICENSE.txt
RENAMED
|
File without changes
|
|
File without changes
|