piphi-runtime-kit-python 0.3.1__py3-none-any.whl → 0.4.2__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 +16 -1
- piphi_runtime_kit_python/runtime/dispatch.py +50 -17
- piphi_runtime_kit_python/runtime/identity.py +80 -0
- piphi_runtime_kit_python/runtime/registry.py +17 -5
- piphi_runtime_kit_python/runtime/starter.py +29 -0
- piphi_runtime_kit_python/schemas.py +129 -2
- {piphi_runtime_kit_python-0.3.1.dist-info → piphi_runtime_kit_python-0.4.2.dist-info}/METADATA +67 -5
- {piphi_runtime_kit_python-0.3.1.dist-info → piphi_runtime_kit_python-0.4.2.dist-info}/RECORD +11 -10
- {piphi_runtime_kit_python-0.3.1.dist-info → piphi_runtime_kit_python-0.4.2.dist-info}/WHEEL +0 -0
- {piphi_runtime_kit_python-0.3.1.dist-info → piphi_runtime_kit_python-0.4.2.dist-info}/entry_points.txt +0 -0
- {piphi_runtime_kit_python-0.3.1.dist-info → piphi_runtime_kit_python-0.4.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -49,6 +49,7 @@ from .runtime.events import (
|
|
|
49
49
|
normalize_event_payload,
|
|
50
50
|
)
|
|
51
51
|
from .runtime.health import build_runtime_diagnostics_response, build_runtime_health_response
|
|
52
|
+
from .runtime.identity import build_runtime_identity, resolve_config_id, resolve_device_id
|
|
52
53
|
from .runtime.lifespan import (
|
|
53
54
|
DEFAULT_CORE_CLIENT_TIMEOUT_SECONDS,
|
|
54
55
|
DEFAULT_RUNTIME_CONTAINER_ID_ENV_NAME,
|
|
@@ -70,7 +71,7 @@ from .runtime.mqtt import (
|
|
|
70
71
|
)
|
|
71
72
|
from .runtime.registry import RuntimeRegistry
|
|
72
73
|
from .runtime.state import RuntimeProcessState
|
|
73
|
-
from .runtime.starter import RuntimeStarter, create_runtime_starter
|
|
74
|
+
from .runtime.starter import RuntimeStarter, build_entities_response, create_runtime_starter
|
|
74
75
|
from .runtime.tasks import create_tracked_task, shutdown_background_tasks, track_background_task
|
|
75
76
|
from .runtime.telemetry import TelemetryClient, build_core_auth_headers
|
|
76
77
|
from .schemas import (
|
|
@@ -80,6 +81,11 @@ from .schemas import (
|
|
|
80
81
|
IntegrationCommandRequest,
|
|
81
82
|
IntegrationDiscoveryRequest,
|
|
82
83
|
IntegrationDiscoveryResponse,
|
|
84
|
+
RuntimeEntityCommandFieldResponse,
|
|
85
|
+
RuntimeEntityCommandResponse,
|
|
86
|
+
RuntimeEntityDashboardResponse,
|
|
87
|
+
RuntimeEntitiesResponse,
|
|
88
|
+
RuntimeEntityResponse,
|
|
83
89
|
IntegrationEventIngestResponse,
|
|
84
90
|
IntegrationEventListResponse,
|
|
85
91
|
IntegrationEventRequest,
|
|
@@ -110,6 +116,11 @@ __all__ = [
|
|
|
110
116
|
"IntegrationCommandRequest",
|
|
111
117
|
"IntegrationDiscoveryRequest",
|
|
112
118
|
"IntegrationDiscoveryResponse",
|
|
119
|
+
"RuntimeEntityCommandFieldResponse",
|
|
120
|
+
"RuntimeEntityCommandResponse",
|
|
121
|
+
"RuntimeEntityDashboardResponse",
|
|
122
|
+
"RuntimeEntitiesResponse",
|
|
123
|
+
"RuntimeEntityResponse",
|
|
113
124
|
"IntegrationEventRequest",
|
|
114
125
|
"RuntimeAuthContext",
|
|
115
126
|
"RuntimeAuthHeaders",
|
|
@@ -138,9 +149,11 @@ __all__ = [
|
|
|
138
149
|
"build_event_ingest_response",
|
|
139
150
|
"build_event_list_response",
|
|
140
151
|
"build_local_event_record",
|
|
152
|
+
"build_runtime_identity",
|
|
141
153
|
"build_core_auth_headers",
|
|
142
154
|
"build_config_apply_response",
|
|
143
155
|
"build_config_remove_response",
|
|
156
|
+
"build_entities_response",
|
|
144
157
|
"build_runtime_auth_headers",
|
|
145
158
|
"build_runtime_diagnostics_response",
|
|
146
159
|
"build_runtime_health_response",
|
|
@@ -168,6 +181,8 @@ __all__ = [
|
|
|
168
181
|
"schedule_telemetry_delivery",
|
|
169
182
|
"shutdown_background_tasks",
|
|
170
183
|
"track_background_task",
|
|
184
|
+
"resolve_config_id",
|
|
185
|
+
"resolve_device_id",
|
|
171
186
|
"validate_typed_config",
|
|
172
187
|
"validate_typed_configs",
|
|
173
188
|
"DEFAULT_SOURCE_TOPIC_PREFIX",
|
|
@@ -2,8 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
"""Helpers for background dispatch of telemetry and event delivery."""
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
from typing import Any, Callable, Mapping
|
|
6
7
|
|
|
8
|
+
from .errors import CoreDeliveryError
|
|
7
9
|
from .events import EventClient
|
|
8
10
|
from .tasks import create_tracked_task
|
|
9
11
|
from .telemetry import TelemetryClient
|
|
@@ -14,6 +16,9 @@ EventRecorder = Callable[[dict[str, Any]], Any]
|
|
|
14
16
|
SkipHandler = Callable[[str, dict[str, Any]], Any]
|
|
15
17
|
ErrorHandler = Callable[[Exception, dict[str, Any]], Any]
|
|
16
18
|
|
|
19
|
+
DEFAULT_RETRYABLE_DELIVERY_ATTEMPTS = 5
|
|
20
|
+
DEFAULT_RETRYABLE_DELIVERY_INITIAL_DELAY_SECONDS = 0.5
|
|
21
|
+
|
|
17
22
|
|
|
18
23
|
def build_local_event_record(
|
|
19
24
|
*,
|
|
@@ -36,6 +41,30 @@ def build_local_event_record(
|
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
|
|
44
|
+
async def _run_with_retryable_core_delivery_backoff(
|
|
45
|
+
delivery: Callable[[], Any],
|
|
46
|
+
*,
|
|
47
|
+
max_attempts: int = DEFAULT_RETRYABLE_DELIVERY_ATTEMPTS,
|
|
48
|
+
initial_delay_seconds: float = DEFAULT_RETRYABLE_DELIVERY_INITIAL_DELAY_SECONDS,
|
|
49
|
+
) -> Any:
|
|
50
|
+
"""Retry transient Core delivery failures with a short exponential backoff."""
|
|
51
|
+
|
|
52
|
+
attempt = 1
|
|
53
|
+
delay_seconds = initial_delay_seconds
|
|
54
|
+
while True:
|
|
55
|
+
try:
|
|
56
|
+
result = delivery()
|
|
57
|
+
if asyncio.iscoroutine(result):
|
|
58
|
+
return await result
|
|
59
|
+
return result
|
|
60
|
+
except CoreDeliveryError as exc:
|
|
61
|
+
if not exc.retryable or attempt >= max_attempts:
|
|
62
|
+
raise
|
|
63
|
+
await asyncio.sleep(delay_seconds)
|
|
64
|
+
attempt += 1
|
|
65
|
+
delay_seconds *= 2
|
|
66
|
+
|
|
67
|
+
|
|
39
68
|
async def dispatch_telemetry_delivery(
|
|
40
69
|
*,
|
|
41
70
|
telemetry_client: TelemetryClient,
|
|
@@ -59,13 +88,15 @@ async def dispatch_telemetry_delivery(
|
|
|
59
88
|
return False
|
|
60
89
|
|
|
61
90
|
try:
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
91
|
+
await _run_with_retryable_core_delivery_backoff(
|
|
92
|
+
lambda: telemetry_client.send_metrics(
|
|
93
|
+
device_id=device_id,
|
|
94
|
+
metrics=metrics,
|
|
95
|
+
auth_context=auth_context,
|
|
96
|
+
container_id=resolved_container_id,
|
|
97
|
+
units=units,
|
|
98
|
+
timestamp=timestamp,
|
|
99
|
+
)
|
|
69
100
|
)
|
|
70
101
|
return True
|
|
71
102
|
except Exception as exc:
|
|
@@ -119,16 +150,18 @@ async def dispatch_event_delivery(
|
|
|
119
150
|
) -> bool:
|
|
120
151
|
"""Send one event to Core and return whether delivery succeeded."""
|
|
121
152
|
try:
|
|
122
|
-
event_sent = await
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
153
|
+
event_sent = await _run_with_retryable_core_delivery_backoff(
|
|
154
|
+
lambda: event_client.send_event(
|
|
155
|
+
event_type=event_type,
|
|
156
|
+
integration_id=device.get("integration_id"),
|
|
157
|
+
config_id=device.get("config_id"),
|
|
158
|
+
auth_context=auth_context,
|
|
159
|
+
container_id=device.get("container_id"),
|
|
160
|
+
device_id=device.get("device_id"),
|
|
161
|
+
payload=payload,
|
|
162
|
+
source=source,
|
|
163
|
+
severity=severity,
|
|
164
|
+
)
|
|
132
165
|
)
|
|
133
166
|
if not event_sent and on_skipped is not None:
|
|
134
167
|
on_skipped(
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Helpers for keeping runtime config and device identity explicit."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _coerce_optional_string(value: Any) -> str | None:
|
|
9
|
+
if value is None:
|
|
10
|
+
return None
|
|
11
|
+
text = str(value).strip()
|
|
12
|
+
return text or None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _first_present(payload: Mapping[str, Any], *keys: str) -> str | None:
|
|
16
|
+
for key in keys:
|
|
17
|
+
resolved = _coerce_optional_string(payload.get(key))
|
|
18
|
+
if resolved:
|
|
19
|
+
return resolved
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_config_id(payload: Mapping[str, Any] | Any) -> str:
|
|
24
|
+
"""Resolve the Core-owned config id from a config-like payload."""
|
|
25
|
+
|
|
26
|
+
if isinstance(payload, Mapping):
|
|
27
|
+
return _first_present(payload, "config_id", "configId", "id") or "missing"
|
|
28
|
+
return (
|
|
29
|
+
_coerce_optional_string(getattr(payload, "config_id", None))
|
|
30
|
+
or _coerce_optional_string(getattr(payload, "configId", None))
|
|
31
|
+
or _coerce_optional_string(getattr(payload, "id", None))
|
|
32
|
+
or "missing"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resolve_device_id(payload: Mapping[str, Any] | Any) -> str:
|
|
37
|
+
"""Resolve the integration-owned device id from a config-like payload."""
|
|
38
|
+
|
|
39
|
+
if isinstance(payload, Mapping):
|
|
40
|
+
return _first_present(payload, "device_id", "deviceId", "id") or "missing"
|
|
41
|
+
return (
|
|
42
|
+
_coerce_optional_string(getattr(payload, "device_id", None))
|
|
43
|
+
or _coerce_optional_string(getattr(payload, "deviceId", None))
|
|
44
|
+
or _coerce_optional_string(getattr(payload, "id", None))
|
|
45
|
+
or "missing"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_runtime_identity(
|
|
50
|
+
payload: Mapping[str, Any] | Any,
|
|
51
|
+
*,
|
|
52
|
+
container_id: str | None = None,
|
|
53
|
+
integration_id: str | None = None,
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
"""Build a canonical runtime identity record for one config-backed device."""
|
|
56
|
+
|
|
57
|
+
resolved = {
|
|
58
|
+
"config_id": resolve_config_id(payload),
|
|
59
|
+
"device_id": resolve_device_id(payload),
|
|
60
|
+
}
|
|
61
|
+
payload_container_id = (
|
|
62
|
+
_first_present(payload, "container_id", "containerId")
|
|
63
|
+
if isinstance(payload, Mapping)
|
|
64
|
+
else (
|
|
65
|
+
_coerce_optional_string(getattr(payload, "container_id", None))
|
|
66
|
+
or _coerce_optional_string(getattr(payload, "containerId", None))
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
payload_integration_id = (
|
|
70
|
+
_first_present(payload, "integration_id", "integrationId")
|
|
71
|
+
if isinstance(payload, Mapping)
|
|
72
|
+
else (
|
|
73
|
+
_coerce_optional_string(getattr(payload, "integration_id", None))
|
|
74
|
+
or _coerce_optional_string(getattr(payload, "integrationId", None))
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
resolved["container_id"] = _coerce_optional_string(container_id) or payload_container_id
|
|
79
|
+
resolved["integration_id"] = _coerce_optional_string(integration_id) or payload_integration_id
|
|
80
|
+
return resolved
|
|
@@ -50,18 +50,30 @@ class RuntimeRegistry(Generic[TEntry, TState, TEvent]):
|
|
|
50
50
|
first_entry_id = next(iter(self.entries))
|
|
51
51
|
return self.entries[first_entry_id]
|
|
52
52
|
|
|
53
|
-
def update_state(
|
|
53
|
+
def update_state(
|
|
54
|
+
self,
|
|
55
|
+
entry_id: str,
|
|
56
|
+
state: dict[str, Any],
|
|
57
|
+
*,
|
|
58
|
+
device_id: str | None = None,
|
|
59
|
+
) -> dict[str, Any]:
|
|
54
60
|
"""Store the latest state snapshot and mirror it onto the active entry."""
|
|
55
61
|
timestamp = dt.datetime.now(dt.timezone.utc).isoformat()
|
|
62
|
+
entry = self.entries.get(entry_id)
|
|
63
|
+
resolved_device_id = (
|
|
64
|
+
device_id
|
|
65
|
+
or (str(entry.get("device_id")) if entry and entry.get("device_id") else None)
|
|
66
|
+
or entry_id
|
|
67
|
+
)
|
|
56
68
|
latest_state = {
|
|
57
|
-
"device_id":
|
|
69
|
+
"device_id": resolved_device_id,
|
|
58
70
|
"state": state,
|
|
59
71
|
"last_updated": timestamp,
|
|
60
72
|
}
|
|
61
73
|
self.state_snapshots[entry_id] = latest_state
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
|
|
74
|
+
if entry is not None:
|
|
75
|
+
entry["latest_state"] = state
|
|
76
|
+
entry["last_updated"] = timestamp
|
|
65
77
|
return latest_state
|
|
66
78
|
|
|
67
79
|
def append_event(self, event: TEvent) -> TEvent:
|
|
@@ -11,6 +11,7 @@ from .events import DEFAULT_CORE_BASE_URL as DEFAULT_EVENTS_CORE_BASE_URL, Event
|
|
|
11
11
|
from .health import build_runtime_diagnostics_response, build_runtime_health_response
|
|
12
12
|
from .registry import RuntimeRegistry
|
|
13
13
|
from .telemetry import DEFAULT_CORE_BASE_URL as DEFAULT_TELEMETRY_CORE_BASE_URL, TelemetryClient
|
|
14
|
+
from ..schemas import RuntimeEntitiesResponse
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
@dataclass(slots=True)
|
|
@@ -77,6 +78,34 @@ class RuntimeStarter:
|
|
|
77
78
|
},
|
|
78
79
|
)
|
|
79
80
|
|
|
81
|
+
def entities_response(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
entities: list[dict[str, Any]],
|
|
85
|
+
capabilities: dict[str, Any] | None = None,
|
|
86
|
+
commands: dict[str, Any] | None = None,
|
|
87
|
+
) -> RuntimeEntitiesResponse:
|
|
88
|
+
"""Build the standard /entities response body for this runtime."""
|
|
89
|
+
return build_entities_response(
|
|
90
|
+
entities=entities,
|
|
91
|
+
capabilities=capabilities,
|
|
92
|
+
commands=commands,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def build_entities_response(
|
|
97
|
+
*,
|
|
98
|
+
entities: list[dict[str, Any]],
|
|
99
|
+
capabilities: dict[str, Any] | None = None,
|
|
100
|
+
commands: dict[str, Any] | None = None,
|
|
101
|
+
) -> RuntimeEntitiesResponse:
|
|
102
|
+
"""Return the standard runtime /entities response wrapper."""
|
|
103
|
+
return RuntimeEntitiesResponse(
|
|
104
|
+
entities=entities,
|
|
105
|
+
capabilities=capabilities or {},
|
|
106
|
+
commands=commands or {},
|
|
107
|
+
)
|
|
108
|
+
|
|
80
109
|
|
|
81
110
|
def create_runtime_starter(
|
|
82
111
|
*,
|
|
@@ -6,13 +6,31 @@ from datetime import datetime, timezone
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class RuntimeConfig(BaseModel):
|
|
13
13
|
"""Base runtime config model with a required config id and extensible fields."""
|
|
14
|
+
|
|
15
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
16
|
+
|
|
14
17
|
id: str
|
|
15
|
-
|
|
18
|
+
config_id: str | None = Field(
|
|
19
|
+
default=None,
|
|
20
|
+
validation_alias=AliasChoices("config_id", "configId"),
|
|
21
|
+
)
|
|
22
|
+
container_id: str | None = Field(
|
|
23
|
+
default=None,
|
|
24
|
+
validation_alias=AliasChoices("container_id", "containerId"),
|
|
25
|
+
)
|
|
26
|
+
integration_id: str | None = Field(
|
|
27
|
+
default=None,
|
|
28
|
+
validation_alias=AliasChoices("integration_id", "integrationId"),
|
|
29
|
+
)
|
|
30
|
+
device_id: str | None = Field(
|
|
31
|
+
default=None,
|
|
32
|
+
validation_alias=AliasChoices("device_id", "deviceId"),
|
|
33
|
+
)
|
|
16
34
|
|
|
17
35
|
|
|
18
36
|
class RuntimeConfigSnapshot(BaseModel):
|
|
@@ -74,6 +92,115 @@ class IntegrationCommandRequest(BaseModel):
|
|
|
74
92
|
args: dict[str, Any] = Field(default_factory=dict)
|
|
75
93
|
|
|
76
94
|
|
|
95
|
+
class RuntimeEntityDashboardResponse(BaseModel):
|
|
96
|
+
"""Optional per-entity dashboard hints returned by a runtime."""
|
|
97
|
+
|
|
98
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
99
|
+
|
|
100
|
+
allowed_widgets: list[str] = Field(
|
|
101
|
+
default_factory=list,
|
|
102
|
+
validation_alias=AliasChoices("allowed_widgets", "allowedWidgets"),
|
|
103
|
+
)
|
|
104
|
+
default_widget: str | None = Field(
|
|
105
|
+
default=None,
|
|
106
|
+
validation_alias=AliasChoices("default_widget", "defaultWidget"),
|
|
107
|
+
)
|
|
108
|
+
recommended_widgets: list[str] = Field(
|
|
109
|
+
default_factory=list,
|
|
110
|
+
validation_alias=AliasChoices("recommended_widgets", "recommendedWidgets"),
|
|
111
|
+
)
|
|
112
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class RuntimeEntityCommandFieldResponse(BaseModel):
|
|
116
|
+
"""Optional per-command argument metadata returned by a runtime entity."""
|
|
117
|
+
|
|
118
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
119
|
+
|
|
120
|
+
type: str
|
|
121
|
+
label: str | None = None
|
|
122
|
+
description: str | None = None
|
|
123
|
+
required: bool = False
|
|
124
|
+
default: Any | None = None
|
|
125
|
+
minimum: float | None = Field(
|
|
126
|
+
default=None,
|
|
127
|
+
validation_alias=AliasChoices("minimum", "min"),
|
|
128
|
+
)
|
|
129
|
+
maximum: float | None = Field(
|
|
130
|
+
default=None,
|
|
131
|
+
validation_alias=AliasChoices("maximum", "max"),
|
|
132
|
+
)
|
|
133
|
+
step: float | None = None
|
|
134
|
+
unit: str | None = None
|
|
135
|
+
options: list[Any] = Field(default_factory=list)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class RuntimeEntityCommandResponse(BaseModel):
|
|
139
|
+
"""One device-scoped command exposed by a runtime entity."""
|
|
140
|
+
|
|
141
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
142
|
+
|
|
143
|
+
id: str
|
|
144
|
+
label: str | None = None
|
|
145
|
+
description: str | None = None
|
|
146
|
+
kind: str | None = None
|
|
147
|
+
args_schema: dict[str, RuntimeEntityCommandFieldResponse] = Field(
|
|
148
|
+
default_factory=dict,
|
|
149
|
+
validation_alias=AliasChoices("args_schema", "argsSchema"),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class RuntimeEntityResponse(BaseModel):
|
|
154
|
+
"""One live runtime entity tied to a configured device/account."""
|
|
155
|
+
|
|
156
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
157
|
+
|
|
158
|
+
id: str
|
|
159
|
+
name: str
|
|
160
|
+
capabilities: list[str] = Field(default_factory=list)
|
|
161
|
+
config_id: str | None = Field(
|
|
162
|
+
default=None,
|
|
163
|
+
validation_alias=AliasChoices("config_id", "configId"),
|
|
164
|
+
)
|
|
165
|
+
device_id: str | None = Field(
|
|
166
|
+
default=None,
|
|
167
|
+
validation_alias=AliasChoices("device_id", "deviceId"),
|
|
168
|
+
)
|
|
169
|
+
device_type: str | None = Field(
|
|
170
|
+
default=None,
|
|
171
|
+
validation_alias=AliasChoices("device_type", "deviceType"),
|
|
172
|
+
)
|
|
173
|
+
device_class: str | None = Field(
|
|
174
|
+
default=None,
|
|
175
|
+
validation_alias=AliasChoices("device_class", "deviceClass"),
|
|
176
|
+
)
|
|
177
|
+
entity_type: str | None = Field(
|
|
178
|
+
default=None,
|
|
179
|
+
validation_alias=AliasChoices("entity_type", "entityType"),
|
|
180
|
+
)
|
|
181
|
+
available_commands: list[RuntimeEntityCommandResponse | str] = Field(
|
|
182
|
+
default_factory=list,
|
|
183
|
+
validation_alias=AliasChoices(
|
|
184
|
+
"available_commands",
|
|
185
|
+
"availableCommands",
|
|
186
|
+
"supported_commands",
|
|
187
|
+
"supportedCommands",
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
dashboard: RuntimeEntityDashboardResponse | None = None
|
|
191
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class RuntimeEntitiesResponse(BaseModel):
|
|
195
|
+
"""Standard response body returned by runtime /entities endpoints."""
|
|
196
|
+
|
|
197
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
198
|
+
|
|
199
|
+
entities: list[RuntimeEntityResponse] = Field(default_factory=list)
|
|
200
|
+
capabilities: dict[str, Any] = Field(default_factory=dict)
|
|
201
|
+
commands: dict[str, Any] = Field(default_factory=dict)
|
|
202
|
+
|
|
203
|
+
|
|
77
204
|
class IntegrationEventRequest(BaseModel):
|
|
78
205
|
"""Generic event payload emitted by a runtime integration."""
|
|
79
206
|
event_type: str
|
{piphi_runtime_kit_python-0.3.1.dist-info → piphi_runtime_kit_python-0.4.2.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: piphi-runtime-kit-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: PiPhi Network runtime integration helpers
|
|
5
5
|
Keywords: piphi,runtime,integration,iot
|
|
6
6
|
Author-Email: KelvinSan <support@piphi.network>
|
|
@@ -170,9 +170,71 @@ These are the most common runtime routes and what they are for.
|
|
|
170
170
|
| `/deconfigure` | `POST` | Usually | Remove one config | `RuntimeConfigRemoveResponse` |
|
|
171
171
|
| `/events` | `GET` | Common | Show recent local runtime events | `build_event_list_response(...)` |
|
|
172
172
|
| `/state` | `GET` | Common | Show current runtime state | `registry.entries`, `registry.state_snapshots` |
|
|
173
|
-
| `/entities` | `GET` | Integration-specific | Show normalized entity list |
|
|
173
|
+
| `/entities` | `GET` | Integration-specific | Show normalized entity list | `build_entities_response(...)`, `starter.entities_response(...)` |
|
|
174
174
|
| `/ui` or `/ui-config` | `GET` | Optional | Return config UI metadata | integration-owned |
|
|
175
175
|
|
|
176
|
+
## Modeling `/entities`
|
|
177
|
+
|
|
178
|
+
For simple integrations, `/entities` can still be a plain list of generic
|
|
179
|
+
entities.
|
|
180
|
+
|
|
181
|
+
For smart-home and multi-device integrations, PiPhi now recommends a richer
|
|
182
|
+
runtime-owned entity shape that ties each entity to a real saved config or
|
|
183
|
+
device. That lets Core make better device-first dashboard suggestions, and it
|
|
184
|
+
gives the frontend enough context to render better widget defaults for plugs,
|
|
185
|
+
bulbs, thermostats, and other device-specific entities.
|
|
186
|
+
|
|
187
|
+
Recommended fields:
|
|
188
|
+
|
|
189
|
+
- `id`: stable runtime entity id
|
|
190
|
+
- `name`: user-facing label
|
|
191
|
+
- `capabilities`: actual capabilities for that specific device
|
|
192
|
+
- `config_id` / `configId`: PiPhi config UUID when available
|
|
193
|
+
- `device_id` / `deviceId`: integration-native device id
|
|
194
|
+
- `device_type` / `device_class`: values like `plug`, `bulb`, `sensor`, `climate`
|
|
195
|
+
- `entity_type`: values like `switch`, `light`, `sensor`, `media`
|
|
196
|
+
- `dashboard.allowed_widgets`, `dashboard.default_widget`, `dashboard.recommended_widgets`: optional UI hints
|
|
197
|
+
|
|
198
|
+
The SDK now includes `RuntimeEntityResponse`, `RuntimeEntitiesResponse`,
|
|
199
|
+
`build_entities_response(...)`, and `starter.entities_response(...)` to make
|
|
200
|
+
that payload easier to return.
|
|
201
|
+
|
|
202
|
+
The helper returns the standard wrapper shape:
|
|
203
|
+
|
|
204
|
+
- `entities`: the runtime-owned list you generated
|
|
205
|
+
- `capabilities`: optional manifest capability metadata
|
|
206
|
+
- `commands`: optional manifest command metadata
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from piphi_runtime_kit_python import build_entities_response
|
|
210
|
+
|
|
211
|
+
@app.get("/entities")
|
|
212
|
+
async def entities() -> dict[str, Any]:
|
|
213
|
+
return build_entities_response(
|
|
214
|
+
entities=[
|
|
215
|
+
{
|
|
216
|
+
"id": "office-plug",
|
|
217
|
+
"name": "Office Plug",
|
|
218
|
+
"config_id": "core-config-uuid",
|
|
219
|
+
"device_id": "office-plug",
|
|
220
|
+
"device_class": "plug",
|
|
221
|
+
"entity_type": "switch",
|
|
222
|
+
"capabilities": ["switch", "power", "energy_today"],
|
|
223
|
+
"dashboard": {
|
|
224
|
+
"allowed_widgets": ["tile", "button", "stat"],
|
|
225
|
+
"default_widget": "tile",
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
capabilities=manifest["capabilities"],
|
|
230
|
+
commands=manifest.get("commands", {}),
|
|
231
|
+
).model_dump(exclude_none=True)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
If you are already using the starter object, the same response can be built with
|
|
235
|
+
`starter.entities_response(...)` instead of calling the standalone helper
|
|
236
|
+
directly.
|
|
237
|
+
|
|
176
238
|
## UI Config Endpoints
|
|
177
239
|
|
|
178
240
|
Many integrations expose `/ui` or `/ui-config` so the PiPhi frontend knows how
|
|
@@ -300,15 +362,15 @@ async def health():
|
|
|
300
362
|
async def config(payload: DemoConfig, request: Request):
|
|
301
363
|
sync_runtime_auth_from_fastapi_payload(starter.runtime, request, payload)
|
|
302
364
|
typed_payload = validate_typed_config(payload, DemoConfig)
|
|
365
|
+
identity = build_runtime_identity(typed_payload)
|
|
303
366
|
starter.registry.set(
|
|
304
367
|
typed_payload.id,
|
|
305
368
|
{
|
|
306
|
-
|
|
307
|
-
"device_id": typed_payload.device_id or typed_payload.id,
|
|
369
|
+
**identity,
|
|
308
370
|
"host": typed_payload.host,
|
|
309
371
|
},
|
|
310
372
|
)
|
|
311
|
-
return build_config_apply_response(config_id=
|
|
373
|
+
return build_config_apply_response(config_id=identity["config_id"])
|
|
312
374
|
|
|
313
375
|
|
|
314
376
|
@app.post("/telemetry/example")
|
{piphi_runtime_kit_python-0.3.1.dist-info → piphi_runtime_kit_python-0.4.2.dist-info}/RECORD
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
piphi_runtime_kit_python-0.
|
|
2
|
-
piphi_runtime_kit_python-0.
|
|
3
|
-
piphi_runtime_kit_python-0.
|
|
4
|
-
piphi_runtime_kit_python-0.
|
|
5
|
-
piphi_runtime_kit_python/__init__.py,sha256=
|
|
1
|
+
piphi_runtime_kit_python-0.4.2.dist-info/METADATA,sha256=th_AhsVfZ_at7LDyKQv7ZA1EVxiasOUwoSdTxQvdulI,30849
|
|
2
|
+
piphi_runtime_kit_python-0.4.2.dist-info/WHEEL,sha256=Z36eTX6lG3PITRleSd5hAZHCcz52yg3c0JQVxKBbLW0,90
|
|
3
|
+
piphi_runtime_kit_python-0.4.2.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
|
|
4
|
+
piphi_runtime_kit_python-0.4.2.dist-info/licenses/LICENSE,sha256=pS6szQI4X7qAin1ODdoDp-XwZvsFy6ZZ28-JAwGHgzo,1070
|
|
5
|
+
piphi_runtime_kit_python/__init__.py,sha256=P8yx6C1P88xuXWuf3D58d5zhUD-pprhBhbVeyNbDaJQ,6013
|
|
6
6
|
piphi_runtime_kit_python/adapters/__init__.py,sha256=3jBhPZSBN3Qo7MKHlxAol2jPU3cK23BXHR2oTlircsw,361
|
|
7
7
|
piphi_runtime_kit_python/adapters/fastapi.py,sha256=1QcOBYprg6AmQVrkI8g61NIelBB_7BTbdvNaMvG2-s8,2098
|
|
8
8
|
piphi_runtime_kit_python/fastapi.py,sha256=OZ-ds2-g32kB4YkF90D5bM4vVVjpOWPtrNC-WdhDNyo,361
|
|
@@ -12,16 +12,17 @@ piphi_runtime_kit_python/runtime/config_sync.py,sha256=IzsH5AGxnISISWx8OYfNMTk2O
|
|
|
12
12
|
piphi_runtime_kit_python/runtime/configuration.py,sha256=4kkoJeENOhJHeNvC9QDu81ejDJUQJG7S5GmQG1sv3YQ,3631
|
|
13
13
|
piphi_runtime_kit_python/runtime/context.py,sha256=CwXgrivciBd7eTREn0u0zjqkDPy6VthASW-gsoZqCTg,1109
|
|
14
14
|
piphi_runtime_kit_python/runtime/discovery.py,sha256=uH6rJ4BnLVukTCxu2QwvN8yJhsI_fcT29VEat62oBcY,2274
|
|
15
|
-
piphi_runtime_kit_python/runtime/dispatch.py,sha256=
|
|
15
|
+
piphi_runtime_kit_python/runtime/dispatch.py,sha256=QcbA9B5izyyg0znlKHq94dbr-oqHVxKW2LCjrQKYfkY,7341
|
|
16
16
|
piphi_runtime_kit_python/runtime/errors.py,sha256=_mjqmJLsdM5NfrTwb6YFkhheqPfn6Df-QtAKgl9U6QU,4120
|
|
17
17
|
piphi_runtime_kit_python/runtime/events.py,sha256=JcvrTZlJ-vgY237jTDVOAVTbSxbcuh9u2QCdOGyuduA,6543
|
|
18
18
|
piphi_runtime_kit_python/runtime/health.py,sha256=9YO0K_UCJKTe6uSFLgb1kStr5dAgtIUc8Ha0dnRi2vA,1880
|
|
19
|
+
piphi_runtime_kit_python/runtime/identity.py,sha256=cth0_QHR9R5LTdp-6DPD1IhM8cYJT5HB1OUQYW7kLGs,2762
|
|
19
20
|
piphi_runtime_kit_python/runtime/lifespan.py,sha256=mVSdZZAS66xlcnlzAwkztFjzw_l2pyLrJqULJsb0Ezk,2850
|
|
20
21
|
piphi_runtime_kit_python/runtime/mqtt.py,sha256=JKMN5zxg1BcOidu7TaYnuRD3HuoAJ_WcvXMTTSlAteo,6069
|
|
21
|
-
piphi_runtime_kit_python/runtime/registry.py,sha256=
|
|
22
|
-
piphi_runtime_kit_python/runtime/starter.py,sha256=
|
|
22
|
+
piphi_runtime_kit_python/runtime/registry.py,sha256=8rKLCWt1_0SlDm3TepEUGDliOhVEDTv_zYR0kxLpSHM,3373
|
|
23
|
+
piphi_runtime_kit_python/runtime/starter.py,sha256=j5C4GOOcRHNLJ-HYu8fmf3bE00baDOzAfuq1MefT-Jg,4651
|
|
23
24
|
piphi_runtime_kit_python/runtime/state.py,sha256=4xvQGTT59DF9rXHAtM3kDyV_UGg2a9tLPvGiCp_PFMc,674
|
|
24
25
|
piphi_runtime_kit_python/runtime/tasks.py,sha256=gMd-2K96jMU8tCPvI12xdWxuyNt5gsLDss2QWOoM94k,1241
|
|
25
26
|
piphi_runtime_kit_python/runtime/telemetry.py,sha256=RN6SFrhmAZ7aJLyioQMBzJEFsxQgOjjA1pV5qNeaR4E,4279
|
|
26
|
-
piphi_runtime_kit_python/schemas.py,sha256=
|
|
27
|
-
piphi_runtime_kit_python-0.
|
|
27
|
+
piphi_runtime_kit_python/schemas.py,sha256=LGz53OHmWwUEQb_nHbVBgRDGVGowJPT53S2P7otP3lo,9073
|
|
28
|
+
piphi_runtime_kit_python-0.4.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|