@uns-kit/cli 0.0.32 → 0.0.35
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.
- package/package.json +2 -2
- package/templates/api/src/examples/api-example.ts +1 -0
- package/templates/cron/src/examples/cron-example.ts +1 -0
- package/templates/default/config.json +3 -1
- package/templates/default/src/examples/table-example.ts +2 -1
- package/templates/python/app/README.md +8 -0
- package/templates/python/app/__init__.py +0 -0
- package/templates/python/examples/README.md +90 -40
- package/templates/python/examples/__init__.py +0 -0
- package/templates/python/examples/api_handler.py +26 -99
- package/templates/python/examples/data_publish.py +11 -0
- package/templates/python/examples/data_subscribe.py +6 -122
- package/templates/python/examples/data_transformer.py +13 -143
- package/templates/python/examples/table_transformer.py +14 -161
- package/templates/python/gateway/__init__.py +0 -0
- package/templates/python/gateway/cli.py +75 -0
- package/templates/python/gateway/client.py +155 -0
- package/templates/python/gateway/manager.py +97 -0
- package/templates/python/main.py +1 -0
- package/templates/python/pyproject.toml +5 -0
- package/templates/python/scripts/setup.sh +37 -14
- package/templates/temporal/src/examples/temporal-example.ts +1 -0
- package/templates/python/examples/api_register_and_serve.py +0 -159
- package/templates/python/examples/data_publish_once.py +0 -140
- package/templates/python/examples/data_publisher_loop.py +0 -142
- package/templates/python/gateway_client.py +0 -242
- package/templates/python/local/README.md +0 -8
- package/templates/python/local/__init__.py +0 -2
- package/templates/python/requirements.txt +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uns-kit/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.35",
|
|
4
4
|
"description": "Command line scaffolding tool for UNS applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"azure-devops-node-api": "^15.1.1",
|
|
29
|
-
"@uns-kit/core": "0.0.
|
|
29
|
+
"@uns-kit/core": "0.0.35"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { UnsProxyProcess, ConfigFile, logger } from "@uns-kit/core";
|
|
5
5
|
import type { IUnsMessage, UnsEvents } from "@uns-kit/core";
|
|
6
|
+
import "@uns-kit/cron";
|
|
6
7
|
import { type UnsProxyProcessWithCron } from "@uns-kit/cron";
|
|
7
8
|
import { UnsTags } from "../uns/uns-tags.js";
|
|
8
9
|
import { UnsTopics } from "../uns/uns-topics.js";
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
"instanceMode": "wait",
|
|
7
7
|
"handover": true,
|
|
8
8
|
"jwksWellKnownUrl": "http://localhost:3200/api/.well-known/jwks.json",
|
|
9
|
-
"kidWellKnownUrl": "http://localhost:3200/api/.well-known/kid"
|
|
9
|
+
"kidWellKnownUrl": "http://localhost:3200/api/.well-known/kid",
|
|
10
|
+
"email": "user@example-org.com",
|
|
11
|
+
"password": "secret"
|
|
10
12
|
},
|
|
11
13
|
"infra": {
|
|
12
14
|
"host": "localhost:1883"
|
|
@@ -51,7 +51,8 @@ mqttInput.event.on("input", async (event) => {
|
|
|
51
51
|
// delete(jsonObject.Timestamp);
|
|
52
52
|
|
|
53
53
|
const time = UnsPacket.formatToISO8601(new Date(timestamp));
|
|
54
|
-
const message: IUnsMessage = { table: {dataGroup:"demo_table", values:jsonObject, columnTypes, time}};
|
|
54
|
+
// const message: IUnsMessage = { table: {dataGroup:"demo_table", values:jsonObject, columnTypes, time}};
|
|
55
|
+
const message: IUnsMessage = { table: {dataGroup:"demo_table", values:jsonObject, time}};
|
|
55
56
|
const topic: UnsTopics = "example/factory-a/line-1/";
|
|
56
57
|
const tags: UnsTags[] = [];
|
|
57
58
|
const packet = await UnsPacket.unsPacketFromUnsMessage(message);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
This folder is reserved for your project-specific Python code.
|
|
2
|
+
|
|
3
|
+
The update tools will NOT overwrite `python/app/**` (and also preserve legacy `python/rtt/**`, `python/venv`, `python/.venv`, and `python/__pycache__`).
|
|
4
|
+
|
|
5
|
+
Suggested layout:
|
|
6
|
+
- `python/app/` your packages, modules, or CLI scripts
|
|
7
|
+
- Reference the gateway client in `python/gateway_client.py` if you interact with the UNS gateway from Python
|
|
8
|
+
|
|
File without changes
|
|
@@ -29,46 +29,96 @@ Readiness: Examples call the gateway’s `Ready()` RPC to wait for the requested
|
|
|
29
29
|
- `--auto`: Auto-start the gateway if not running
|
|
30
30
|
|
|
31
31
|
## Scripts
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
32
|
+
### `data_transformer.py`
|
|
33
|
+
|
|
34
|
+
Transforms incoming sensor data and republishes it to a target topic.
|
|
35
|
+
|
|
36
|
+
- **Description:**
|
|
37
|
+
Subscribes to `sensors/temperature`, applies a transformation (e.g., scales the value), and republishes it as processed data.
|
|
38
|
+
|
|
39
|
+
- **Example:**
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python python/examples/data_transformer.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Transforms data from `sensors/temperature` and republishes to `sensors/temperature/room1` it with a modified value.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### `data_subscribe.py`
|
|
51
|
+
|
|
52
|
+
Subscribes to topics via the gateway and prints received messages.
|
|
53
|
+
|
|
54
|
+
- **Description:**
|
|
55
|
+
Useful for monitoring live data updates.
|
|
56
|
+
|
|
57
|
+
- **Example:**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python python/examples/data_subscribe.py
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Prints all messages received from `sensors/temperature`.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### `data_publish.py`
|
|
69
|
+
|
|
70
|
+
Publishes a single data point to a UNS topic.
|
|
71
|
+
|
|
72
|
+
- **Description:**
|
|
73
|
+
Sends a single temperature measurement (e.g., 22.5 °C) to the gateway, waits 10 seconds, then exits.
|
|
74
|
+
|
|
75
|
+
- **Example:**
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python python/examples/data_publish.py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Publishes one temperature value to `sensors/temperature/room1`.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### `table_transformer.py`
|
|
87
|
+
|
|
88
|
+
Publishes a structured table-like JSON object as UNS table data.
|
|
89
|
+
|
|
90
|
+
- **Description:**
|
|
91
|
+
Sends summarized or grouped sensor data (e.g., temperature, humidity, status) to a topic.
|
|
92
|
+
|
|
93
|
+
- **Example:**
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python python/examples/dtable_transformer.py
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Publishes a table with room statistics (temperature, humidity, etc.) to `sensors/summary/room1`.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### `api_handler.py`
|
|
105
|
+
|
|
106
|
+
Registers and handles an example API endpoint for querying data.
|
|
107
|
+
|
|
108
|
+
- **Description:**
|
|
109
|
+
Demonstrates registering an API endpoint (`example/summary-1`) with query parameters and returning JSON responses.
|
|
110
|
+
|
|
111
|
+
- **Example:**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python python/examples/api_handler.py
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Then test it with:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
http://<gateway-host>:<gateway-port>/api/example/summary-1?filter=test&limit=10
|
|
121
|
+
```
|
|
72
122
|
|
|
73
123
|
## Auth
|
|
74
124
|
|
|
File without changes
|
|
@@ -1,101 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
1
|
+
from gateway.client import Client
|
|
2
|
+
from uns_gateway_pb2 import ApiQueryParam, ApiEventResponse
|
|
5
3
|
import json
|
|
6
|
-
import os
|
|
7
|
-
import queue
|
|
8
|
-
from typing import Dict, Iterable, Optional
|
|
9
|
-
|
|
10
|
-
import grpc
|
|
11
|
-
|
|
12
|
-
import sys, os
|
|
13
|
-
# Ensure generated stubs (python/gen) are importable as top-level modules
|
|
14
|
-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'gen')))
|
|
15
|
-
import uns_gateway_pb2 as pb2
|
|
16
|
-
import uns_gateway_pb2_grpc as gw
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def make_channel(addr: str) -> grpc.Channel:
|
|
20
|
-
# Supports unix:/path.sock or host:port
|
|
21
|
-
return grpc.insecure_channel(addr)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def register_endpoints(stub: gw.UnsGatewayStub) -> None:
|
|
25
|
-
# Mirror TS api-example.ts for two GET endpoints under /api/example/summary-1 and -2
|
|
26
|
-
qp = [
|
|
27
|
-
pb2.ApiQueryParam(name="filter", type="string", required=True, description="Filter za podatke"),
|
|
28
|
-
pb2.ApiQueryParam(name="limit", type="number", required=False, description="Koliko podatkov želiš"),
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
for idx, tag in [(1, "Tag1"), (2, "Tag2")]:
|
|
32
|
-
req = pb2.RegisterApiGetRequest(
|
|
33
|
-
topic="example/",
|
|
34
|
-
attribute=f"summary-{idx}",
|
|
35
|
-
api_description=f"Test API endpoint {idx}",
|
|
36
|
-
tags=[tag],
|
|
37
|
-
query_params=qp,
|
|
38
|
-
)
|
|
39
|
-
res = stub.RegisterApiGet(req)
|
|
40
|
-
if not res.ok:
|
|
41
|
-
raise RuntimeError(f"Failed to register summary-{idx}: {res.error}")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def serve_requests(stub: gw.UnsGatewayStub, echo: bool = False) -> None:
|
|
45
|
-
q: "queue.Queue[pb2.ApiEventResponse|None]" = queue.Queue()
|
|
46
|
-
|
|
47
|
-
def req_iter():
|
|
48
|
-
while True:
|
|
49
|
-
item = q.get()
|
|
50
|
-
if item is None:
|
|
51
|
-
break
|
|
52
|
-
yield item
|
|
53
|
-
|
|
54
|
-
stream = stub.ApiEventStream(req_iter())
|
|
55
|
-
try:
|
|
56
|
-
for ev in stream:
|
|
57
|
-
# ev.path e.g. /example/summary-1; ev.query is a map<string,string>
|
|
58
|
-
path = ev.path
|
|
59
|
-
query: Dict[str, str] = dict(ev.query)
|
|
60
|
-
filt = query.get("filter", "")
|
|
61
|
-
limit = query.get("limit")
|
|
62
|
-
endpoint = "summary-1" if path.endswith("summary-1") else ("summary-2" if path.endswith("summary-2") else "unknown")
|
|
63
|
-
|
|
64
|
-
# Your business logic here. This example returns JSON reflecting the request.
|
|
65
|
-
body = {
|
|
66
|
-
"status": "OK",
|
|
67
|
-
"endpoint": endpoint,
|
|
68
|
-
"filter": filt,
|
|
69
|
-
"limit": int(limit) if (limit is not None and limit.isdigit()) else None,
|
|
70
|
-
"data": [
|
|
71
|
-
{"id": 1, "value": 42},
|
|
72
|
-
{"id": 2, "value": 43},
|
|
73
|
-
],
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
q.put(
|
|
77
|
-
pb2.ApiEventResponse(
|
|
78
|
-
id=ev.id,
|
|
79
|
-
status=200,
|
|
80
|
-
headers={"Content-Type": "application/json"},
|
|
81
|
-
body=json.dumps(body),
|
|
82
|
-
)
|
|
83
|
-
)
|
|
84
|
-
except KeyboardInterrupt:
|
|
85
|
-
q.put(None)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def main():
|
|
89
|
-
parser = argparse.ArgumentParser(description="Python API handler for UNS gRPC Gateway")
|
|
90
|
-
parser.add_argument("--addr", default=os.environ.get("GATEWAY_ADDR", "unix:/tmp/template-uns-rtt-1.6.14-uns-gateway.sock"), help="Gateway address (unix:/path.sock or host:port)")
|
|
91
|
-
parser.add_argument("--echo", action="store_true", help="Echo simple OK response instead of JSON body")
|
|
92
|
-
args = parser.parse_args()
|
|
93
|
-
|
|
94
|
-
with make_channel(args.addr) as ch:
|
|
95
|
-
stub = gw.UnsGatewayStub(ch)
|
|
96
|
-
register_endpoints(stub)
|
|
97
|
-
serve_requests(stub, echo=args.echo)
|
|
98
|
-
|
|
99
4
|
|
|
100
|
-
|
|
101
|
-
|
|
5
|
+
client = Client()
|
|
6
|
+
|
|
7
|
+
# Register API with structured query params
|
|
8
|
+
qp = [
|
|
9
|
+
ApiQueryParam(name="filter", type="string", required=True, description="Filter za podatke"),
|
|
10
|
+
ApiQueryParam(name="limit", type="number", required=False, description="Koliko podatkov želiš"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
client.register_api(
|
|
14
|
+
topic="example/",
|
|
15
|
+
attribute="summary-1",
|
|
16
|
+
desc="Example API endpoint",
|
|
17
|
+
tags=["example", "demo"],
|
|
18
|
+
query_params=qp
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Handle API events
|
|
22
|
+
def handle_api(ev):
|
|
23
|
+
print(f"API request: {ev.path}, query={dict(ev.query)}")
|
|
24
|
+
body = {"status": "OK", "path": ev.path, "query": dict(ev.query)}
|
|
25
|
+
return ApiEventResponse(id=ev.id, status=200, headers={"Content-Type": "application/json"},
|
|
26
|
+
body=json.dumps(body))
|
|
27
|
+
|
|
28
|
+
client.api_stream(handle_event=handle_api)
|
|
@@ -1,124 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
from __future__ import annotations
|
|
1
|
+
from gateway.client import Client
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import socket
|
|
3
|
+
def handle_message(msg):
|
|
4
|
+
print(f"Callback got {msg.topic}: {msg.payload}")
|
|
5
|
+
# You could also transform, save, or push to another service
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import sys, os
|
|
12
|
-
# Ensure generated stubs (python/gen) are importable as top-level modules
|
|
13
|
-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'gen')))
|
|
14
|
-
import uns_gateway_pb2 as pb2
|
|
15
|
-
import uns_gateway_pb2_grpc as gw
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def make_channel(addr: str) -> grpc.Channel:
|
|
19
|
-
return grpc.insecure_channel(addr)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import atexit, signal, subprocess
|
|
23
|
-
_AUTO_GATEWAY_PROC = None
|
|
24
|
-
def _cleanup_gateway():
|
|
25
|
-
global _AUTO_GATEWAY_PROC
|
|
26
|
-
if _AUTO_GATEWAY_PROC and _AUTO_GATEWAY_PROC.poll() is None:
|
|
27
|
-
try:
|
|
28
|
-
if os.name == 'nt':
|
|
29
|
-
_AUTO_GATEWAY_PROC.terminate()
|
|
30
|
-
else:
|
|
31
|
-
os.killpg(os.getpgid(_AUTO_GATEWAY_PROC.pid), signal.SIGTERM)
|
|
32
|
-
try:
|
|
33
|
-
_AUTO_GATEWAY_PROC.wait(timeout=3)
|
|
34
|
-
except Exception:
|
|
35
|
-
if os.name == 'nt':
|
|
36
|
-
_AUTO_GATEWAY_PROC.kill()
|
|
37
|
-
else:
|
|
38
|
-
os.killpg(os.getpgid(_AUTO_GATEWAY_PROC.pid), signal.SIGKILL)
|
|
39
|
-
except Exception:
|
|
40
|
-
pass
|
|
41
|
-
_AUTO_GATEWAY_PROC = None
|
|
42
|
-
|
|
43
|
-
def _default_addr() -> str:
|
|
44
|
-
if os.name == 'nt':
|
|
45
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
46
|
-
s.bind(('127.0.0.1', 0))
|
|
47
|
-
port = s.getsockname()[1]
|
|
48
|
-
return f"127.0.0.1:{port}"
|
|
49
|
-
else:
|
|
50
|
-
return f"unix:/tmp/uns-gateway-data-subscribe-{os.getpid()}.sock"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def ensure_gateway_running(addr: str | None, *, auto: bool, timeout_s: int = 20) -> str:
|
|
54
|
-
if not addr:
|
|
55
|
-
addr = _default_addr()
|
|
56
|
-
ch = make_channel(addr)
|
|
57
|
-
try:
|
|
58
|
-
grpc.channel_ready_future(ch).result(timeout=2)
|
|
59
|
-
ch.close(); return addr
|
|
60
|
-
except Exception:
|
|
61
|
-
ch.close()
|
|
62
|
-
if not auto:
|
|
63
|
-
return addr
|
|
64
|
-
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
65
|
-
cli_path = os.path.join(repo_root, "dist", "uns-grpc", "uns-gateway-cli")
|
|
66
|
-
popen_kwargs = {}
|
|
67
|
-
creationflags = 0
|
|
68
|
-
if os.name != 'nt':
|
|
69
|
-
popen_kwargs['preexec_fn'] = os.setsid
|
|
70
|
-
else:
|
|
71
|
-
creationflags = getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
|
72
|
-
suffix = f"py-{os.path.basename(__file__).replace('.py','')}-{os.getpid()}"
|
|
73
|
-
proc = subprocess.Popen(["node", cli_path, "--addr", addr, "--instanceSuffix", suffix, "--instanceMode", "force"], cwd=repo_root, creationflags=creationflags, **popen_kwargs)
|
|
74
|
-
global _AUTO_GATEWAY_PROC
|
|
75
|
-
_AUTO_GATEWAY_PROC = proc
|
|
76
|
-
atexit.register(_cleanup_gateway)
|
|
77
|
-
start = time.time()
|
|
78
|
-
while time.time() - start < timeout_s:
|
|
79
|
-
ch2 = make_channel(addr)
|
|
80
|
-
try:
|
|
81
|
-
grpc.channel_ready_future(ch2).result(timeout=2)
|
|
82
|
-
ch2.close();
|
|
83
|
-
try:
|
|
84
|
-
wait_s = int(os.environ.get("UNS_GATEWAY_HANDOVER_WAIT", "11"))
|
|
85
|
-
except Exception:
|
|
86
|
-
wait_s = 11
|
|
87
|
-
if wait_s > 0:
|
|
88
|
-
time.sleep(wait_s)
|
|
89
|
-
return addr
|
|
90
|
-
except Exception:
|
|
91
|
-
ch2.close(); time.sleep(0.5)
|
|
92
|
-
raise RuntimeError("Gateway did not become ready in time")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def subscribe(addr: str | None, topics: list[str], auto: bool) -> None:
|
|
96
|
-
addr = ensure_gateway_running(addr, auto=auto)
|
|
97
|
-
# Ensure subscriber readiness
|
|
98
|
-
try:
|
|
99
|
-
with make_channel(addr) as ch:
|
|
100
|
-
gw.UnsGatewayStub(ch).Ready(pb2.ReadyRequest(timeout_ms=15000, wait_input=True))
|
|
101
|
-
except Exception:
|
|
102
|
-
pass
|
|
103
|
-
with make_channel(addr) as ch:
|
|
104
|
-
stub = gw.UnsGatewayStub(ch)
|
|
105
|
-
stream = stub.Subscribe(pb2.SubscribeRequest(topics=topics))
|
|
106
|
-
try:
|
|
107
|
-
for msg in stream:
|
|
108
|
-
print(f"{msg.topic}: {msg.payload}")
|
|
109
|
-
except KeyboardInterrupt:
|
|
110
|
-
pass
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def main():
|
|
114
|
-
parser = argparse.ArgumentParser(description="Subscribe to MQTT topics via gateway")
|
|
115
|
-
parser.add_argument("--addr", default=None)
|
|
116
|
-
parser.add_argument("--auto", action="store_true")
|
|
117
|
-
parser.add_argument("topics", nargs="+")
|
|
118
|
-
args = parser.parse_args()
|
|
119
|
-
|
|
120
|
-
subscribe(args.addr, args.topics, args.auto)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if __name__ == "__main__":
|
|
124
|
-
main()
|
|
7
|
+
client = Client()
|
|
8
|
+
client.subscribe(["sensors/temperature"], callback=handle_message)
|
|
@@ -1,147 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
1
|
+
from gateway.client import Client, iso_now
|
|
5
2
|
import json
|
|
6
|
-
import os
|
|
7
|
-
import time
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from typing import Iterable
|
|
10
|
-
import socket
|
|
11
|
-
|
|
12
|
-
import grpc
|
|
13
|
-
|
|
14
|
-
import sys, os
|
|
15
|
-
# Ensure generated stubs (python/gen) are importable as top-level modules
|
|
16
|
-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'gen')))
|
|
17
|
-
import uns_gateway_pb2 as pb2
|
|
18
|
-
import uns_gateway_pb2_grpc as gw
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def iso_now() -> str:
|
|
22
|
-
dt = datetime.now(timezone.utc)
|
|
23
|
-
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def make_channel(addr: str) -> grpc.Channel:
|
|
27
|
-
return grpc.insecure_channel(addr)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import atexit, signal, subprocess
|
|
31
|
-
_AUTO_GATEWAY_PROC = None
|
|
32
|
-
def _cleanup_gateway():
|
|
33
|
-
global _AUTO_GATEWAY_PROC
|
|
34
|
-
if _AUTO_GATEWAY_PROC and _AUTO_GATEWAY_PROC.poll() is None:
|
|
35
|
-
try:
|
|
36
|
-
if os.name == 'nt':
|
|
37
|
-
_AUTO_GATEWAY_PROC.terminate()
|
|
38
|
-
else:
|
|
39
|
-
os.killpg(os.getpgid(_AUTO_GATEWAY_PROC.pid), signal.SIGTERM)
|
|
40
|
-
try:
|
|
41
|
-
_AUTO_GATEWAY_PROC.wait(timeout=3)
|
|
42
|
-
except Exception:
|
|
43
|
-
if os.name == 'nt':
|
|
44
|
-
_AUTO_GATEWAY_PROC.kill()
|
|
45
|
-
else:
|
|
46
|
-
os.killpg(os.getpgid(_AUTO_GATEWAY_PROC.pid), signal.SIGKILL)
|
|
47
|
-
except Exception:
|
|
48
|
-
pass
|
|
49
|
-
_AUTO_GATEWAY_PROC = None
|
|
50
|
-
|
|
51
|
-
def _default_addr() -> str:
|
|
52
|
-
if os.name == 'nt':
|
|
53
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
54
|
-
s.bind(('127.0.0.1', 0))
|
|
55
|
-
port = s.getsockname()[1]
|
|
56
|
-
return f"127.0.0.1:{port}"
|
|
57
|
-
else:
|
|
58
|
-
return f"unix:/tmp/uns-gateway-data-transformer-{os.getpid()}.sock"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def ensure_gateway_running(addr: str | None, *, auto: bool, timeout_s: int = 20) -> str:
|
|
62
|
-
if not addr:
|
|
63
|
-
addr = _default_addr()
|
|
64
|
-
ch = make_channel(addr)
|
|
65
|
-
try:
|
|
66
|
-
grpc.channel_ready_future(ch).result(timeout=2)
|
|
67
|
-
ch.close(); return addr
|
|
68
|
-
except Exception:
|
|
69
|
-
ch.close()
|
|
70
|
-
if not auto:
|
|
71
|
-
return addr
|
|
72
|
-
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
73
|
-
cli_path = os.path.join(repo_root, "dist", "uns-grpc", "uns-gateway-cli")
|
|
74
|
-
popen_kwargs = {}
|
|
75
|
-
creationflags = 0
|
|
76
|
-
if os.name != 'nt':
|
|
77
|
-
popen_kwargs['preexec_fn'] = os.setsid
|
|
78
|
-
else:
|
|
79
|
-
creationflags = getattr(subprocess, 'CREATE_NEW_PROCESS_GROUP', 0)
|
|
80
|
-
suffix = f"py-{os.path.basename(__file__).replace('.py','')}-{os.getpid()}"
|
|
81
|
-
proc = subprocess.Popen(["node", cli_path, "--addr", addr, "--instanceSuffix", suffix, "--instanceMode", "force"], cwd=repo_root, creationflags=creationflags, **popen_kwargs)
|
|
82
|
-
global _AUTO_GATEWAY_PROC
|
|
83
|
-
_AUTO_GATEWAY_PROC = proc
|
|
84
|
-
atexit.register(_cleanup_gateway)
|
|
85
|
-
start = time.time()
|
|
86
|
-
while time.time() - start < timeout_s:
|
|
87
|
-
ch2 = make_channel(addr)
|
|
88
|
-
try:
|
|
89
|
-
grpc.channel_ready_future(ch2).result(timeout=2)
|
|
90
|
-
ch2.close();
|
|
91
|
-
try:
|
|
92
|
-
wait_s = int(os.environ.get("UNS_GATEWAY_HANDOVER_WAIT", "11"))
|
|
93
|
-
except Exception:
|
|
94
|
-
wait_s = 11
|
|
95
|
-
if wait_s > 0:
|
|
96
|
-
time.sleep(wait_s)
|
|
97
|
-
return addr
|
|
98
|
-
except Exception:
|
|
99
|
-
ch2.close(); time.sleep(0.5)
|
|
100
|
-
raise RuntimeError("Gateway did not become ready in time")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def transform(addr: str | None, in_topics: Iterable[str], out_topic: str, out_attribute: str, uom: str, data_group: str, auto: bool) -> None:
|
|
104
|
-
addr = ensure_gateway_running(addr, auto=auto)
|
|
105
|
-
# Ensure both subscriber and publisher are ready
|
|
106
|
-
try:
|
|
107
|
-
with make_channel(addr) as ch:
|
|
108
|
-
gw.UnsGatewayStub(ch).Ready(pb2.ReadyRequest(timeout_ms=15000, wait_input=True, wait_output=True))
|
|
109
|
-
except Exception:
|
|
110
|
-
pass
|
|
111
|
-
with make_channel(addr) as ch:
|
|
112
|
-
stub = gw.UnsGatewayStub(ch)
|
|
113
|
-
stream = stub.Subscribe(pb2.SubscribeRequest(topics=list(in_topics)))
|
|
114
|
-
for msg in stream:
|
|
115
|
-
# Expecting raw/data with payload "<number>,<timestamp>"
|
|
116
|
-
if msg.topic == "raw/data":
|
|
117
|
-
parts = msg.payload.split(",")
|
|
118
|
-
try:
|
|
119
|
-
value = float(parts[0])
|
|
120
|
-
except Exception:
|
|
121
|
-
continue
|
|
122
|
-
req = pb2.PublishRequest(
|
|
123
|
-
topic=out_topic,
|
|
124
|
-
attribute=out_attribute,
|
|
125
|
-
data=pb2.Data(time=iso_now(), value_number=value, uom=uom, data_group=data_group),
|
|
126
|
-
)
|
|
127
|
-
res = stub.Publish(req)
|
|
128
|
-
if not res.ok:
|
|
129
|
-
print("publish error:", res.error)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def main():
|
|
133
|
-
parser = argparse.ArgumentParser(description="Subscribe to raw/# and publish UNS data packets")
|
|
134
|
-
parser.add_argument("--addr", default=None, help="Gateway address (unix:/path.sock or host:port); defaults to unique per-script")
|
|
135
|
-
parser.add_argument("--auto", action="store_true", help="auto-start gateway if not running")
|
|
136
|
-
parser.add_argument("--in", dest="in_topics", nargs="+", default=["raw/#"], help="input topics to subscribe")
|
|
137
|
-
parser.add_argument("--out-topic", default="example/", help="output UNS base topic")
|
|
138
|
-
parser.add_argument("--attribute", default="data-number", help="output attribute")
|
|
139
|
-
parser.add_argument("--uom", default="mV", help="unit of measure")
|
|
140
|
-
parser.add_argument("--group", default="electricity", help="dataGroup")
|
|
141
|
-
args = parser.parse_args()
|
|
142
3
|
|
|
143
|
-
|
|
4
|
+
client = Client()
|
|
5
|
+
|
|
6
|
+
def transform_data(value):
|
|
7
|
+
|
|
8
|
+
return value * 1.1
|
|
144
9
|
|
|
10
|
+
def handle_message(msg):
|
|
11
|
+
payload = json.loads(msg.payload)
|
|
12
|
+
value = float(payload["value"])
|
|
13
|
+
|
|
14
|
+
transformed_value = transform_data(value)
|
|
15
|
+
client.publish_data("sensors/temperature", "room1", value=transformed_value, uom="°C")
|
|
145
16
|
|
|
146
|
-
|
|
147
|
-
main()
|
|
17
|
+
client.subscribe(["sensors/temperature"], callback=handle_message)
|