piphi-runtime-kit-python 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- piphi_runtime_kit_python/__init__.py +180 -0
- piphi_runtime_kit_python/adapters/__init__.py +13 -0
- piphi_runtime_kit_python/adapters/fastapi.py +73 -0
- piphi_runtime_kit_python/fastapi.py +13 -0
- piphi_runtime_kit_python/runtime/__init__.py +124 -0
- piphi_runtime_kit_python/runtime/auth.py +134 -0
- piphi_runtime_kit_python/runtime/config_sync.py +157 -0
- piphi_runtime_kit_python/runtime/configuration.py +112 -0
- piphi_runtime_kit_python/runtime/context.py +29 -0
- piphi_runtime_kit_python/runtime/discovery.py +71 -0
- piphi_runtime_kit_python/runtime/dispatch.py +197 -0
- piphi_runtime_kit_python/runtime/errors.py +131 -0
- piphi_runtime_kit_python/runtime/events.py +194 -0
- piphi_runtime_kit_python/runtime/health.py +54 -0
- piphi_runtime_kit_python/runtime/lifespan.py +79 -0
- piphi_runtime_kit_python/runtime/mqtt.py +213 -0
- piphi_runtime_kit_python/runtime/registry.py +77 -0
- piphi_runtime_kit_python/runtime/starter.py +96 -0
- piphi_runtime_kit_python/runtime/state.py +22 -0
- piphi_runtime_kit_python/runtime/tasks.py +42 -0
- piphi_runtime_kit_python/runtime/telemetry.py +130 -0
- piphi_runtime_kit_python/schemas.py +145 -0
- piphi_runtime_kit_python-0.3.0.dist-info/METADATA +875 -0
- piphi_runtime_kit_python-0.3.0.dist-info/RECORD +27 -0
- piphi_runtime_kit_python-0.3.0.dist-info/WHEEL +4 -0
- piphi_runtime_kit_python-0.3.0.dist-info/entry_points.txt +4 -0
- piphi_runtime_kit_python-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: piphi-runtime-kit-python
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: PiPhi Network runtime integration helpers
|
|
5
|
+
Keywords: piphi,runtime,integration,iot
|
|
6
|
+
Author-Email: KelvinSan <support@piphi.network>
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Project-URL: Homepage, https://github.com/PiPhi-io/piphi-runtime-kit-python#readme
|
|
16
|
+
Project-URL: Repository, https://github.com/PiPhi-io/piphi-runtime-kit-python
|
|
17
|
+
Project-URL: Issues, https://github.com/PiPhi-io/piphi-runtime-kit-python/issues
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
20
|
+
Requires-Dist: pydantic<3.0,>=2.8
|
|
21
|
+
Provides-Extra: fastapi
|
|
22
|
+
Requires-Dist: fastapi<1.0,>=0.115; extra == "fastapi"
|
|
23
|
+
Provides-Extra: mqtt
|
|
24
|
+
Requires-Dist: aiomqtt<3.0,>=2.4; extra == "mqtt"
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# piphi-runtime-kit-python
|
|
28
|
+
|
|
29
|
+
Small Python helpers for building PiPhi runtime integrations.
|
|
30
|
+
|
|
31
|
+
This package is intentionally thin. It removes repetitive PiPhi runtime
|
|
32
|
+
plumbing without hiding the HTTP contract behind a large framework. The goal
|
|
33
|
+
is to help developers move faster while still understanding what their runtime
|
|
34
|
+
is doing.
|
|
35
|
+
|
|
36
|
+
Version `0.3.0` is the current documented baseline.
|
|
37
|
+
|
|
38
|
+
> New to PiPhi? Start with [First 15 Minutes](#first-15-minutes), then read [The Golden Path](#the-golden-path), then compare your code to the example app.
|
|
39
|
+
|
|
40
|
+
> Safety note: every ID, token, hostname, and UUID shown in this README is placeholder example data. Do not copy real internal tokens, production hosts, or live container IDs into code samples, docs, or tests.
|
|
41
|
+
|
|
42
|
+
## Quick Navigation
|
|
43
|
+
|
|
44
|
+
- [Who this is for](#who-this-is-for)
|
|
45
|
+
- [Install](#install)
|
|
46
|
+
- [First 15 Minutes](#first-15-minutes)
|
|
47
|
+
- [Route Contract At A Glance](#route-contract-at-a-glance)
|
|
48
|
+
- [UI Config Endpoints](#ui-config-endpoints)
|
|
49
|
+
- [One Complete Mini Example](#one-complete-mini-example)
|
|
50
|
+
- [The Golden Path](#the-golden-path)
|
|
51
|
+
- [The IDs You Need To Understand](#the-ids-you-need-to-understand)
|
|
52
|
+
- [Plain-Language Concepts](#plain-language-concepts)
|
|
53
|
+
- [When To Use Which Helper](#when-to-use-which-helper)
|
|
54
|
+
- [Troubleshooting](#troubleshooting)
|
|
55
|
+
- [What A Real Integration Still Needs](#what-a-real-integration-still-needs)
|
|
56
|
+
|
|
57
|
+
## Reading Paths
|
|
58
|
+
|
|
59
|
+
- New developer
|
|
60
|
+
Read `First 15 Minutes`, `One Complete Mini Example`, `The Golden Path`, and `Plain-Language Concepts`.
|
|
61
|
+
- Experienced developer
|
|
62
|
+
Read `Route Contract At A Glance`, `When To Use Which Helper`, and `What A Real Integration Still Needs`.
|
|
63
|
+
- Debugging a runtime
|
|
64
|
+
Jump to `Troubleshooting`, `Common Mistakes`, and `Clear Error Handling`.
|
|
65
|
+
|
|
66
|
+
## Who this is for
|
|
67
|
+
|
|
68
|
+
This SDK is for developers building PiPhi integrations in Python.
|
|
69
|
+
|
|
70
|
+
It is a good fit if you are using:
|
|
71
|
+
|
|
72
|
+
- FastAPI
|
|
73
|
+
- Starlette
|
|
74
|
+
- another async Python HTTP framework
|
|
75
|
+
- plain request/response handlers where you still want PiPhi helpers
|
|
76
|
+
|
|
77
|
+
If you are brand new to PiPhi, start with the golden path in this README and
|
|
78
|
+
then compare your code to the example app.
|
|
79
|
+
|
|
80
|
+
## What the SDK handles
|
|
81
|
+
|
|
82
|
+
The SDK is meant to own the shared PiPhi runtime plumbing:
|
|
83
|
+
|
|
84
|
+
- runtime auth context
|
|
85
|
+
- request/header auth helpers
|
|
86
|
+
- optional FastAPI request helpers
|
|
87
|
+
- process state
|
|
88
|
+
- background task tracking
|
|
89
|
+
- telemetry delivery to PiPhi Core
|
|
90
|
+
- event delivery to PiPhi Core
|
|
91
|
+
- config sync helpers
|
|
92
|
+
- typed config validation helpers
|
|
93
|
+
- discovery normalization and response helpers
|
|
94
|
+
- lifecycle bootstrap helpers
|
|
95
|
+
- runtime health and diagnostics helpers
|
|
96
|
+
- in-memory runtime registry for active entries, state, and recent events
|
|
97
|
+
- clearer PiPhi-specific delivery errors
|
|
98
|
+
|
|
99
|
+
## What stays in your integration
|
|
100
|
+
|
|
101
|
+
Your integration still owns vendor-specific behavior:
|
|
102
|
+
|
|
103
|
+
- how to discover devices
|
|
104
|
+
- how to talk to the vendor API or device
|
|
105
|
+
- how often to poll
|
|
106
|
+
- how to transform vendor data into PiPhi entities or telemetry
|
|
107
|
+
- which runtime events matter for that integration
|
|
108
|
+
|
|
109
|
+
This boundary is important. The SDK should make runtime plumbing easier, not
|
|
110
|
+
hide vendor logic behind an overly magical abstraction.
|
|
111
|
+
|
|
112
|
+
## Install
|
|
113
|
+
|
|
114
|
+
The package currently supports Python `>=3.11`.
|
|
115
|
+
|
|
116
|
+
Install from a local checkout:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pdm add /path/to/piphi-runtime-kit-python
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If you need the optional FastAPI helpers:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pdm add "/path/to/piphi-runtime-kit-python[fastapi]"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## First 15 Minutes
|
|
129
|
+
|
|
130
|
+
If you want the shortest path to a working runtime, do this:
|
|
131
|
+
|
|
132
|
+
1. install the SDK
|
|
133
|
+
2. create a starter with `create_runtime_starter(...)`
|
|
134
|
+
3. define one typed config model
|
|
135
|
+
4. add `GET /health`
|
|
136
|
+
5. add `POST /config`
|
|
137
|
+
6. add one telemetry example route
|
|
138
|
+
7. compare your result to the example app
|
|
139
|
+
|
|
140
|
+
Minimal success checklist:
|
|
141
|
+
|
|
142
|
+
- the runtime starts
|
|
143
|
+
- `/health` returns `200`
|
|
144
|
+
- `/config` stores one device in the registry
|
|
145
|
+
- a route can queue telemetry without crashing
|
|
146
|
+
|
|
147
|
+
If those four things work, you already have a real PiPhi runtime foundation.
|
|
148
|
+
|
|
149
|
+
## Route Contract At A Glance
|
|
150
|
+
|
|
151
|
+
These are the most common runtime routes and what they are for.
|
|
152
|
+
|
|
153
|
+
| Route | Method | Usually Required | Purpose | Helpful SDK Pieces |
|
|
154
|
+
| --- | --- | --- | --- | --- |
|
|
155
|
+
| `/health` | `GET` | Yes | Basic runtime health | `starter.health_response(...)` |
|
|
156
|
+
| `/diagnostics` | `GET` | Yes | Support/debug details | `starter.diagnostics_response(...)` |
|
|
157
|
+
| `/discover` | `POST` | Usually | Find devices or accounts | `normalize_discovery_inputs(...)`, `build_discovery_response(...)` |
|
|
158
|
+
| `/config` | `POST` | Yes | Apply one config | `validate_typed_config(...)`, `build_config_apply_response(...)` |
|
|
159
|
+
| `/config/sync` | `POST` | Usually | Reconcile the runtime to a full snapshot | `validate_typed_configs(...)`, `config_sync.apply_snapshot(...)` |
|
|
160
|
+
| `/deconfigure` | `POST` | Usually | Remove one config | `RuntimeConfigRemoveResponse` |
|
|
161
|
+
| `/events` | `GET` | Common | Show recent local runtime events | `build_event_list_response(...)` |
|
|
162
|
+
| `/state` | `GET` | Common | Show current runtime state | `registry.entries`, `registry.state_snapshots` |
|
|
163
|
+
| `/entities` | `GET` | Integration-specific | Show normalized entity list | integration-owned |
|
|
164
|
+
| `/ui` or `/ui-config` | `GET` | Optional | Return config UI metadata | integration-owned |
|
|
165
|
+
|
|
166
|
+
## UI Config Endpoints
|
|
167
|
+
|
|
168
|
+
Many integrations expose `/ui` or `/ui-config` so the PiPhi frontend knows how
|
|
169
|
+
to render a configuration form.
|
|
170
|
+
|
|
171
|
+
The important thing to know is:
|
|
172
|
+
|
|
173
|
+
- this is still just plain JSON
|
|
174
|
+
- the runtime SDK does not require a special wrapper for it
|
|
175
|
+
- your integration can return schema data directly
|
|
176
|
+
|
|
177
|
+
The usual pattern is to return:
|
|
178
|
+
|
|
179
|
+
- a JSON Schema object under `schema`
|
|
180
|
+
- a UI customization object under `uiSchema`
|
|
181
|
+
|
|
182
|
+
Simple example:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
@app.get("/ui-config")
|
|
186
|
+
async def ui_config() -> dict[str, Any]:
|
|
187
|
+
return {
|
|
188
|
+
"schema": {
|
|
189
|
+
"title": "Demo Device Setup",
|
|
190
|
+
"type": "object",
|
|
191
|
+
"required": ["host"],
|
|
192
|
+
"properties": {
|
|
193
|
+
"host": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"title": "Host",
|
|
196
|
+
},
|
|
197
|
+
"alias": {
|
|
198
|
+
"type": "string",
|
|
199
|
+
"title": "Alias",
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
"uiSchema": {
|
|
204
|
+
"host": {
|
|
205
|
+
"placeholder": "192.168.1.50",
|
|
206
|
+
},
|
|
207
|
+
"alias": {
|
|
208
|
+
"placeholder": "Office Sensor",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
If your frontend uses `svelte-jsonschema-form`, the official docs are here:
|
|
215
|
+
|
|
216
|
+
- https://x0k.dev/svelte-jsonschema-form/
|
|
217
|
+
|
|
218
|
+
That library is a good fit when your frontend is already rendering JSON Schema
|
|
219
|
+
forms and you want integrations to stay simple by returning plain schema data.
|
|
220
|
+
|
|
221
|
+
For now, the recommended SDK approach is:
|
|
222
|
+
|
|
223
|
+
- document `/ui-config`
|
|
224
|
+
- return plain JSON schema/uiSchema objects
|
|
225
|
+
- keep any frontend-specific rendering helpers outside the runtime SDK
|
|
226
|
+
|
|
227
|
+
## Optional MQTT Source Topics
|
|
228
|
+
|
|
229
|
+
Some integrations work better with a shared source stream than direct runtime-to-runtime HTTP calls.
|
|
230
|
+
|
|
231
|
+
Examples:
|
|
232
|
+
|
|
233
|
+
- `rtl_433` collectors
|
|
234
|
+
- packet capture helpers
|
|
235
|
+
- protocol bridges that many integrations may want to listen to
|
|
236
|
+
|
|
237
|
+
For those cases, the SDK now includes an optional MQTT helper with a small source-oriented topic contract.
|
|
238
|
+
|
|
239
|
+
Recommended topic layout:
|
|
240
|
+
|
|
241
|
+
- `piphi/sources/<source>/packets`
|
|
242
|
+
- `piphi/sources/<source>/models/<model>/packets`
|
|
243
|
+
- `piphi/sources/<source>/status`
|
|
244
|
+
- `piphi/sources/<source>/errors`
|
|
245
|
+
|
|
246
|
+
For `rtl_433`, the default shared packet topic is:
|
|
247
|
+
|
|
248
|
+
```text
|
|
249
|
+
piphi/sources/rtl433/packets
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The payload should carry detailed device hints in the JSON body so subscribers do not need complex topic parsing just to identify a packet.
|
|
253
|
+
|
|
254
|
+
## One Complete Mini Example
|
|
255
|
+
|
|
256
|
+
The snippets in this README are useful, but sometimes it helps to see one small
|
|
257
|
+
working shape in one place.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from fastapi import FastAPI, Request
|
|
261
|
+
|
|
262
|
+
from piphi_runtime_kit_python import (
|
|
263
|
+
RuntimeConfig,
|
|
264
|
+
build_config_apply_response,
|
|
265
|
+
create_runtime_starter,
|
|
266
|
+
schedule_telemetry_delivery,
|
|
267
|
+
validate_typed_config,
|
|
268
|
+
)
|
|
269
|
+
from piphi_runtime_kit_python.fastapi import sync_runtime_auth_from_fastapi_payload
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class DemoConfig(RuntimeConfig):
|
|
273
|
+
host: str
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
starter = create_runtime_starter(
|
|
277
|
+
integration_id="demo-runtime",
|
|
278
|
+
integration_name="Demo Runtime",
|
|
279
|
+
version="0.1.0",
|
|
280
|
+
)
|
|
281
|
+
app = FastAPI()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@app.get("/health")
|
|
285
|
+
async def health():
|
|
286
|
+
return starter.health_response()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@app.post("/config")
|
|
290
|
+
async def config(payload: DemoConfig, request: Request):
|
|
291
|
+
sync_runtime_auth_from_fastapi_payload(starter.runtime, request, payload)
|
|
292
|
+
typed_payload = validate_typed_config(payload, DemoConfig)
|
|
293
|
+
starter.registry.set(
|
|
294
|
+
typed_payload.id,
|
|
295
|
+
{
|
|
296
|
+
"config_id": typed_payload.config_id or typed_payload.id,
|
|
297
|
+
"device_id": typed_payload.device_id or typed_payload.id,
|
|
298
|
+
"host": typed_payload.host,
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
return build_config_apply_response(config_id=typed_payload.config_id or typed_payload.id)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@app.post("/telemetry/example")
|
|
305
|
+
async def telemetry_example():
|
|
306
|
+
entry = starter.registry.primary_entry()
|
|
307
|
+
if entry is None:
|
|
308
|
+
return {"status": "skipped", "reason": "no configured device"}
|
|
309
|
+
|
|
310
|
+
schedule_telemetry_delivery(
|
|
311
|
+
process_state=starter.runtime.process_state,
|
|
312
|
+
telemetry_client=starter.telemetry_client,
|
|
313
|
+
auth_context=starter.runtime.auth,
|
|
314
|
+
device_id=str(entry["device_id"]),
|
|
315
|
+
metrics={"temperature_c": 21.4},
|
|
316
|
+
units={"temperature_c": "C"},
|
|
317
|
+
)
|
|
318
|
+
return {"status": "queued"}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
That example is not production-ready, but it is enough to show the most
|
|
322
|
+
important runtime ideas working together.
|
|
323
|
+
|
|
324
|
+
## The Golden Path
|
|
325
|
+
|
|
326
|
+
If you only read one section, read this one. This is the intended beginner path.
|
|
327
|
+
|
|
328
|
+
### 1. Create a starter
|
|
329
|
+
|
|
330
|
+
Start with `create_runtime_starter(...)`. It gives you one obvious object that
|
|
331
|
+
already contains the most common pieces:
|
|
332
|
+
|
|
333
|
+
- shared runtime auth and process state
|
|
334
|
+
- an in-memory registry
|
|
335
|
+
- a telemetry client
|
|
336
|
+
- an event client
|
|
337
|
+
- a config sync coordinator
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from piphi_runtime_kit_python import create_runtime_starter
|
|
341
|
+
|
|
342
|
+
starter = create_runtime_starter(
|
|
343
|
+
integration_id="demo-runtime",
|
|
344
|
+
integration_name="Demo Runtime",
|
|
345
|
+
version="0.1.0",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
runtime = starter.runtime
|
|
349
|
+
registry = starter.registry
|
|
350
|
+
telemetry = starter.telemetry_client
|
|
351
|
+
events = starter.event_client
|
|
352
|
+
config_sync = starter.config_sync
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
For most new integrations, this is the right place to begin.
|
|
356
|
+
|
|
357
|
+
### 2. Define a typed config model
|
|
358
|
+
|
|
359
|
+
Each integration should subclass `RuntimeConfig` with its own fields.
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from piphi_runtime_kit_python import RuntimeConfig
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class DemoDeviceConfig(RuntimeConfig):
|
|
366
|
+
host: str
|
|
367
|
+
alias: str | None = None
|
|
368
|
+
poll_interval_seconds: int = 30
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Use `validate_typed_config(...)` when accepting config payloads:
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
from piphi_runtime_kit_python import validate_typed_config
|
|
375
|
+
|
|
376
|
+
typed_payload = validate_typed_config(payload, DemoDeviceConfig)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 3. Sync auth from each request
|
|
380
|
+
|
|
381
|
+
PiPhi runtimes receive auth and scope through headers. Your integration should
|
|
382
|
+
sync that into the runtime context for every relevant request.
|
|
383
|
+
|
|
384
|
+
Framework-agnostic usage:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
runtime.auth.sync_from_headers(request.headers, payload_container_id=payload.container_id)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
FastAPI helper usage:
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
from piphi_runtime_kit_python.fastapi import sync_runtime_auth_from_fastapi_payload
|
|
394
|
+
|
|
395
|
+
parsed = sync_runtime_auth_from_fastapi_payload(runtime, request, payload)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### 4. Store active runtime entries in the registry
|
|
399
|
+
|
|
400
|
+
Use the runtime registry for the in-memory working set of active devices.
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
registry.set(
|
|
404
|
+
typed_payload.id,
|
|
405
|
+
{
|
|
406
|
+
"device_id": typed_payload.id,
|
|
407
|
+
"config_id": typed_payload.config_id or typed_payload.id,
|
|
408
|
+
"integration_id": typed_payload.integration_id,
|
|
409
|
+
"host": typed_payload.host,
|
|
410
|
+
},
|
|
411
|
+
)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
This is not the source of truth for configs. PiPhi Core is. The registry is
|
|
415
|
+
just the runtime's active in-memory working set.
|
|
416
|
+
|
|
417
|
+
### 5. Send telemetry and events
|
|
418
|
+
|
|
419
|
+
You can call the clients directly:
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
await starter.telemetry_client.send_metrics(
|
|
423
|
+
auth_context=starter.runtime.auth,
|
|
424
|
+
device_id="plug-1",
|
|
425
|
+
metrics={"is_on": True, "current_power_w": 13.2},
|
|
426
|
+
)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Or queue delivery in the background:
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
from piphi_runtime_kit_python import (
|
|
433
|
+
schedule_event_delivery,
|
|
434
|
+
schedule_telemetry_delivery,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
schedule_telemetry_delivery(
|
|
438
|
+
process_state=runtime.process_state,
|
|
439
|
+
telemetry_client=telemetry,
|
|
440
|
+
auth_context=runtime.auth,
|
|
441
|
+
device_id="plug-1",
|
|
442
|
+
metrics={"is_on": True},
|
|
443
|
+
container_id=runtime.auth.container_id,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
schedule_event_delivery(
|
|
447
|
+
process_state=runtime.process_state,
|
|
448
|
+
event_client=events,
|
|
449
|
+
auth_context=runtime.auth,
|
|
450
|
+
event_type="device.turned_on",
|
|
451
|
+
device={
|
|
452
|
+
"device_id": "plug-1",
|
|
453
|
+
"config_id": "core-config-uuid",
|
|
454
|
+
"integration_id": "demo-runtime",
|
|
455
|
+
},
|
|
456
|
+
source="demo_runtime",
|
|
457
|
+
)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 6. Expose the common runtime routes
|
|
461
|
+
|
|
462
|
+
Most runtimes should provide at least:
|
|
463
|
+
|
|
464
|
+
- `/health`
|
|
465
|
+
- `/diagnostics`
|
|
466
|
+
- `/discover`
|
|
467
|
+
- `/config`
|
|
468
|
+
- `/configs/sync` or `/config/sync`
|
|
469
|
+
- `/deconfigure`
|
|
470
|
+
- `/events`
|
|
471
|
+
- `/state`
|
|
472
|
+
- `/entities`
|
|
473
|
+
|
|
474
|
+
Some integrations also expose `/ui` or `/ui-config`.
|
|
475
|
+
|
|
476
|
+
### 7. Use the example app as your checklist
|
|
477
|
+
|
|
478
|
+
The example app shows the intended flow end to end:
|
|
479
|
+
|
|
480
|
+
- [`examples/minimal_fastapi_runtime/app.py`](./examples/minimal_fastapi_runtime/app.py)
|
|
481
|
+
- [`examples/minimal_fastapi_runtime/README.md`](./examples/minimal_fastapi_runtime/README.md)
|
|
482
|
+
|
|
483
|
+
## The IDs You Need To Understand
|
|
484
|
+
|
|
485
|
+
These are the identifiers that show up most often:
|
|
486
|
+
|
|
487
|
+
- `id`
|
|
488
|
+
This is the runtime's local config id in the payload your integration receives.
|
|
489
|
+
- `config_id`
|
|
490
|
+
This is the real PiPhi Core config UUID.
|
|
491
|
+
- `device_id`
|
|
492
|
+
This is the physical or logical device identifier used by the runtime.
|
|
493
|
+
- `container_id`
|
|
494
|
+
This is the runtime/container scope used for Core auth.
|
|
495
|
+
- `integration_id`
|
|
496
|
+
This is the installed integration id in Core.
|
|
497
|
+
|
|
498
|
+
The most common beginner mistake is confusing `id` with `config_id`.
|
|
499
|
+
|
|
500
|
+
A safe mental model is:
|
|
501
|
+
|
|
502
|
+
- `id` is local to the runtime payload shape
|
|
503
|
+
- `config_id` is the real Core identity for config-backed event flows
|
|
504
|
+
|
|
505
|
+
If you are sending events back to Core, `config_id`, `container_id`, and
|
|
506
|
+
`integration_id` need to be correct.
|
|
507
|
+
|
|
508
|
+
## Plain-Language Concepts
|
|
509
|
+
|
|
510
|
+
If you are new to the platform, these terms can feel more complicated than they
|
|
511
|
+
really are. Here is the simple version.
|
|
512
|
+
|
|
513
|
+
- `snapshot`
|
|
514
|
+
A snapshot is just PiPhi saying, "Here is the full list of configs you should
|
|
515
|
+
have right now." You compare that list to what your runtime currently has.
|
|
516
|
+
Then you add the missing ones and remove the stale ones.
|
|
517
|
+
Example:
|
|
518
|
+
`await config_sync.apply_snapshot(snapshot=payload, active_config_ids=registry.ids(), apply_config=apply_config, remove_config=remove_config, get_active_config_ids=registry.ids)`
|
|
519
|
+
- `config sync`
|
|
520
|
+
Config sync is the process of making your runtime match the latest snapshot.
|
|
521
|
+
Think of it like refreshing a shopping list and making sure your cart matches it.
|
|
522
|
+
Example:
|
|
523
|
+
`typed_configs = validate_typed_configs(snapshot.configs, DemoDeviceConfig)`
|
|
524
|
+
- `registry`
|
|
525
|
+
The registry is the runtime's in-memory notebook. It keeps track of the
|
|
526
|
+
devices and state that are active right now.
|
|
527
|
+
Example:
|
|
528
|
+
`registry.set(config.id, {"device_id": config.device_id or config.id, "host": config.host})`
|
|
529
|
+
- `telemetry`
|
|
530
|
+
Telemetry is the stream of measurements, like temperature, humidity, power,
|
|
531
|
+
or signal strength.
|
|
532
|
+
Example:
|
|
533
|
+
`schedule_telemetry_delivery(process_state=runtime.process_state, telemetry_client=telemetry, auth_context=runtime.auth, device_id="plug-1", metrics={"temperature_c": 21.4}, units={"temperature_c": "C"})`
|
|
534
|
+
- `event`
|
|
535
|
+
An event is a meaningful thing that happened, like "device configured" or
|
|
536
|
+
"device turned on."
|
|
537
|
+
Example:
|
|
538
|
+
`registry.append_event(build_local_event_record(event_type="device.configured", device=entry, payload={"host": entry["host"]}, source="demo-runtime", severity="info"))`
|
|
539
|
+
- `container_id`
|
|
540
|
+
This is the identity of the running runtime process from Core's point of view.
|
|
541
|
+
It helps Core know which runtime is talking to it.
|
|
542
|
+
Example:
|
|
543
|
+
`runtime.auth.sync_from_headers(request.headers, payload_container_id=payload.container_id)`
|
|
544
|
+
- `config_id`
|
|
545
|
+
This is the real Core-side id for a config. If a route or event needs the
|
|
546
|
+
official Core identity, this is the one that matters.
|
|
547
|
+
Example:
|
|
548
|
+
`entry = {"config_id": payload.config_id or payload.id, "device_id": payload.device_id or payload.id}`
|
|
549
|
+
- `device_id`
|
|
550
|
+
This is the actual device or logical thing you are monitoring or controlling.
|
|
551
|
+
One config often points at one device, but they are not always the same idea.
|
|
552
|
+
Example:
|
|
553
|
+
`await telemetry.send_metrics(auth_context=runtime.auth, device_id="plug-1", metrics={"is_on": True})`
|
|
554
|
+
- `starter`
|
|
555
|
+
The starter is the beginner-friendly bundle that gives you the common SDK
|
|
556
|
+
pieces in one place so you do not have to wire them up one by one.
|
|
557
|
+
Example:
|
|
558
|
+
`starter = create_runtime_starter(integration_id="demo-runtime", integration_name="Demo Runtime", version="0.1.0")`
|
|
559
|
+
|
|
560
|
+
## When To Use Which Helper
|
|
561
|
+
|
|
562
|
+
Some helpers look similar at first. This is the fast way to choose.
|
|
563
|
+
|
|
564
|
+
| Use this | When you want | Notes |
|
|
565
|
+
| --- | --- | --- |
|
|
566
|
+
| `create_runtime_starter(...)` | one obvious SDK entry point | Best starting point for new integrations |
|
|
567
|
+
| `validate_typed_config(...)` | one incoming config validated into your model | Use in `/config` |
|
|
568
|
+
| `validate_typed_configs(...)` | many incoming configs validated at once | Use in `/config/sync` |
|
|
569
|
+
| `runtime.auth.sync_from_headers(...)` | sync auth in any framework | Lowest-level option |
|
|
570
|
+
| `sync_runtime_auth_from_fastapi_payload(...)` | sync auth in FastAPI with less boilerplate | Best FastAPI path |
|
|
571
|
+
| `telemetry_client.send_metrics(...)` | send telemetry right now | Use when you want direct control |
|
|
572
|
+
| `schedule_telemetry_delivery(...)` | queue telemetry in the background | Best for route handlers and poll loops |
|
|
573
|
+
| `event_client.send_event(...)` | send a Core event right now | Use when you want direct control |
|
|
574
|
+
| `schedule_event_delivery(...)` | queue Core event delivery in the background | Best for async runtime workflows |
|
|
575
|
+
| `build_local_event_record(...)` | record a runtime-local event | This is not the same as Core delivery |
|
|
576
|
+
| `config_sync.apply_snapshot(...)` | reconcile the runtime to a full snapshot | Best for `/config/sync` |
|
|
577
|
+
| `starter.health_response(...)` | return standard `/health` | Simple and recommended |
|
|
578
|
+
| `starter.diagnostics_response(...)` | return standard `/diagnostics` | Simple and recommended |
|
|
579
|
+
|
|
580
|
+
## Typical Runtime Flow
|
|
581
|
+
|
|
582
|
+
Most integrations follow this shape:
|
|
583
|
+
|
|
584
|
+
1. PiPhi calls your runtime.
|
|
585
|
+
2. Your route syncs auth from headers.
|
|
586
|
+
3. You validate the config payload into a typed model.
|
|
587
|
+
4. You connect to the vendor API or local device.
|
|
588
|
+
5. You store the active runtime entry in the registry.
|
|
589
|
+
6. You begin polling or listening for updates.
|
|
590
|
+
7. You send telemetry to Core.
|
|
591
|
+
8. You emit meaningful events back to Core.
|
|
592
|
+
9. You expose health and diagnostics so the runtime can be supported.
|
|
593
|
+
|
|
594
|
+
The SDK is designed to make steps `2`, `3`, `5`, `7`, `8`, and `9` easier.
|
|
595
|
+
|
|
596
|
+
## Core Usage Patterns
|
|
597
|
+
|
|
598
|
+
### Request auth helpers
|
|
599
|
+
|
|
600
|
+
The kit includes framework-agnostic helpers for extracting PiPhi runtime auth
|
|
601
|
+
from request-like header mappings.
|
|
602
|
+
|
|
603
|
+
```python
|
|
604
|
+
from piphi_runtime_kit_python import extract_runtime_auth_headers
|
|
605
|
+
|
|
606
|
+
parsed = extract_runtime_auth_headers(request.headers)
|
|
607
|
+
runtime.auth.sync_from_headers(request.headers, payload_container_id="runtime-123")
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
For FastAPI integrations, the optional adapter layer trims route boilerplate:
|
|
611
|
+
|
|
612
|
+
```python
|
|
613
|
+
from piphi_runtime_kit_python import format_runtime_auth_sync_log
|
|
614
|
+
from piphi_runtime_kit_python.fastapi import sync_runtime_auth_from_fastapi_payload
|
|
615
|
+
|
|
616
|
+
parsed = sync_runtime_auth_from_fastapi_payload(runtime, request, payload)
|
|
617
|
+
logger.info(
|
|
618
|
+
format_runtime_auth_sync_log(
|
|
619
|
+
parsed,
|
|
620
|
+
payload_container_id=payload.container_id,
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Discovery helpers
|
|
626
|
+
|
|
627
|
+
Use the discovery helpers to normalize input and return a consistent response:
|
|
628
|
+
|
|
629
|
+
```python
|
|
630
|
+
from piphi_runtime_kit_python import (
|
|
631
|
+
build_discovery_response,
|
|
632
|
+
format_discovery_attempt_log,
|
|
633
|
+
normalize_discovery_inputs,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
inputs = normalize_discovery_inputs({"username": " user@example.com ", "password": " "})
|
|
637
|
+
logger.info(format_discovery_attempt_log(inputs=inputs))
|
|
638
|
+
response = build_discovery_response(devices)
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Event helpers
|
|
642
|
+
|
|
643
|
+
Use the event helpers when you want consistent runtime event logging and Core
|
|
644
|
+
event payload generation:
|
|
645
|
+
|
|
646
|
+
```python
|
|
647
|
+
from piphi_runtime_kit_python import (
|
|
648
|
+
build_core_event_payload,
|
|
649
|
+
build_event_ingest_response,
|
|
650
|
+
format_event_log,
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
logger.info(format_event_log(payload))
|
|
654
|
+
event_response = build_event_ingest_response(event)
|
|
655
|
+
|
|
656
|
+
core_event = build_core_event_payload(
|
|
657
|
+
event_type="device.turned_on",
|
|
658
|
+
integration_id="demo-runtime",
|
|
659
|
+
config_id="core-config-uuid",
|
|
660
|
+
container_id="runtime-123",
|
|
661
|
+
device_id="plug-1",
|
|
662
|
+
payload={"host": "10.0.0.227"},
|
|
663
|
+
)
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Health and diagnostics helpers
|
|
667
|
+
|
|
668
|
+
The SDK can build consistent support endpoints:
|
|
669
|
+
|
|
670
|
+
```python
|
|
671
|
+
from piphi_runtime_kit_python import (
|
|
672
|
+
build_runtime_diagnostics_response,
|
|
673
|
+
build_runtime_health_response,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
health = build_runtime_health_response(
|
|
677
|
+
runtime,
|
|
678
|
+
integration={"id": "demo-runtime", "version": "0.1.0"},
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
diagnostics = build_runtime_diagnostics_response(
|
|
682
|
+
runtime,
|
|
683
|
+
integration={"id": "demo-runtime", "version": "0.1.0"},
|
|
684
|
+
diagnostics={"configured_device_ids": ["plug-1"]},
|
|
685
|
+
)
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
These helpers include:
|
|
689
|
+
|
|
690
|
+
- pending task counts
|
|
691
|
+
- current config generation
|
|
692
|
+
- whether runtime auth is present
|
|
693
|
+
- whether a shared Core client is bound
|
|
694
|
+
|
|
695
|
+
## Clear Error Handling
|
|
696
|
+
|
|
697
|
+
The SDK now classifies common delivery failures into PiPhi-specific errors.
|
|
698
|
+
|
|
699
|
+
Important examples:
|
|
700
|
+
|
|
701
|
+
- `CoreUnavailableError`
|
|
702
|
+
PiPhi Core could not be reached at all.
|
|
703
|
+
- `CoreTimeoutError`
|
|
704
|
+
PiPhi Core did not respond before the client timeout.
|
|
705
|
+
- `CoreRouteNotFoundError`
|
|
706
|
+
The expected Core route is not mounted or the URL is wrong.
|
|
707
|
+
- `CoreAuthError`
|
|
708
|
+
PiPhi Core rejected runtime auth.
|
|
709
|
+
- `CoreServerError`
|
|
710
|
+
PiPhi Core returned a server-side failure.
|
|
711
|
+
|
|
712
|
+
This is meant to make runtime logs easier to understand than raw `httpx`
|
|
713
|
+
exceptions alone.
|
|
714
|
+
|
|
715
|
+
## Common Mistakes
|
|
716
|
+
|
|
717
|
+
- Mistake: using `id` where `config_id` should be used.
|
|
718
|
+
Wrong:
|
|
719
|
+
`{"config_id": payload.id}`
|
|
720
|
+
Right:
|
|
721
|
+
`{"config_id": payload.config_id or payload.id}`
|
|
722
|
+
Symptom:
|
|
723
|
+
Core event delivery may fail or point at the wrong config identity.
|
|
724
|
+
- Mistake: forgetting to sync auth from request headers before sending telemetry.
|
|
725
|
+
Wrong:
|
|
726
|
+
`await telemetry.send_metrics(auth_context=runtime.auth, device_id="plug-1", metrics={"is_on": True})`
|
|
727
|
+
Right:
|
|
728
|
+
`runtime.auth.sync_from_headers(request.headers, payload_container_id=payload.container_id)`
|
|
729
|
+
Symptom:
|
|
730
|
+
Core may reject or ignore the request because the runtime context has no valid scope.
|
|
731
|
+
- Mistake: sending events without `integration_id`, `config_id`, or `container_id`.
|
|
732
|
+
Wrong:
|
|
733
|
+
building event payloads with only `device_id`
|
|
734
|
+
Right:
|
|
735
|
+
include the full device/config/runtime scope whenever the event goes back to Core
|
|
736
|
+
Symptom:
|
|
737
|
+
Core event delivery may fail or become ambiguous.
|
|
738
|
+
- Mistake: treating the registry as the source of truth instead of Core.
|
|
739
|
+
Wrong:
|
|
740
|
+
storing config state only in the registry and assuming that is enough
|
|
741
|
+
Right:
|
|
742
|
+
treat Core as the source of truth and the registry as runtime working memory
|
|
743
|
+
Symptom:
|
|
744
|
+
config sync and rehydrate flows drift from what Core expects.
|
|
745
|
+
- Mistake: putting polling cadence and vendor logic into the SDK layer instead of the integration.
|
|
746
|
+
Wrong:
|
|
747
|
+
expecting the SDK to decide vendor polling behavior
|
|
748
|
+
Right:
|
|
749
|
+
keep vendor behavior in the integration and use the SDK for runtime plumbing
|
|
750
|
+
Symptom:
|
|
751
|
+
the integration becomes harder to reason about and the SDK becomes too magical.
|
|
752
|
+
|
|
753
|
+
## Troubleshooting
|
|
754
|
+
|
|
755
|
+
### Symptom: telemetry times out
|
|
756
|
+
|
|
757
|
+
Check:
|
|
758
|
+
|
|
759
|
+
- PiPhi Core is reachable
|
|
760
|
+
- the request timeout is long enough for your environment
|
|
761
|
+
- Core is not busy or restarting
|
|
762
|
+
- your route is using `schedule_telemetry_delivery(...)` if background delivery is acceptable
|
|
763
|
+
|
|
764
|
+
### Symptom: event delivery returns `404`
|
|
765
|
+
|
|
766
|
+
Check:
|
|
767
|
+
|
|
768
|
+
- the correct Core route exists
|
|
769
|
+
- the runtime is pointing at the right Core base URL
|
|
770
|
+
- `config_id` is the real Core config UUID
|
|
771
|
+
- `container_id` and `integration_id` are present
|
|
772
|
+
|
|
773
|
+
### Symptom: Core is unavailable
|
|
774
|
+
|
|
775
|
+
Check:
|
|
776
|
+
|
|
777
|
+
- PiPhi Core is actually running
|
|
778
|
+
- the runtime can reach the host and port
|
|
779
|
+
- the SDK error is `CoreUnavailableError` and not a different failure class
|
|
780
|
+
|
|
781
|
+
### Symptom: config sync removes devices unexpectedly
|
|
782
|
+
|
|
783
|
+
Check:
|
|
784
|
+
|
|
785
|
+
- the snapshot really contains the configs you expect
|
|
786
|
+
- your registry is storing the right active ids
|
|
787
|
+
- your `get_active_config_ids` callback matches what you actually applied
|
|
788
|
+
- you are not mixing local `id` and real `config_id`
|
|
789
|
+
|
|
790
|
+
### Symptom: telemetry or events are rejected by Core
|
|
791
|
+
|
|
792
|
+
Check:
|
|
793
|
+
|
|
794
|
+
- auth was synced before delivery
|
|
795
|
+
- the runtime has a valid `container_id`
|
|
796
|
+
- the device/config scope is complete
|
|
797
|
+
- the typed delivery error explains whether this is auth, routing, timeout, or server failure
|
|
798
|
+
|
|
799
|
+
## Example App
|
|
800
|
+
|
|
801
|
+
The example app is intentionally small, but it is meant to be a real reference:
|
|
802
|
+
|
|
803
|
+
- [`examples/minimal_fastapi_runtime/app.py`](./examples/minimal_fastapi_runtime/app.py)
|
|
804
|
+
- [`examples/minimal_fastapi_runtime/README.md`](./examples/minimal_fastapi_runtime/README.md)
|
|
805
|
+
|
|
806
|
+
## Summary
|
|
807
|
+
|
|
808
|
+
If you are unsure where to begin:
|
|
809
|
+
|
|
810
|
+
1. create a starter
|
|
811
|
+
2. define a typed config model
|
|
812
|
+
3. sync auth in every route
|
|
813
|
+
4. store active entries in the registry
|
|
814
|
+
5. send telemetry and events through the SDK
|
|
815
|
+
6. compare your runtime to the example app
|
|
816
|
+
|
|
817
|
+
## What belongs in the SDK
|
|
818
|
+
|
|
819
|
+
The SDK should own PiPhi-specific plumbing:
|
|
820
|
+
|
|
821
|
+
- runtime auth parsing and outbound Core headers
|
|
822
|
+
- config sync orchestration and typed config validation
|
|
823
|
+
- telemetry and Core event publishing
|
|
824
|
+
- health and diagnostics helpers
|
|
825
|
+
- background task tracking
|
|
826
|
+
- thin framework adapters
|
|
827
|
+
|
|
828
|
+
## What stays in the integration
|
|
829
|
+
|
|
830
|
+
The integration should own vendor logic:
|
|
831
|
+
|
|
832
|
+
- device library calls and protocol handling
|
|
833
|
+
- discovery strategy specific to the vendor
|
|
834
|
+
- entity modeling and command behavior
|
|
835
|
+
- device-specific event semantics
|
|
836
|
+
- integration-specific UI schema and config fields
|
|
837
|
+
|
|
838
|
+
## What A Real Integration Still Needs
|
|
839
|
+
|
|
840
|
+
Even with the SDK, a real integration still needs application code.
|
|
841
|
+
|
|
842
|
+
You still need to write:
|
|
843
|
+
|
|
844
|
+
- a vendor client or local device client
|
|
845
|
+
- discovery logic that makes sense for that vendor
|
|
846
|
+
- entity mapping for the PiPhi frontend and automation model
|
|
847
|
+
- polling or subscription logic
|
|
848
|
+
- command handling if the device supports actions
|
|
849
|
+
- integration-specific config fields and UI schema
|
|
850
|
+
|
|
851
|
+
The SDK is the runtime foundation, not the whole house.
|
|
852
|
+
|
|
853
|
+
## Versioning and compatibility
|
|
854
|
+
|
|
855
|
+
See [VERSIONING.md](./VERSIONING.md) for:
|
|
856
|
+
|
|
857
|
+
- semver policy
|
|
858
|
+
- `0.x` stability expectations
|
|
859
|
+
- Core compatibility guidance
|
|
860
|
+
- release checklist notes
|
|
861
|
+
|
|
862
|
+
## Planned direction
|
|
863
|
+
|
|
864
|
+
The kit is intentionally small. It should cover PiPhi runtime plumbing, not
|
|
865
|
+
device-specific integration logic.
|
|
866
|
+
|
|
867
|
+
Good candidates for future additions:
|
|
868
|
+
|
|
869
|
+
- framework adapters layered on top of the core runtime helpers
|
|
870
|
+
- more route-level helpers once the core abstractions stabilize
|
|
871
|
+
|
|
872
|
+
## Included example
|
|
873
|
+
|
|
874
|
+
See [examples/minimal_fastapi_runtime](./examples/minimal_fastapi_runtime) for a
|
|
875
|
+
small reference runtime that shows how the kit fits together in a real app.
|