@uns-kit/cli 0.0.34 → 0.0.36

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.
Files changed (57) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +128 -128
  3. package/dist/index.js +181 -6
  4. package/package.json +2 -2
  5. package/templates/api/src/examples/api-example.ts +62 -61
  6. package/templates/azure-pipelines.yml +21 -21
  7. package/templates/codegen/codegen.ts +15 -15
  8. package/templates/codegen/src/uns/uns-tags.ts +1 -1
  9. package/templates/codegen/src/uns/uns-topics.ts +1 -1
  10. package/templates/config-files/config-docker.json +27 -0
  11. package/templates/config-files/config-localhost.json +27 -0
  12. package/templates/cron/src/examples/cron-example.ts +46 -45
  13. package/templates/default/README.md +30 -30
  14. package/templates/default/config.json +27 -27
  15. package/templates/default/gitignore +51 -48
  16. package/templates/default/package.json +19 -19
  17. package/templates/default/src/config/project.config.extension.example +23 -23
  18. package/templates/default/src/config/project.config.extension.ts +6 -6
  19. package/templates/default/src/examples/data-example.ts +68 -68
  20. package/templates/default/src/examples/load-test-data.ts +108 -108
  21. package/templates/default/src/examples/table-example.ts +66 -66
  22. package/templates/default/src/examples/uns-gateway-cli.ts +7 -7
  23. package/templates/default/src/index.ts +15 -15
  24. package/templates/default/src/uns/uns-tags.ts +2 -2
  25. package/templates/default/src/uns/uns-topics.ts +2 -2
  26. package/templates/default/tsconfig.json +16 -16
  27. package/templates/python/app/README.md +8 -0
  28. package/templates/python/app/__init__.py +0 -0
  29. package/templates/python/examples/README.md +134 -84
  30. package/templates/python/examples/__init__.py +0 -0
  31. package/templates/python/examples/api_handler.py +28 -101
  32. package/templates/python/examples/data_publish.py +11 -0
  33. package/templates/python/examples/data_subscribe.py +8 -124
  34. package/templates/python/examples/data_transformer.py +17 -147
  35. package/templates/python/examples/table_transformer.py +16 -163
  36. package/templates/python/gateway/__init__.py +0 -0
  37. package/templates/python/gateway/cli.py +75 -0
  38. package/templates/python/gateway/client.py +155 -0
  39. package/templates/python/gateway/manager.py +97 -0
  40. package/templates/python/gen/__init__.py +1 -0
  41. package/templates/python/gen/uns_gateway_pb2.py +70 -0
  42. package/templates/python/gen/uns_gateway_pb2_grpc.py +312 -0
  43. package/templates/python/main.py +1 -0
  44. package/templates/python/proto/uns-gateway.proto +102 -102
  45. package/templates/python/pyproject.toml +5 -0
  46. package/templates/python/scripts/setup.sh +87 -64
  47. package/templates/temporal/src/examples/temporal-example.ts +35 -34
  48. package/templates/vscode/.vscode/launch.json +164 -164
  49. package/templates/vscode/.vscode/settings.json +9 -9
  50. package/templates/vscode/uns-kit.code-workspace +13 -13
  51. package/templates/python/examples/api_register_and_serve.py +0 -159
  52. package/templates/python/examples/data_publish_once.py +0 -140
  53. package/templates/python/examples/data_publisher_loop.py +0 -142
  54. package/templates/python/gateway_client.py +0 -242
  55. package/templates/python/local/README.md +0 -8
  56. package/templates/python/local/__init__.py +0 -2
  57. package/templates/python/requirements.txt +0 -3
