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,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Opinionated beginner-friendly starter helpers for PiPhi runtimes."""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .config_sync import ConfigSyncCoordinator
|
|
9
|
+
from .context import RuntimeContext
|
|
10
|
+
from .events import DEFAULT_CORE_BASE_URL as DEFAULT_EVENTS_CORE_BASE_URL, EventClient
|
|
11
|
+
from .health import build_runtime_diagnostics_response, build_runtime_health_response
|
|
12
|
+
from .registry import RuntimeRegistry
|
|
13
|
+
from .telemetry import DEFAULT_CORE_BASE_URL as DEFAULT_TELEMETRY_CORE_BASE_URL, TelemetryClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class RuntimeStarter:
|
|
18
|
+
"""Small “golden path” object for beginner-friendly runtime setup.
|
|
19
|
+
|
|
20
|
+
This intentionally bundles the runtime context, in-memory registry, config
|
|
21
|
+
sync coordinator, and Core delivery clients into one obvious object so new
|
|
22
|
+
integration authors have fewer moving parts to assemble by hand.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
integration_id: str
|
|
26
|
+
integration_name: str
|
|
27
|
+
version: str = "0.1.0"
|
|
28
|
+
core_base_url: str = DEFAULT_TELEMETRY_CORE_BASE_URL
|
|
29
|
+
max_recent_events: int = 100
|
|
30
|
+
runtime: RuntimeContext = field(init=False)
|
|
31
|
+
registry: RuntimeRegistry[dict[str, Any], dict[str, Any], dict[str, Any]] = field(init=False)
|
|
32
|
+
telemetry_client: TelemetryClient = field(init=False)
|
|
33
|
+
event_client: EventClient = field(init=False)
|
|
34
|
+
config_sync: ConfigSyncCoordinator = field(init=False)
|
|
35
|
+
|
|
36
|
+
def __post_init__(self) -> None:
|
|
37
|
+
self.runtime = RuntimeContext()
|
|
38
|
+
self.registry = RuntimeRegistry(
|
|
39
|
+
max_recent_events=self.max_recent_events
|
|
40
|
+
)
|
|
41
|
+
self.telemetry_client = TelemetryClient(
|
|
42
|
+
process_state=self.runtime.process_state,
|
|
43
|
+
core_base_url=self.core_base_url or DEFAULT_TELEMETRY_CORE_BASE_URL,
|
|
44
|
+
)
|
|
45
|
+
self.event_client = EventClient(
|
|
46
|
+
process_state=self.runtime.process_state,
|
|
47
|
+
core_base_url=self.core_base_url or DEFAULT_EVENTS_CORE_BASE_URL,
|
|
48
|
+
)
|
|
49
|
+
self.config_sync = ConfigSyncCoordinator(process_state=self.runtime.process_state)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def integration_metadata(self) -> dict[str, Any]:
|
|
53
|
+
"""Return the standard integration metadata shape used in responses."""
|
|
54
|
+
return {
|
|
55
|
+
"id": self.integration_id,
|
|
56
|
+
"name": self.integration_name,
|
|
57
|
+
"version": self.version,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def health_response(self, *, metadata: dict[str, Any] | None = None):
|
|
61
|
+
"""Build a health response using the starter's shared runtime state."""
|
|
62
|
+
return build_runtime_health_response(
|
|
63
|
+
self.runtime,
|
|
64
|
+
integration=self.integration_metadata,
|
|
65
|
+
metadata=metadata or {"active_configs": len(self.registry.ids())},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def diagnostics_response(self, *, diagnostics: dict[str, Any] | None = None):
|
|
69
|
+
"""Build a diagnostics response using the starter's shared runtime state."""
|
|
70
|
+
return build_runtime_diagnostics_response(
|
|
71
|
+
self.runtime,
|
|
72
|
+
integration=self.integration_metadata,
|
|
73
|
+
diagnostics=diagnostics
|
|
74
|
+
or {
|
|
75
|
+
"active_config_ids": self.registry.ids(),
|
|
76
|
+
"recent_event_count": len(self.registry.recent_events),
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def create_runtime_starter(
|
|
82
|
+
*,
|
|
83
|
+
integration_id: str,
|
|
84
|
+
integration_name: str,
|
|
85
|
+
version: str = "0.1.0",
|
|
86
|
+
core_base_url: str = DEFAULT_TELEMETRY_CORE_BASE_URL,
|
|
87
|
+
max_recent_events: int = 100,
|
|
88
|
+
) -> RuntimeStarter:
|
|
89
|
+
"""Return the recommended beginner-friendly runtime starter object."""
|
|
90
|
+
return RuntimeStarter(
|
|
91
|
+
integration_id=integration_id,
|
|
92
|
+
integration_name=integration_name,
|
|
93
|
+
version=version,
|
|
94
|
+
core_base_url=core_base_url,
|
|
95
|
+
max_recent_events=max_recent_events,
|
|
96
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Shared mutable runtime state used by the thin PiPhi runtime helpers."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(slots=True)
|
|
13
|
+
class RuntimeProcessState:
|
|
14
|
+
"""Process-scoped runtime state.
|
|
15
|
+
|
|
16
|
+
This keeps non-device-specific runtime concerns in one place:
|
|
17
|
+
the active Core HTTP client, the latest config generation, and tracked
|
|
18
|
+
background tasks that should be cleaned up on shutdown.
|
|
19
|
+
"""
|
|
20
|
+
current_generation: int | None = None
|
|
21
|
+
core_http_client: httpx.AsyncClient | None = None
|
|
22
|
+
background_tasks: set[asyncio.Task[Any]] = field(default_factory=set)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Helpers for creating and shutting down tracked background tasks."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .state import RuntimeProcessState
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def track_background_task(
|
|
13
|
+
task: asyncio.Task[Any],
|
|
14
|
+
*,
|
|
15
|
+
process_state: RuntimeProcessState,
|
|
16
|
+
) -> asyncio.Task[Any]:
|
|
17
|
+
"""Register an already-created task so it can be cleaned up later."""
|
|
18
|
+
process_state.background_tasks.add(task)
|
|
19
|
+
task.add_done_callback(process_state.background_tasks.discard)
|
|
20
|
+
return task
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_tracked_task(
|
|
24
|
+
coro,
|
|
25
|
+
*,
|
|
26
|
+
process_state: RuntimeProcessState,
|
|
27
|
+
) -> asyncio.Task[Any]:
|
|
28
|
+
"""Create a task and automatically register it with the process state."""
|
|
29
|
+
return track_background_task(
|
|
30
|
+
asyncio.create_task(coro),
|
|
31
|
+
process_state=process_state,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def shutdown_background_tasks(process_state: RuntimeProcessState) -> None:
|
|
36
|
+
"""Cancel and await all tracked background tasks still running."""
|
|
37
|
+
pending_tasks = [task for task in process_state.background_tasks if not task.done()]
|
|
38
|
+
for task in pending_tasks:
|
|
39
|
+
task.cancel()
|
|
40
|
+
for task in pending_tasks:
|
|
41
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
42
|
+
await task
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Helpers for sending PiPhi telemetry back to Core."""
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from ..schemas import TelemetryPayload
|
|
13
|
+
from .auth import RuntimeAuthContext, build_runtime_auth_headers
|
|
14
|
+
from .errors import classify_core_delivery_error
|
|
15
|
+
from .state import RuntimeProcessState
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_CORE_BASE_URL = "http://127.0.0.1:31419"
|
|
19
|
+
DEFAULT_TELEMETRY_PATH = "/api/v2/integrations/telemetry"
|
|
20
|
+
DEFAULT_TIMEOUT_SECONDS = 3.0
|
|
21
|
+
|
|
22
|
+
def build_core_auth_headers(
|
|
23
|
+
*,
|
|
24
|
+
container_id: str,
|
|
25
|
+
internal_token: str | None,
|
|
26
|
+
) -> dict[str, str]:
|
|
27
|
+
"""Build outbound Core auth headers using the standard PiPhi header names."""
|
|
28
|
+
return build_runtime_auth_headers(
|
|
29
|
+
container_id=container_id,
|
|
30
|
+
internal_token=internal_token,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_telemetry_payload(
|
|
35
|
+
*,
|
|
36
|
+
device_id: str,
|
|
37
|
+
metrics: dict[str, Any],
|
|
38
|
+
units: dict[str, Any] | None = None,
|
|
39
|
+
timestamp: str | None = None,
|
|
40
|
+
) -> TelemetryPayload:
|
|
41
|
+
"""Build the standard telemetry payload expected by PiPhi Core."""
|
|
42
|
+
return TelemetryPayload(
|
|
43
|
+
device_id=device_id,
|
|
44
|
+
metrics=metrics,
|
|
45
|
+
timestamp=timestamp or dt.datetime.now(dt.timezone.utc).isoformat(),
|
|
46
|
+
units=units or {},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True)
|
|
51
|
+
class TelemetryClient:
|
|
52
|
+
"""Thin Core telemetry client for runtime integrations.
|
|
53
|
+
|
|
54
|
+
This helper reuses a shared Core HTTP client when one is available and
|
|
55
|
+
otherwise creates a short-lived client with the configured timeout.
|
|
56
|
+
"""
|
|
57
|
+
process_state: RuntimeProcessState
|
|
58
|
+
core_base_url: str = DEFAULT_CORE_BASE_URL
|
|
59
|
+
telemetry_path: str = DEFAULT_TELEMETRY_PATH
|
|
60
|
+
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def telemetry_url(self) -> str:
|
|
64
|
+
"""Return the fully qualified Core telemetry endpoint URL."""
|
|
65
|
+
return f"{self.core_base_url.rstrip('/')}{self.telemetry_path}"
|
|
66
|
+
|
|
67
|
+
@asynccontextmanager
|
|
68
|
+
async def borrow_http_client(self):
|
|
69
|
+
"""Yield a shared Core client when present, otherwise a temporary client."""
|
|
70
|
+
if self.process_state.core_http_client is not None:
|
|
71
|
+
yield self.process_state.core_http_client
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
|
|
75
|
+
yield client
|
|
76
|
+
|
|
77
|
+
async def send_metrics(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
device_id: str,
|
|
81
|
+
metrics: dict[str, Any],
|
|
82
|
+
auth_context: RuntimeAuthContext,
|
|
83
|
+
container_id: str | None = None,
|
|
84
|
+
units: dict[str, Any] | None = None,
|
|
85
|
+
timestamp: str | None = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Send a telemetry payload for one device to PiPhi Core.
|
|
88
|
+
|
|
89
|
+
Typical usage:
|
|
90
|
+
await telemetry_client.send_metrics(
|
|
91
|
+
device_id="plug-1",
|
|
92
|
+
metrics={"is_on": True, "current_power_w": 12.4},
|
|
93
|
+
units={"current_power_w": "W"},
|
|
94
|
+
auth_context=runtime_context.auth,
|
|
95
|
+
container_id="runtime-123",
|
|
96
|
+
)
|
|
97
|
+
"""
|
|
98
|
+
resolved_container_id, resolved_internal_token = auth_context.resolve(
|
|
99
|
+
container_id=container_id,
|
|
100
|
+
)
|
|
101
|
+
if not resolved_container_id:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
payload = build_telemetry_payload(
|
|
105
|
+
device_id=device_id,
|
|
106
|
+
metrics=metrics,
|
|
107
|
+
units=units,
|
|
108
|
+
timestamp=timestamp,
|
|
109
|
+
)
|
|
110
|
+
headers = build_core_auth_headers(
|
|
111
|
+
container_id=resolved_container_id,
|
|
112
|
+
internal_token=resolved_internal_token,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async with self.borrow_http_client() as client:
|
|
116
|
+
try:
|
|
117
|
+
response = await client.post(
|
|
118
|
+
self.telemetry_url,
|
|
119
|
+
json=payload.model_dump(),
|
|
120
|
+
headers=headers,
|
|
121
|
+
timeout=self.timeout_seconds,
|
|
122
|
+
)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
except (httpx.HTTPStatusError, httpx.ConnectError, httpx.ReadTimeout) as exc:
|
|
125
|
+
raise classify_core_delivery_error(
|
|
126
|
+
exc=exc,
|
|
127
|
+
operation="telemetry_delivery",
|
|
128
|
+
url=self.telemetry_url,
|
|
129
|
+
timeout_seconds=self.timeout_seconds,
|
|
130
|
+
) from exc
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Framework-agnostic schema models shared by PiPhi runtime integrations."""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RuntimeConfig(BaseModel):
|
|
13
|
+
"""Base runtime config model with a required config id and extensible fields."""
|
|
14
|
+
id: str
|
|
15
|
+
model_config = ConfigDict(extra="allow")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RuntimeConfigSnapshot(BaseModel):
|
|
19
|
+
"""Snapshot payload sent from PiPhi Core to an integration runtime."""
|
|
20
|
+
container_id: str
|
|
21
|
+
integration_id: str | None = None
|
|
22
|
+
driver_pid: int | None = None
|
|
23
|
+
reason: str | None = None
|
|
24
|
+
generation: int | None = None
|
|
25
|
+
configs: list[RuntimeConfig] = Field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RuntimeConfigSyncResponse(BaseModel):
|
|
29
|
+
"""Standard response body returned by runtime config sync endpoints."""
|
|
30
|
+
status: str
|
|
31
|
+
container_id: str
|
|
32
|
+
reason: str | None = None
|
|
33
|
+
generation: int | None = None
|
|
34
|
+
applied: list[str] = Field(default_factory=list)
|
|
35
|
+
removed: list[str] = Field(default_factory=list)
|
|
36
|
+
active_config_ids: list[str] = Field(default_factory=list)
|
|
37
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RuntimeConfigApplyResponse(BaseModel):
|
|
41
|
+
"""Standard response body returned after one config is applied."""
|
|
42
|
+
status: str = "configured"
|
|
43
|
+
config_id: str
|
|
44
|
+
container_id: str | None = None
|
|
45
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RuntimeConfigRemoveResponse(BaseModel):
|
|
49
|
+
"""Standard response body returned after one config is removed."""
|
|
50
|
+
status: str = "deconfigured"
|
|
51
|
+
config_id: str
|
|
52
|
+
removed: bool = True
|
|
53
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class IntegrationDiscoveryRequest(BaseModel):
|
|
57
|
+
"""Standard discovery request sent from PiPhi Core to a runtime."""
|
|
58
|
+
container_id: str | None = None
|
|
59
|
+
driver_pid: int | None = None
|
|
60
|
+
inputs: dict[str, Any] = Field(default_factory=dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class IntegrationDiscoveryResponse(BaseModel):
|
|
64
|
+
"""Standard discovery response returned by a runtime."""
|
|
65
|
+
devices: list[dict[str, Any]] = Field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class IntegrationCommandRequest(BaseModel):
|
|
69
|
+
"""Standard command payload forwarded from PiPhi Core to a runtime."""
|
|
70
|
+
command: str
|
|
71
|
+
capability_id: str | None = None
|
|
72
|
+
device_id: str | None = None
|
|
73
|
+
entity_id: str | None = None
|
|
74
|
+
args: dict[str, Any] = Field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class IntegrationEventRequest(BaseModel):
|
|
78
|
+
"""Generic event payload emitted by a runtime integration."""
|
|
79
|
+
event_type: str
|
|
80
|
+
source: str | None = None
|
|
81
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class IntegrationEventListResponse(BaseModel):
|
|
85
|
+
"""Standard response body for listing previously captured runtime events."""
|
|
86
|
+
events: list[dict[str, Any]] = Field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class IntegrationEventIngestResponse(BaseModel):
|
|
90
|
+
"""Standard response body returned after a runtime ingests an event."""
|
|
91
|
+
status: str = "accepted"
|
|
92
|
+
event: dict[str, Any] = Field(default_factory=dict)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class EventSeverity(str, Enum):
|
|
96
|
+
"""Supported severity values for events sent back to PiPhi Core."""
|
|
97
|
+
info = "info"
|
|
98
|
+
warning = "warning"
|
|
99
|
+
error = "error"
|
|
100
|
+
critical = "critical"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class EventTransport(str, Enum):
|
|
104
|
+
"""Supported transport labels for events sent back to PiPhi Core."""
|
|
105
|
+
rest = "rest"
|
|
106
|
+
mqtt = "mqtt"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CoreEventPayload(BaseModel):
|
|
110
|
+
"""Canonical event payload posted from a runtime back to PiPhi Core."""
|
|
111
|
+
event_id: str
|
|
112
|
+
type: str
|
|
113
|
+
ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
114
|
+
integration_id: str | None = None
|
|
115
|
+
config_id: str | None = None
|
|
116
|
+
container_id: str | None = None
|
|
117
|
+
device_id: str | None = None
|
|
118
|
+
severity: EventSeverity = Field(default=EventSeverity.info)
|
|
119
|
+
transport: EventTransport = Field(default=EventTransport.rest)
|
|
120
|
+
topic: str | None = None
|
|
121
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
122
|
+
received_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class RuntimeHealthResponse(BaseModel):
|
|
126
|
+
"""Standard health response body returned by runtime integrations."""
|
|
127
|
+
status: str = "ok"
|
|
128
|
+
integration: dict[str, Any] = Field(default_factory=dict)
|
|
129
|
+
runtime: dict[str, Any] = Field(default_factory=dict)
|
|
130
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class RuntimeDiagnosticsResponse(BaseModel):
|
|
134
|
+
"""Standard diagnostics response body returned by runtime integrations."""
|
|
135
|
+
status: str = "ok"
|
|
136
|
+
integration: dict[str, Any] = Field(default_factory=dict)
|
|
137
|
+
diagnostics: dict[str, Any] = Field(default_factory=dict)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TelemetryPayload(BaseModel):
|
|
141
|
+
"""Standard telemetry request body posted back to PiPhi Core."""
|
|
142
|
+
device_id: str
|
|
143
|
+
metrics: dict[str, Any] = Field(default_factory=dict)
|
|
144
|
+
timestamp: str
|
|
145
|
+
units: dict[str, Any] = Field(default_factory=dict)
|