wizata-dsapi 2.2.0.dev1__py3-none-any.whl → 2.2.0.dev2__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 +4 -0
- wizata_dsapi/connection.py +299 -0
- wizata_dsapi/connection_types.py +325 -0
- wizata_dsapi/version.py +1 -1
- {wizata_dsapi-2.2.0.dev1.dist-info → wizata_dsapi-2.2.0.dev2.dist-info}/METADATA +1 -1
- {wizata_dsapi-2.2.0.dev1.dist-info → wizata_dsapi-2.2.0.dev2.dist-info}/RECORD +9 -7
- {wizata_dsapi-2.2.0.dev1.dist-info → wizata_dsapi-2.2.0.dev2.dist-info}/WHEEL +0 -0
- {wizata_dsapi-2.2.0.dev1.dist-info → wizata_dsapi-2.2.0.dev2.dist-info}/licenses/LICENSE.txt +0 -0
- {wizata_dsapi-2.2.0.dev1.dist-info → wizata_dsapi-2.2.0.dev2.dist-info}/top_level.txt +0 -0
wizata_dsapi/__init__.py
CHANGED
|
@@ -61,6 +61,10 @@ from .notification import NotificationScope, DeviceToken, AlertRule, AlertActive
|
|
|
61
61
|
from .edge_device import EdgeDevice, EdgeDeviceStatus, EdgeDeviceType
|
|
62
62
|
from .edge_module import EdgeModule, EdgeModuleType, WIZATA_MODULES
|
|
63
63
|
from .edge_config import EdgeConsumer, EdgeWriter, EdgeTrigger
|
|
64
|
+
from .connection import Connection, ConnectionMapping, ConnectionDirection, TransformKind, \
|
|
65
|
+
EdgeDeviceKey, EdgeDeviceKeyStatus
|
|
66
|
+
from .connection_types import ConnectionType, CONNECTION_TYPES, TRANSFORM_SCHEMAS, get_connection_type, \
|
|
67
|
+
list_connection_types
|
|
64
68
|
from .dashboard import Dashboard, DashboardType
|
|
65
69
|
from .mobile_asset import MobileAssetTile, MobileAssetTileType, MobileAssetConfig
|
|
66
70
|
from .graylog_log import GraylogLog
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Edge connection DTOs (DEV-6013).
|
|
3
|
+
|
|
4
|
+
An edge connector used to be one untyped dict in the device's ``consumers.json``,
|
|
5
|
+
credentials inline. It is now split in two entities, the same for every type:
|
|
6
|
+
|
|
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 sealed envelope
|
|
9
|
+
per secret field, readable by the owning edge only.
|
|
10
|
+
|
|
11
|
+
ConnectionMapping — one thing to read or write through a connection (an OPC node, an
|
|
12
|
+
MQTT topic, a Modbus register, an S7 block entry, a queue). ``address`` and
|
|
13
|
+
``options`` are shaped by the connection type (see ``connection_types``); the other
|
|
14
|
+
fields are common to every type.
|
|
15
|
+
|
|
16
|
+
EdgeDeviceKey — the public half of the key pair an edge generated for itself. Secrets
|
|
17
|
+
are sealed for the edge's active key; rotating adds a row and retires the previous.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConnectionDirection(str, Enum):
|
|
26
|
+
READ = "read"
|
|
27
|
+
WRITE = "write"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TransformKind(str, Enum):
|
|
31
|
+
# value -> value, a python expression on ``x`` (e.g. ``"x / 100"``)
|
|
32
|
+
EXPRESSION = "expression"
|
|
33
|
+
# payload -> list of messages, ``{"filepath": ..., "function_name": ...}``
|
|
34
|
+
FUNCTION = "function"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EdgeDeviceKeyStatus(str, Enum):
|
|
38
|
+
ACTIVE = "active"
|
|
39
|
+
RETIRED = "retired"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ts_out(value: Optional[datetime]) -> Optional[int]:
|
|
43
|
+
return int(value.timestamp() * 1000) if value is not None else None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _ts_in(value) -> Optional[datetime]:
|
|
47
|
+
if value is None:
|
|
48
|
+
return None
|
|
49
|
+
if isinstance(value, datetime):
|
|
50
|
+
return value
|
|
51
|
+
return datetime.fromtimestamp(value / 1000, tz=timezone.utc)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Connection:
|
|
55
|
+
"""
|
|
56
|
+
Connection entity stored in PostgreSQL ``"Connection"`` table. PK ``id`` (UUID).
|
|
57
|
+
|
|
58
|
+
- ``key``: unique user-facing identifier, also the consumer id on the edge.
|
|
59
|
+
- ``connection_type``: a name from ``connection_types.CONNECTION_TYPES``.
|
|
60
|
+
- ``edge_device_id``: the edge that owns it and alone can read its secrets.
|
|
61
|
+
- ``config``: clear parameters, validated against the type's config schema.
|
|
62
|
+
- ``secrets``: ``{field: envelope}`` once stored. On the write path it carries
|
|
63
|
+
plaintext instead — ``{field: "value"}`` to set, ``{field: None}`` to clear;
|
|
64
|
+
fields left out keep their current value.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self):
|
|
68
|
+
self.id: Optional[str] = None
|
|
69
|
+
self.key: Optional[str] = None
|
|
70
|
+
self.name: Optional[str] = None
|
|
71
|
+
self.connection_type: Optional[str] = None
|
|
72
|
+
self.edge_device_id: Optional[str] = None
|
|
73
|
+
self.config: dict = {}
|
|
74
|
+
self.secrets: dict = {}
|
|
75
|
+
self.created_at: Optional[datetime] = None
|
|
76
|
+
self.updated_at: Optional[datetime] = None
|
|
77
|
+
|
|
78
|
+
def secrets_set(self) -> list:
|
|
79
|
+
"""Names of the secret fields holding a value."""
|
|
80
|
+
return sorted(k for k, v in (self.secrets or {}).items() if v is not None)
|
|
81
|
+
|
|
82
|
+
def to_dict(self, include_secrets: bool = False, active_key_id: str = None) -> dict:
|
|
83
|
+
"""
|
|
84
|
+
:param include_secrets: emit the sealed envelopes (persistence and edge
|
|
85
|
+
rendering only). Never set it on a response to a user: the default
|
|
86
|
+
shows which fields are set, not what they hold.
|
|
87
|
+
:param active_key_id: fingerprint of the edge's active key. When given, the
|
|
88
|
+
public form also lists the secrets sealed for another key — the edge
|
|
89
|
+
can no longer open those and they must be entered again.
|
|
90
|
+
"""
|
|
91
|
+
obj = {}
|
|
92
|
+
if self.id is not None:
|
|
93
|
+
obj["id"] = str(self.id)
|
|
94
|
+
if self.key is not None:
|
|
95
|
+
obj["key"] = self.key
|
|
96
|
+
if self.name is not None:
|
|
97
|
+
obj["name"] = self.name
|
|
98
|
+
if self.connection_type is not None:
|
|
99
|
+
obj["connectionType"] = self.connection_type
|
|
100
|
+
if self.edge_device_id is not None:
|
|
101
|
+
obj["edgeDeviceId"] = self.edge_device_id
|
|
102
|
+
obj["config"] = dict(self.config or {})
|
|
103
|
+
if include_secrets:
|
|
104
|
+
obj["secrets"] = dict(self.secrets or {})
|
|
105
|
+
else:
|
|
106
|
+
obj["secretsSet"] = self.secrets_set()
|
|
107
|
+
if active_key_id is not None:
|
|
108
|
+
obj["secretsStale"] = sorted(
|
|
109
|
+
k for k, v in (self.secrets or {}).items()
|
|
110
|
+
if isinstance(v, dict) and v.get("kid") != active_key_id
|
|
111
|
+
)
|
|
112
|
+
if self.created_at is not None:
|
|
113
|
+
obj["createdAt"] = _ts_out(self.created_at)
|
|
114
|
+
if self.updated_at is not None:
|
|
115
|
+
obj["updatedAt"] = _ts_out(self.updated_at)
|
|
116
|
+
return obj
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_dict(cls, data: dict) -> "Connection":
|
|
120
|
+
obj = cls()
|
|
121
|
+
obj.id = data.get("id")
|
|
122
|
+
obj.key = data.get("key")
|
|
123
|
+
obj.name = data.get("name")
|
|
124
|
+
obj.connection_type = data.get("connectionType") or data.get("connection_type")
|
|
125
|
+
obj.edge_device_id = data.get("edgeDeviceId") or data.get("edge_device_id")
|
|
126
|
+
obj.config = data.get("config") or {}
|
|
127
|
+
obj.secrets = data.get("secrets") or {}
|
|
128
|
+
obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
|
|
129
|
+
obj.updated_at = _ts_in(data.get("updatedAt") or data.get("updated_at"))
|
|
130
|
+
return obj
|
|
131
|
+
|
|
132
|
+
def api_id(self) -> str:
|
|
133
|
+
return str(self.id) if self.id else None
|
|
134
|
+
|
|
135
|
+
def api_key(self) -> str:
|
|
136
|
+
return self.key
|
|
137
|
+
|
|
138
|
+
def set_id(self, value):
|
|
139
|
+
self.id = str(value)
|
|
140
|
+
|
|
141
|
+
def __repr__(self) -> str:
|
|
142
|
+
# secrets deliberately left out, even sealed
|
|
143
|
+
return (f"Connection(id={self.id!r}, key={self.key!r}, "
|
|
144
|
+
f"type={self.connection_type!r}, edge={self.edge_device_id!r})")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ConnectionMapping:
|
|
148
|
+
"""
|
|
149
|
+
ConnectionMapping entity stored in PostgreSQL ``"ConnectionMapping"`` table.
|
|
150
|
+
PK ``id`` (UUID). FK ``connection_id`` → ``Connection.id``, cascade on delete.
|
|
151
|
+
|
|
152
|
+
- ``direction``: READ (source → platform) or WRITE (platform → target).
|
|
153
|
+
- ``address``: where, shaped by the type (``{"node_id": ...}``, ``{"topic": ...}``).
|
|
154
|
+
- ``hardware_id``: the datapoint. Optional where the source names it (OPC browse
|
|
155
|
+
with ``children``, a topic function fanning out to many datapoints).
|
|
156
|
+
- ``poll_interval_ms``: polling cadence for polled types; ignored when pushed.
|
|
157
|
+
- ``transform``: ``{"kind": "expression", "expression": "x / 10"}`` or
|
|
158
|
+
``{"kind": "function", "filepath": ..., "function_name": ...}``.
|
|
159
|
+
- ``options``: type-specific tuning (``qos``, ``batch_size``, ``pack``...).
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
def __init__(self):
|
|
163
|
+
self.id: Optional[str] = None
|
|
164
|
+
self.connection_id: Optional[str] = None
|
|
165
|
+
self.direction: ConnectionDirection = ConnectionDirection.READ
|
|
166
|
+
self.address: dict = {}
|
|
167
|
+
self.hardware_id: Optional[str] = None
|
|
168
|
+
self.poll_interval_ms: Optional[int] = None
|
|
169
|
+
self.transform: Optional[dict] = None
|
|
170
|
+
self.options: dict = {}
|
|
171
|
+
self.created_at: Optional[datetime] = None
|
|
172
|
+
self.updated_at: Optional[datetime] = None
|
|
173
|
+
|
|
174
|
+
def to_dict(self) -> dict:
|
|
175
|
+
obj = {}
|
|
176
|
+
if self.id is not None:
|
|
177
|
+
obj["id"] = str(self.id)
|
|
178
|
+
if self.connection_id is not None:
|
|
179
|
+
obj["connectionId"] = str(self.connection_id)
|
|
180
|
+
obj["direction"] = (self.direction.value
|
|
181
|
+
if isinstance(self.direction, ConnectionDirection)
|
|
182
|
+
else self.direction)
|
|
183
|
+
obj["address"] = dict(self.address or {})
|
|
184
|
+
if self.hardware_id is not None:
|
|
185
|
+
obj["hardwareId"] = self.hardware_id
|
|
186
|
+
if self.poll_interval_ms is not None:
|
|
187
|
+
obj["pollIntervalMs"] = self.poll_interval_ms
|
|
188
|
+
if self.transform is not None:
|
|
189
|
+
obj["transform"] = dict(self.transform)
|
|
190
|
+
obj["options"] = dict(self.options or {})
|
|
191
|
+
if self.created_at is not None:
|
|
192
|
+
obj["createdAt"] = _ts_out(self.created_at)
|
|
193
|
+
if self.updated_at is not None:
|
|
194
|
+
obj["updatedAt"] = _ts_out(self.updated_at)
|
|
195
|
+
return obj
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def from_dict(cls, data: dict) -> "ConnectionMapping":
|
|
199
|
+
obj = cls()
|
|
200
|
+
obj.id = data.get("id")
|
|
201
|
+
obj.connection_id = data.get("connectionId") or data.get("connection_id")
|
|
202
|
+
raw_direction = data.get("direction")
|
|
203
|
+
if raw_direction is not None:
|
|
204
|
+
obj.direction = ConnectionDirection(str(raw_direction).lower())
|
|
205
|
+
obj.address = data.get("address") or {}
|
|
206
|
+
obj.hardware_id = data.get("hardwareId") or data.get("hardware_id")
|
|
207
|
+
poll = data.get("pollIntervalMs", data.get("poll_interval_ms"))
|
|
208
|
+
obj.poll_interval_ms = int(poll) if poll is not None else None
|
|
209
|
+
obj.transform = data.get("transform")
|
|
210
|
+
obj.options = data.get("options") or {}
|
|
211
|
+
obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
|
|
212
|
+
obj.updated_at = _ts_in(data.get("updatedAt") or data.get("updated_at"))
|
|
213
|
+
return obj
|
|
214
|
+
|
|
215
|
+
def api_id(self) -> str:
|
|
216
|
+
return str(self.id) if self.id else None
|
|
217
|
+
|
|
218
|
+
def api_key(self) -> str:
|
|
219
|
+
return self.api_id()
|
|
220
|
+
|
|
221
|
+
def set_id(self, value):
|
|
222
|
+
self.id = str(value)
|
|
223
|
+
|
|
224
|
+
def __repr__(self) -> str:
|
|
225
|
+
return (f"ConnectionMapping(id={self.id!r}, connection={self.connection_id!r}, "
|
|
226
|
+
f"direction={self.direction!r}, hardware_id={self.hardware_id!r})")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class EdgeDeviceKey:
|
|
230
|
+
"""
|
|
231
|
+
EdgeDeviceKey entity stored in PostgreSQL ``"EdgeDeviceKey"`` table. PK ``id``.
|
|
232
|
+
|
|
233
|
+
The edge generates the pair and registers only the public half; the private half
|
|
234
|
+
never leaves the device, and the platform holds no key that could open a secret.
|
|
235
|
+
At most one ACTIVE row per ``edge_device_id``.
|
|
236
|
+
|
|
237
|
+
- ``public_key``: PEM (SubjectPublicKeyInfo).
|
|
238
|
+
- ``algorithm``: e.g. ``"x25519"``.
|
|
239
|
+
- ``fingerprint``: sha256 of the raw public key, hex — the ``kid`` envelopes carry.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
def __init__(self):
|
|
243
|
+
self.id: Optional[str] = None
|
|
244
|
+
self.edge_device_id: Optional[str] = None
|
|
245
|
+
self.public_key: Optional[str] = None
|
|
246
|
+
self.algorithm: Optional[str] = None
|
|
247
|
+
self.fingerprint: Optional[str] = None
|
|
248
|
+
self.status: EdgeDeviceKeyStatus = EdgeDeviceKeyStatus.ACTIVE
|
|
249
|
+
self.created_at: Optional[datetime] = None
|
|
250
|
+
self.retired_at: Optional[datetime] = None
|
|
251
|
+
|
|
252
|
+
def to_dict(self) -> dict:
|
|
253
|
+
obj = {}
|
|
254
|
+
if self.id is not None:
|
|
255
|
+
obj["id"] = str(self.id)
|
|
256
|
+
if self.edge_device_id is not None:
|
|
257
|
+
obj["edgeDeviceId"] = self.edge_device_id
|
|
258
|
+
if self.public_key is not None:
|
|
259
|
+
obj["publicKey"] = self.public_key
|
|
260
|
+
if self.algorithm is not None:
|
|
261
|
+
obj["algorithm"] = self.algorithm
|
|
262
|
+
if self.fingerprint is not None:
|
|
263
|
+
obj["fingerprint"] = self.fingerprint
|
|
264
|
+
obj["status"] = (self.status.value
|
|
265
|
+
if isinstance(self.status, EdgeDeviceKeyStatus)
|
|
266
|
+
else self.status)
|
|
267
|
+
if self.created_at is not None:
|
|
268
|
+
obj["createdAt"] = _ts_out(self.created_at)
|
|
269
|
+
if self.retired_at is not None:
|
|
270
|
+
obj["retiredAt"] = _ts_out(self.retired_at)
|
|
271
|
+
return obj
|
|
272
|
+
|
|
273
|
+
@classmethod
|
|
274
|
+
def from_dict(cls, data: dict) -> "EdgeDeviceKey":
|
|
275
|
+
obj = cls()
|
|
276
|
+
obj.id = data.get("id")
|
|
277
|
+
obj.edge_device_id = data.get("edgeDeviceId") or data.get("edge_device_id")
|
|
278
|
+
obj.public_key = data.get("publicKey") or data.get("public_key")
|
|
279
|
+
obj.algorithm = data.get("algorithm")
|
|
280
|
+
obj.fingerprint = data.get("fingerprint")
|
|
281
|
+
raw_status = data.get("status")
|
|
282
|
+
if raw_status is not None:
|
|
283
|
+
obj.status = EdgeDeviceKeyStatus(str(raw_status).lower())
|
|
284
|
+
obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
|
|
285
|
+
obj.retired_at = _ts_in(data.get("retiredAt") or data.get("retired_at"))
|
|
286
|
+
return obj
|
|
287
|
+
|
|
288
|
+
def api_id(self) -> str:
|
|
289
|
+
return str(self.id) if self.id else None
|
|
290
|
+
|
|
291
|
+
def api_key(self) -> str:
|
|
292
|
+
return self.api_id()
|
|
293
|
+
|
|
294
|
+
def set_id(self, value):
|
|
295
|
+
self.id = str(value)
|
|
296
|
+
|
|
297
|
+
def __repr__(self) -> str:
|
|
298
|
+
return (f"EdgeDeviceKey(edge={self.edge_device_id!r}, "
|
|
299
|
+
f"fingerprint={self.fingerprint!r}, status={self.status!r})")
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connection type registry (DEV-6013).
|
|
3
|
+
|
|
4
|
+
One definition per connector type, used for three things:
|
|
5
|
+
- the front-end lists the types and builds each form from its JSON Schemas;
|
|
6
|
+
- the platform validates every connection and mapping against them on write — the
|
|
7
|
+
single place a secret-bearing key is refused outside ``secrets``;
|
|
8
|
+
- the platform renders the edge configuration from the entities.
|
|
9
|
+
|
|
10
|
+
Schemas are plain JSON Schema (draft 2020-12) dicts, so serving them needs nothing.
|
|
11
|
+
Secret fields are ``writeOnly``: accepted on write, never returned.
|
|
12
|
+
|
|
13
|
+
Key names are the entity's, not the legacy ``consumers.json`` spelling, which drifted
|
|
14
|
+
per type (``opc_session_timeout_ms`` vs ``opc_session_timeout``, snap7 ``port`` vs
|
|
15
|
+
``tcp_port``, RabbitMQ ``RQ_USER`` vs ``user``). Rendering maps them back.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from .connection import ConnectionDirection, TransformKind
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_READ = ConnectionDirection.READ.value
|
|
24
|
+
_WRITE = ConnectionDirection.WRITE.value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _obj(properties: dict, required: list = None) -> dict:
|
|
28
|
+
schema = {"type": "object", "properties": properties, "additionalProperties": False}
|
|
29
|
+
if required:
|
|
30
|
+
schema["required"] = required
|
|
31
|
+
return schema
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _secret(title: str, pem: bool = False) -> dict:
|
|
35
|
+
field = {"type": "string", "title": title, "writeOnly": True}
|
|
36
|
+
if pem:
|
|
37
|
+
field["contentMediaType"] = "application/x-pem-file"
|
|
38
|
+
return field
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _pem(title: str) -> dict:
|
|
42
|
+
return {"type": "string", "title": title, "contentMediaType": "application/x-pem-file"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_EMPTY = _obj({})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ConnectionType:
|
|
49
|
+
"""
|
|
50
|
+
One supported connector type.
|
|
51
|
+
|
|
52
|
+
: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
|
+
:param config: schema for ``Connection.config``.
|
|
59
|
+
:param secrets: schema for ``Connection.secrets`` (plaintext write form).
|
|
60
|
+
:param mapping: per direction, ``{"address": schema, "options": schema}``.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, name: str, label: str, description: str,
|
|
64
|
+
directions: list, transforms: list, polled: bool,
|
|
65
|
+
hardware_id_required: dict,
|
|
66
|
+
config: dict, secrets: dict, mapping: dict,
|
|
67
|
+
supports_browse: bool = False):
|
|
68
|
+
self.name = name
|
|
69
|
+
self.label = label
|
|
70
|
+
self.description = description
|
|
71
|
+
self.directions = directions
|
|
72
|
+
self.transforms = transforms
|
|
73
|
+
self.polled = polled
|
|
74
|
+
self.hardware_id_required = hardware_id_required
|
|
75
|
+
self.config = config
|
|
76
|
+
self.secrets = secrets
|
|
77
|
+
self.mapping = mapping
|
|
78
|
+
self.supports_browse = supports_browse
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def secret_fields(self) -> list:
|
|
82
|
+
return list(self.secrets.get("properties", {}).keys())
|
|
83
|
+
|
|
84
|
+
def describe(self) -> dict:
|
|
85
|
+
"""Summary for the type list: enough to offer the type, not to build a form."""
|
|
86
|
+
return {
|
|
87
|
+
"type": self.name,
|
|
88
|
+
"label": self.label,
|
|
89
|
+
"description": self.description,
|
|
90
|
+
"directions": list(self.directions),
|
|
91
|
+
"transforms": list(self.transforms),
|
|
92
|
+
"polled": self.polled,
|
|
93
|
+
"supportsBrowse": self.supports_browse,
|
|
94
|
+
"secretFields": self.secret_fields,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def schema(self) -> dict:
|
|
98
|
+
"""Everything a form needs: the summary plus every JSON Schema."""
|
|
99
|
+
out = self.describe()
|
|
100
|
+
out["hardwareIdRequired"] = dict(self.hardware_id_required)
|
|
101
|
+
out["config"] = self.config
|
|
102
|
+
out["secrets"] = self.secrets
|
|
103
|
+
out["mapping"] = {d: dict(s) for d, s in self.mapping.items()}
|
|
104
|
+
return out
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ── OPC-UA ─────────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
_OPC_SECURITY_POLICIES = ["None", "Basic128Rsa15", "Basic256", "Basic256Sha256",
|
|
110
|
+
"Aes128Sha256RsaOaep", "Aes256Sha256RsaPss"]
|
|
111
|
+
|
|
112
|
+
OPC_UA = ConnectionType(
|
|
113
|
+
name="opc-ua",
|
|
114
|
+
label="OPC-UA",
|
|
115
|
+
description="OPC-UA server — read by polling or subscription, write to nodes.",
|
|
116
|
+
directions=[_READ, _WRITE],
|
|
117
|
+
transforms=[],
|
|
118
|
+
polled=True,
|
|
119
|
+
supports_browse=True,
|
|
120
|
+
hardware_id_required={_READ: False, _WRITE: True},
|
|
121
|
+
config=_obj({
|
|
122
|
+
"server_url": {"type": "string", "title": "Server URL", "pattern": "^opc\\.tcp://"},
|
|
123
|
+
"username": {"type": "string", "title": "Username"},
|
|
124
|
+
"application_uri": {"type": "string", "title": "Application URI"},
|
|
125
|
+
"security_policy": {"type": "string", "title": "Security policy",
|
|
126
|
+
"enum": _OPC_SECURITY_POLICIES, "default": "None"},
|
|
127
|
+
"security_mode": {"type": "string", "title": "Security mode",
|
|
128
|
+
"enum": ["None", "Sign", "SignAndEncrypt"], "default": "None"},
|
|
129
|
+
"client_certificate": _pem("Client certificate"),
|
|
130
|
+
"server_certificate": _pem("Server certificate"),
|
|
131
|
+
"session_timeout_ms": {"type": "integer", "title": "Session timeout (ms)",
|
|
132
|
+
"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
|
+
}, required=["server_url"]),
|
|
141
|
+
secrets=_obj({
|
|
142
|
+
"password": _secret("Password"),
|
|
143
|
+
"client_private_key": _secret("Client private key", pem=True),
|
|
144
|
+
}),
|
|
145
|
+
mapping={
|
|
146
|
+
_READ: {
|
|
147
|
+
"address": _obj({
|
|
148
|
+
"node_id": {"type": "string", "title": "Node id", "examples": ["ns=2;s=Temperature"]},
|
|
149
|
+
"children": {"type": "boolean", "title": "Browse children", "default": False},
|
|
150
|
+
}, required=["node_id"]),
|
|
151
|
+
"options": _obj({
|
|
152
|
+
"batch_size": {"type": "integer", "minimum": 1},
|
|
153
|
+
"max_concurrent_batches": {"type": "integer", "minimum": 1},
|
|
154
|
+
"publishing_interval": {"type": "integer", "minimum": 0, "default": 1000},
|
|
155
|
+
"sampling_interval": {"type": "integer", "minimum": 0, "default": 0},
|
|
156
|
+
"queue_size": {"type": "integer", "minimum": 1, "default": 1},
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
_WRITE: {
|
|
160
|
+
"address": _obj({
|
|
161
|
+
"node_id": {"type": "string", "title": "Node id"},
|
|
162
|
+
}, required=["node_id"]),
|
|
163
|
+
"options": _EMPTY,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# ── MQTT ───────────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
MQTT = ConnectionType(
|
|
171
|
+
name="mqtt",
|
|
172
|
+
label="MQTT",
|
|
173
|
+
description="MQTT broker — subscribe to topics, optional function to parse payloads.",
|
|
174
|
+
directions=[_READ],
|
|
175
|
+
transforms=[TransformKind.FUNCTION.value],
|
|
176
|
+
polled=False,
|
|
177
|
+
hardware_id_required={_READ: False},
|
|
178
|
+
config=_obj({
|
|
179
|
+
"host": {"type": "string", "title": "Broker host"},
|
|
180
|
+
"port": {"type": "integer", "title": "Port", "default": 1883},
|
|
181
|
+
"tls": {"type": "boolean", "title": "TLS", "default": False},
|
|
182
|
+
"username": {"type": "string", "title": "Username"},
|
|
183
|
+
"client_id": {"type": "string", "title": "Client id"},
|
|
184
|
+
"keepalive": {"type": "integer", "title": "Keepalive (s)", "minimum": 1, "default": 60},
|
|
185
|
+
"payload_format": {"type": "string", "enum": ["json", "raw"], "default": "json"},
|
|
186
|
+
"reconnect_min": {"type": "number", "minimum": 0, "default": 1.0},
|
|
187
|
+
"reconnect_max": {"type": "number", "minimum": 0, "default": 120.0},
|
|
188
|
+
"connect_timeout": {"type": "number", "minimum": 0, "default": 30.0},
|
|
189
|
+
"watchdog_timeout": {"type": "number", "minimum": 0, "default": 0},
|
|
190
|
+
}, required=["host"]),
|
|
191
|
+
secrets=_obj({
|
|
192
|
+
"password": _secret("Password"),
|
|
193
|
+
}),
|
|
194
|
+
mapping={
|
|
195
|
+
_READ: {
|
|
196
|
+
"address": _obj({
|
|
197
|
+
"topic": {"type": "string", "title": "Topic", "examples": ["plant/line1/#"]},
|
|
198
|
+
}, required=["topic"]),
|
|
199
|
+
"options": _obj({
|
|
200
|
+
"qos": {"type": "integer", "enum": [0, 1, 2], "default": 1},
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# ── Modbus TCP ─────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
MODBUS = ConnectionType(
|
|
209
|
+
name="modbus",
|
|
210
|
+
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},
|
|
216
|
+
config=_obj({
|
|
217
|
+
"ip_address": {"type": "string", "title": "IP address"},
|
|
218
|
+
"port": {"type": "integer", "title": "Port", "default": 502},
|
|
219
|
+
"unit_id": {"type": "integer", "title": "Unit id", "minimum": 0, "default": 1},
|
|
220
|
+
}, required=["ip_address"]),
|
|
221
|
+
secrets=_EMPTY,
|
|
222
|
+
mapping={
|
|
223
|
+
_READ: {
|
|
224
|
+
"address": _obj({
|
|
225
|
+
"address": {"type": "integer", "title": "Register address", "minimum": 0},
|
|
226
|
+
"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({
|
|
231
|
+
"pack": {"type": "string", "title": "struct pack format", "examples": [">HH"]},
|
|
232
|
+
"unpack": {"type": "string", "title": "struct unpack format", "examples": [">f"]},
|
|
233
|
+
}, required=["pack", "unpack"]),
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# ── Siemens S7 (snap7) ─────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
SNAP7 = ConnectionType(
|
|
241
|
+
name="snap7",
|
|
242
|
+
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},
|
|
248
|
+
config=_obj({
|
|
249
|
+
"address": {"type": "string", "title": "PLC address"},
|
|
250
|
+
"rack": {"type": "integer", "minimum": 0, "default": 0},
|
|
251
|
+
"slot": {"type": "integer", "minimum": 0, "default": 1},
|
|
252
|
+
"tcp_port": {"type": "integer", "title": "Port", "default": 102},
|
|
253
|
+
}, required=["address"]),
|
|
254
|
+
secrets=_EMPTY,
|
|
255
|
+
mapping={
|
|
256
|
+
_READ: {
|
|
257
|
+
"address": _obj({
|
|
258
|
+
"db_number": {"type": "integer", "minimum": 0},
|
|
259
|
+
"start_offset": {"type": "integer", "minimum": 0},
|
|
260
|
+
"size": {"type": "integer", "minimum": 1},
|
|
261
|
+
"index": {"type": "integer", "title": "Byte index in the block", "minimum": 0},
|
|
262
|
+
"type": {"type": "string", "enum": ["dword", "word", "bool", "real", "int"]},
|
|
263
|
+
"bit_position": {"type": "integer", "minimum": 0, "maximum": 7},
|
|
264
|
+
}, required=["db_number", "start_offset", "size", "index", "type"]),
|
|
265
|
+
"options": _EMPTY,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# ── RabbitMQ (external queue) ──────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
RABBITMQ = ConnectionType(
|
|
273
|
+
name="rabbitmq",
|
|
274
|
+
label="RabbitMQ",
|
|
275
|
+
description="External RabbitMQ broker — consume a queue, function to parse payloads.",
|
|
276
|
+
directions=[_READ],
|
|
277
|
+
transforms=[TransformKind.FUNCTION.value],
|
|
278
|
+
polled=False,
|
|
279
|
+
hardware_id_required={_READ: False},
|
|
280
|
+
config=_obj({
|
|
281
|
+
"host": {"type": "string", "title": "Host"},
|
|
282
|
+
"port": {"type": "integer", "title": "Port", "default": 5671},
|
|
283
|
+
"vhost": {"type": "string", "title": "Virtual host", "default": "/"},
|
|
284
|
+
"tls": {"type": "boolean", "title": "TLS", "default": True},
|
|
285
|
+
"username": {"type": "string", "title": "Username"},
|
|
286
|
+
}, required=["host", "username"]),
|
|
287
|
+
secrets=_obj({
|
|
288
|
+
"password": _secret("Password"),
|
|
289
|
+
}),
|
|
290
|
+
mapping={
|
|
291
|
+
_READ: {
|
|
292
|
+
"address": _obj({
|
|
293
|
+
"queue": {"type": "string", "title": "Queue", "default": "messages"},
|
|
294
|
+
}, required=["queue"]),
|
|
295
|
+
"options": _obj({
|
|
296
|
+
"prefetch": {"type": "integer", "minimum": 1},
|
|
297
|
+
"error_queue": {"type": "string"},
|
|
298
|
+
}),
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
CONNECTION_TYPES = {t.name: t for t in (OPC_UA, MQTT, MODBUS, SNAP7, RABBITMQ)}
|
|
305
|
+
|
|
306
|
+
# Transform payloads, whatever the type (the type decides which kinds it accepts).
|
|
307
|
+
TRANSFORM_SCHEMAS = {
|
|
308
|
+
TransformKind.EXPRESSION.value: _obj({
|
|
309
|
+
"kind": {"const": TransformKind.EXPRESSION.value},
|
|
310
|
+
"expression": {"type": "string", "title": "Expression on x", "examples": ["x / 100"]},
|
|
311
|
+
}, required=["kind", "expression"]),
|
|
312
|
+
TransformKind.FUNCTION.value: _obj({
|
|
313
|
+
"kind": {"const": TransformKind.FUNCTION.value},
|
|
314
|
+
"filepath": {"type": "string", "title": "Function file"},
|
|
315
|
+
"function_name": {"type": "string", "title": "Function name"},
|
|
316
|
+
}, required=["kind", "filepath", "function_name"]),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def get_connection_type(name: str) -> Optional[ConnectionType]:
|
|
321
|
+
return CONNECTION_TYPES.get((name or "").lower())
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def list_connection_types() -> list:
|
|
325
|
+
return [t.describe() for t in CONNECTION_TYPES.values()]
|
wizata_dsapi/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "2.2.0.
|
|
1
|
+
__version__ = "2.2.0.dev2"
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
wizata_dsapi/__init__.py,sha256=
|
|
1
|
+
wizata_dsapi/__init__.py,sha256=8zazLs_NYKRhveJt3vhy1SxwXHvBv5fEl6yfX-UilrI,3374
|
|
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=C1VcA1oFqRLtiJN8jHkwlJobH5Lh2OCohEN_2mLP41c,12200
|
|
8
|
+
wizata_dsapi/connection_types.py,sha256=nEQRp5-hGKGmOfZfKqocM-THuw3nQYJIqrX2BMK-mUs,13986
|
|
7
9
|
wizata_dsapi/context.py,sha256=1t0wCzRnBEzch7fx7jG4Oj3Ezrjv9getJtCrqEpn-pc,13188
|
|
8
10
|
wizata_dsapi/dashboard.py,sha256=NY4m-1EpMRoKDHem0EwsRF0QQzal3bvrm9gQv3BNizk,3841
|
|
9
11
|
wizata_dsapi/dataframe_toolkit.py,sha256=7D8JrhsFSGXeO3mRAd2e2cLmdjbXRjk04IuaMG8F974,9078
|
|
@@ -42,7 +44,7 @@ wizata_dsapi/trigger.py,sha256=waOX2AhrLvFzNFRQ535j6iemgg_QJP2QsWGL2ipljFg,8848
|
|
|
42
44
|
wizata_dsapi/twin.py,sha256=8AnrxSaL7dMrILXJ3-GGmmUz078j3qqwzDTe6i86xqA,12672
|
|
43
45
|
wizata_dsapi/twinregistration.py,sha256=iXt55AVGAQNl0jmxa3ZVynPqlSsIunFf4sLfp-MWTw0,13244
|
|
44
46
|
wizata_dsapi/user.py,sha256=-hchKAMZULu9P00RUHDqu_5AT4NI5BYOJd8XCBrt3eQ,1950
|
|
45
|
-
wizata_dsapi/version.py,sha256=
|
|
47
|
+
wizata_dsapi/version.py,sha256=bKxMxf4FLo3j7fCZ_ayiGhLIAkPquwpEeY8g9TOU0P8,27
|
|
46
48
|
wizata_dsapi/wizard_function.py,sha256=RbM7W7Gf-6Rhp_1dU9DBYkHaciknGAGvuAndhAS_vyo,942
|
|
47
49
|
wizata_dsapi/wizard_request.py,sha256=v6BaqKLKvTWmUSo0_gda9FabAQz5x_-GOH1Av50GzFo,3762
|
|
48
50
|
wizata_dsapi/wizata_dsapi_client.py,sha256=DX8WXKT5gi_nfHHX2-jz4dvUe-Bo_HszQmr94R3JvV0,112418
|
|
@@ -54,8 +56,8 @@ wizata_dsapi/plots/common.py,sha256=7F4WMBaddubnCpdSafgaI9U9Sz0Sngg0VREP_vnIMno,
|
|
|
54
56
|
wizata_dsapi/plots/theme.py,sha256=D4sSgEwabXVxF3z1GV8DivXJbbY61KgxXAWfjUbNDAc,3594
|
|
55
57
|
wizata_dsapi/scripts/__init__.py,sha256=9fRdTMuuXcqMkIsUPf5dwFsTWwBzKteQdAMVkyy2acQ,212
|
|
56
58
|
wizata_dsapi/scripts/common.py,sha256=T2cP8aGPg8OgIvBWuQBNonVtph_dHlNFubByuYrLb0o,15675
|
|
57
|
-
wizata_dsapi-2.2.0.
|
|
58
|
-
wizata_dsapi-2.2.0.
|
|
59
|
-
wizata_dsapi-2.2.0.
|
|
60
|
-
wizata_dsapi-2.2.0.
|
|
61
|
-
wizata_dsapi-2.2.0.
|
|
59
|
+
wizata_dsapi-2.2.0.dev2.dist-info/licenses/LICENSE.txt,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
60
|
+
wizata_dsapi-2.2.0.dev2.dist-info/METADATA,sha256=dVvL2OBGr42dKhKzMnfULhMExHhmUkJkAW1X8oR7_uU,14037
|
|
61
|
+
wizata_dsapi-2.2.0.dev2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
62
|
+
wizata_dsapi-2.2.0.dev2.dist-info/top_level.txt,sha256=-OeTJbEnh5DuWyTOHtvw0Dw3LRg3G27TNS6W4ZtfwPs,13
|
|
63
|
+
wizata_dsapi-2.2.0.dev2.dist-info/RECORD,,
|
|
File without changes
|
{wizata_dsapi-2.2.0.dev1.dist-info → wizata_dsapi-2.2.0.dev2.dist-info}/licenses/LICENSE.txt
RENAMED
|
File without changes
|
|
File without changes
|