@@ -1,84 +1,134 @@
1
- # Python Examples for the UNS gRPC Gateway
2
-
3
- These examples demonstrate how to use the Node-based UNS Gateway (MQTT + UNS infra) from Python via gRPC. They mirror the TypeScript examples for data, table, cron-like publishing, and API endpoints.
4
-
5
- The gateway centralizes all UNS rules (packet format, sequenceId, interval, produced topics, handover). Python focuses on business logic and ML/ETL while calling the gateway.
6
-
7
- ## Prerequisites
8
-
9
- - Build the Node project once: `npm run build`
10
- - Generate Python stubs and venv: `cd python && ./scripts/setup.sh && source venv/bin/activate`
11
- - Ensure `config.json` is present at repo root (Node gateway reads brokers and UNS options from it)
12
-
13
- ## Gateway Address
14
-
15
- - If `--addr` is omitted, each script auto‑generates a unique address:
16
- - Unix/macOS: `unix:/tmp/uns-gateway-<script>-<pid>.sock`
17
- - Windows: `127.0.0.1:<ephemeral_port>`
18
- - You can still pass an explicit `--addr` if you want a fixed address.
19
-
20
- All examples accept `--addr` and `--auto`. With `--auto`, the example will spawn the gateway via `npm run gateway` and wait until it's ready.
21
-
22
- Imports note: Examples add the generated stubs folder (`python/gen`) to `sys.path` so you can run them from the repo root or from the `python/` folder. Ensure you’ve run `./scripts/setup.sh` so `gen` exists.
23
-
24
- Readiness: Examples call the gateway’s `Ready()` RPC to wait for the requested components (publisher/subscriber/API) to be active before publishing or subscribing. No manual sleeps are required.
25
-
26
- ## Common Flags
27
-
28
- - `--addr`: Gateway address (`unix:/path.sock` or `host:port`)
29
- - `--auto`: Auto-start the gateway if not running
30
-
31
- ## Scripts
32
-
33
- - `data_transformer.py`
34
- - Subscribes to input topics (default `raw/#`) and publishes UNS data packets (`example/` + `data-number`).
35
- - Examples:
36
- - Unix/macOS: `python python/examples/data_transformer.py --addr unix:/tmp/uns-gateway.sock --auto --in raw/# --out-topic example/ --attribute data-number --uom mV --group electricity`
37
- - Windows: `python python/examples/data_transformer.py --addr 127.0.0.1:50051 --auto --in raw/# --out-topic example/ --attribute data-number --uom mV --group electricity`
38
-
39
- - `table_transformer.py`
40
- - Subscribes to `integration/raw-table` JSON and publishes a UNS table packet (`example/factory-a/line-1/` + `table-sample`).
41
- - Examples:
42
- - Unix/macOS: `python python/examples/table_transformer.py --addr unix:/tmp/uns-gateway.sock --auto --in-topic integration/raw-table --out-topic example/factory-a/line-1/ --attribute table-sample`
43
- - Windows: `python python/examples/table_transformer.py --addr 127.0.0.1:50051 --auto --in-topic integration/raw-table --out-topic example/factory-a/line-1/ --attribute table-sample`
44
-
45
- - `data_publisher_loop.py`
46
- - Cron-like loop that publishes a data value periodically (default 1000 ms).
47
- - Examples:
48
- - Unix/macOS: `python python/examples/data_publisher_loop.py --addr unix:/tmp/uns-gateway.sock --auto --out-topic example/ --attribute data-number --uom mV --period-ms 1000`
49
- - Windows: `python python/examples/data_publisher_loop.py --addr 127.0.0.1:50051 --auto --out-topic example/ --attribute data-number --uom mV --period-ms 1000`
50
-
51
- - `data_publish_once.py`
52
- - Sends a single UNS data packet.
53
- - Examples:
54
- - Unix/macOS: `python python/examples/data_publish_once.py --addr unix:/tmp/uns-gateway.sock --auto --out-topic example/ --attribute data-number --value 42 --uom mV --group electricity`
55
- - Windows: `python python/examples/data_publish_once.py --addr 127.0.0.1:50051 --auto --out-topic example/ --attribute data-number --value 42 --uom mV --group electricity`
56
-
57
- - `data_subscribe.py`
58
- - Subscribes to topics via the gateway and prints messages.
59
- - Examples:
60
- - Unix/macOS: `python python/examples/data_subscribe.py --addr unix:/tmp/uns-gateway.sock --auto raw/# some/other/topic`
61
- - Windows: `python python/examples/data_subscribe.py --addr 127.0.0.1:50051 --auto raw/# some/other/topic`
62
-
63
- - `api_register_and_serve.py`
64
- - Registers `/api/example/summary-1` and `/api/example/summary-2` endpoints and serves requests over a bidirectional stream.
65
- - Examples:
66
- - Unix/macOS: `python python/examples/api_register_and_serve.py --addr unix:/tmp/uns-gateway.sock --auto`
67
- - Windows: `python python/examples/api_register_and_serve.py --addr 127.0.0.1:50051 --auto`
68
- - Then call: `http://<gateway-host>:<gateway-port>/api/example/summary-1?filter=foo&limit=10`
69
-
70
- - `api_handler.py`
71
- - Another API handler demo that returns structured JSON responses. Similar to `api_register_and_serve.py`.
72
-
73
- ## Auth
74
-
75
- - If `config.json` sets `uns.jwksWellKnownUrl`, the gateway enforces JWT via JWKS (and path rules), matching UnsApiProxy behavior.
76
- - Otherwise, it uses a dev `jwtSecret` ("CHANGEME"). For local testing, you can:
77
- - Remove JWKS settings in `config.json` and rely on the dev secret, or
78
- - Provide a valid Bearer token as required by your environment.
79
-
80
- ## Notes
81
-
82
- - The gateway binds to `--addr` or the `UNS_GATEWAY_ADDR` env var. Examples pass `--addr` explicitly.
83
- - On Unix/macOS, a stable UDS path (e.g., `/tmp/uns-gateway.sock`) is recommended for simplicity.
84
- - All examples will auto-start the gateway when `--auto` is used.
1
+ # Python Examples for the UNS gRPC Gateway
2
+
3
+ These examples demonstrate how to use the Node-based UNS Gateway (MQTT + UNS infra) from Python via gRPC. They mirror the TypeScript examples for data, table, cron-like publishing, and API endpoints.
4
+
5
+ The gateway centralizes all UNS rules (packet format, sequenceId, interval, produced topics, handover). Python focuses on business logic and ML/ETL while calling the gateway.
6
+
7
+ ## Prerequisites
8
+
9
+ - Build the Node project once: `npm run build`
10
+ - Generate Python stubs and venv: `cd python && ./scripts/setup.sh && source venv/bin/activate`
11
+ - Ensure `config.json` is present at repo root (Node gateway reads brokers and UNS options from it)
12
+
13
+ ## Gateway Address
14
+
15
+ - If `--addr` is omitted, each script auto‑generates a unique address:
16
+ - Unix/macOS: `unix:/tmp/uns-gateway-<script>-<pid>.sock`
17
+ - Windows: `127.0.0.1:<ephemeral_port>`
18
+ - You can still pass an explicit `--addr` if you want a fixed address.
19
+
20
+ All examples accept `--addr` and `--auto`. With `--auto`, the example will spawn the gateway via `npm run gateway` and wait until it's ready.
21
+
22
+ Imports note: Examples add the generated stubs folder (`python/gen`) to `sys.path` so you can run them from the repo root or from the `python/` folder. Ensure you’ve run `./scripts/setup.sh` so `gen` exists.
23
+
24
+ Readiness: Examples call the gateway’s `Ready()` RPC to wait for the requested components (publisher/subscriber/API) to be active before publishing or subscribing. No manual sleeps are required.
25
+
26
+ ## Common Flags
27
+
28
+ - `--addr`: Gateway address (`unix:/path.sock` or `host:port`)
29
+ - `--auto`: Auto-start the gateway if not running
30
+
31
+ ## Scripts
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
+ ```
122
+
123
+ ## Auth
124
+
125
+ - If `config.json` sets `uns.jwksWellKnownUrl`, the gateway enforces JWT via JWKS (and path rules), matching UnsApiProxy behavior.
126
+ - Otherwise, it uses a dev `jwtSecret` ("CHANGEME"). For local testing, you can:
127
+ - Remove JWKS settings in `config.json` and rely on the dev secret, or
128
+ - Provide a valid Bearer token as required by your environment.
129
+
130
+ ## Notes
131
+
132
+ - The gateway binds to `--addr` or the `UNS_GATEWAY_ADDR` env var. Examples pass `--addr` explicitly.
133
+ - On Unix/macOS, a stable UDS path (e.g., `/tmp/uns-gateway.sock`) is recommended for simplicity.
134
+ - All examples will auto-start the gateway when `--auto` is used.
File without changes
@@ -1,101 +1,28 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import argparse
5
- 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
-
100
- if __name__ == "__main__":
101
- main()
1
+ from gateway.client import Client
2
+ from uns_gateway_pb2 import ApiQueryParam, ApiEventResponse
3
+ import json
4
+
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)
@@ -0,0 +1,11 @@
1
+ from gateway.client import Client
2
+ import time
3
+ # Publish a single data point
4
+ client = Client()
5
+ client.publish_data(
6
+ topic="sensors/temperature",
7
+ attribute="room1",
8
+ value=22.5,
9
+ uom="°C"
10
+ )
11
+ time.sleep(10)
@@ -1,124 +1,8 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import argparse
5
- import os
6
- import time
7
- import socket
8
-
9
- import grpc
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()
1
+ from gateway.client import Client
2
+
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
6
+
7
+ client = Client()
8
+ client.subscribe(["sensors/temperature"], callback=handle_message)
@@ -1,147 +1,17 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import argparse
5
- 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
-
143
- transform(args.addr, args.in_topics, args.out_topic, args.attribute, args.uom, args.group, args.auto)
144
-
145
-
146
- if __name__ == "__main__":
147
- main()
1
+ from gateway.client import Client, iso_now
2
+ import json
3
+
4
+ client = Client()
5
+
6
+ def transform_data(value):
7
+
8
+ return value * 1.1
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")
16
+
17
+ client.subscribe(["sensors/temperature"], callback=handle_message)