piphi-runtime-kit-python 0.3.0__tar.gz → 0.4.1__tar.gz

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 (55) hide show
  1. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/PKG-INFO +77 -5
  2. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/README.md +76 -4
  3. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/pyproject.toml +1 -1
  4. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/__init__.py +8 -1
  5. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/starter.py +29 -0
  6. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/schemas.py +63 -1
  7. piphi_runtime_kit_python-0.4.1/tests/test_runtime_starter.py +53 -0
  8. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_schemas_matrix.py +45 -1
  9. piphi_runtime_kit_python-0.3.0/tests/test_runtime_starter.py +0 -21
  10. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/LICENSE +0 -0
  11. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/adapters/__init__.py +0 -0
  12. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/adapters/fastapi.py +0 -0
  13. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/fastapi.py +0 -0
  14. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/__init__.py +0 -0
  15. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/auth.py +0 -0
  16. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/config_sync.py +0 -0
  17. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/configuration.py +0 -0
  18. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/context.py +0 -0
  19. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/discovery.py +0 -0
  20. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/dispatch.py +0 -0
  21. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/errors.py +0 -0
  22. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/events.py +0 -0
  23. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/health.py +0 -0
  24. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/lifespan.py +0 -0
  25. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/mqtt.py +0 -0
  26. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/registry.py +0 -0
  27. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/state.py +0 -0
  28. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/tasks.py +0 -0
  29. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/src/piphi_runtime_kit_python/runtime/telemetry.py +0 -0
  30. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/__init__.py +0 -0
  31. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_config_sync.py +0 -0
  32. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_fastapi_adapter.py +0 -0
  33. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_auth.py +0 -0
  34. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_auth_matrix.py +0 -0
  35. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_config_sync_matrix.py +0 -0
  36. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_configuration.py +0 -0
  37. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_configuration_matrix.py +0 -0
  38. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_delivery_errors.py +0 -0
  39. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_discovery.py +0 -0
  40. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_discovery_matrix.py +0 -0
  41. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_dispatch.py +0 -0
  42. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_dispatch_matrix.py +0 -0
  43. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_errors_matrix.py +0 -0
  44. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_events.py +0 -0
  45. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_events_matrix.py +0 -0
  46. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_health.py +0 -0
  47. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_lifespan.py +0 -0
  48. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_mqtt.py +0 -0
  49. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_mqtt_matrix.py +0 -0
  50. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_registry.py +0 -0
  51. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_registry_matrix.py +0 -0
  52. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_support_matrix.py +0 -0
  53. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_tasks.py +0 -0
  54. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_telemetry.py +0 -0
  55. {piphi_runtime_kit_python-0.3.0 → piphi_runtime_kit_python-0.4.1}/tests/test_runtime_telemetry_matrix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piphi-runtime-kit-python
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: PiPhi Network runtime integration helpers
5
5
  Keywords: piphi,runtime,integration,iot
6
6
  Author-Email: KelvinSan <support@piphi.network>
@@ -113,18 +113,28 @@ hide vendor logic behind an overly magical abstraction.
113
113
 
114
114
  The package currently supports Python `>=3.11`.
115
115
 
116
- Install from a local checkout:
116
+ Install from PyPI:
117
117
 
118
118
  ```bash
119
- pdm add /path/to/piphi-runtime-kit-python
119
+ pdm add piphi-runtime-kit-python
120
120
  ```
121
121
 
122
122
  If you need the optional FastAPI helpers:
123
123
 
124
124
  ```bash
125
- pdm add "/path/to/piphi-runtime-kit-python[fastapi]"
125
+ pdm add "piphi-runtime-kit-python[fastapi]"
126
126
  ```
127
127
 
128
+ If you prefer `pip`:
129
+
130
+ ```bash
131
+ pip install piphi-runtime-kit-python
132
+ ```
133
+
134
+ Package page:
135
+
136
+ - https://pypi.org/project/piphi-runtime-kit-python/0.3.0/
137
+
128
138
  ## First 15 Minutes
129
139
 
130
140
  If you want the shortest path to a working runtime, do this:
@@ -160,9 +170,71 @@ These are the most common runtime routes and what they are for.
160
170
  | `/deconfigure` | `POST` | Usually | Remove one config | `RuntimeConfigRemoveResponse` |
161
171
  | `/events` | `GET` | Common | Show recent local runtime events | `build_event_list_response(...)` |
162
172
  | `/state` | `GET` | Common | Show current runtime state | `registry.entries`, `registry.state_snapshots` |
163
- | `/entities` | `GET` | Integration-specific | Show normalized entity list | integration-owned |
173
+ | `/entities` | `GET` | Integration-specific | Show normalized entity list | `build_entities_response(...)`, `starter.entities_response(...)` |
164
174
  | `/ui` or `/ui-config` | `GET` | Optional | Return config UI metadata | integration-owned |
165
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
+
166
238
  ## UI Config Endpoints
167
239
 
168
240
  Many integrations expose `/ui` or `/ui-config` so the PiPhi frontend knows how
@@ -87,18 +87,28 @@ hide vendor logic behind an overly magical abstraction.
87
87
 
88
88
  The package currently supports Python `>=3.11`.
89
89
 
90
- Install from a local checkout:
90
+ Install from PyPI:
91
91
 
92
92
  ```bash
93
- pdm add /path/to/piphi-runtime-kit-python
93
+ pdm add piphi-runtime-kit-python
94
94
  ```
95
95
 
96
96
  If you need the optional FastAPI helpers:
97
97
 
98
98
  ```bash
99
- pdm add "/path/to/piphi-runtime-kit-python[fastapi]"
99
+ pdm add "piphi-runtime-kit-python[fastapi]"
100
100
  ```
101
101
 
102
+ If you prefer `pip`:
103
+
104
+ ```bash
105
+ pip install piphi-runtime-kit-python
106
+ ```
107
+
108
+ Package page:
109
+
110
+ - https://pypi.org/project/piphi-runtime-kit-python/0.3.0/
111
+
102
112
  ## First 15 Minutes
103
113
 
104
114
  If you want the shortest path to a working runtime, do this:
@@ -134,9 +144,71 @@ These are the most common runtime routes and what they are for.
134
144
  | `/deconfigure` | `POST` | Usually | Remove one config | `RuntimeConfigRemoveResponse` |
135
145
  | `/events` | `GET` | Common | Show recent local runtime events | `build_event_list_response(...)` |
136
146
  | `/state` | `GET` | Common | Show current runtime state | `registry.entries`, `registry.state_snapshots` |
137
- | `/entities` | `GET` | Integration-specific | Show normalized entity list | integration-owned |
147
+ | `/entities` | `GET` | Integration-specific | Show normalized entity list | `build_entities_response(...)`, `starter.entities_response(...)` |
138
148
  | `/ui` or `/ui-config` | `GET` | Optional | Return config UI metadata | integration-owned |
139
149
 
150
+ ## Modeling `/entities`
151
+
152
+ For simple integrations, `/entities` can still be a plain list of generic
153
+ entities.
154
+
155
+ For smart-home and multi-device integrations, PiPhi now recommends a richer
156
+ runtime-owned entity shape that ties each entity to a real saved config or
157
+ device. That lets Core make better device-first dashboard suggestions, and it
158
+ gives the frontend enough context to render better widget defaults for plugs,
159
+ bulbs, thermostats, and other device-specific entities.
160
+
161
+ Recommended fields:
162
+
163
+ - `id`: stable runtime entity id
164
+ - `name`: user-facing label
165
+ - `capabilities`: actual capabilities for that specific device
166
+ - `config_id` / `configId`: PiPhi config UUID when available
167
+ - `device_id` / `deviceId`: integration-native device id
168
+ - `device_type` / `device_class`: values like `plug`, `bulb`, `sensor`, `climate`
169
+ - `entity_type`: values like `switch`, `light`, `sensor`, `media`
170
+ - `dashboard.allowed_widgets`, `dashboard.default_widget`, `dashboard.recommended_widgets`: optional UI hints
171
+
172
+ The SDK now includes `RuntimeEntityResponse`, `RuntimeEntitiesResponse`,
173
+ `build_entities_response(...)`, and `starter.entities_response(...)` to make
174
+ that payload easier to return.
175
+
176
+ The helper returns the standard wrapper shape:
177
+
178
+ - `entities`: the runtime-owned list you generated
179
+ - `capabilities`: optional manifest capability metadata
180
+ - `commands`: optional manifest command metadata
181
+
182
+ ```python
183
+ from piphi_runtime_kit_python import build_entities_response
184
+
185
+ @app.get("/entities")
186
+ async def entities() -> dict[str, Any]:
187
+ return build_entities_response(
188
+ entities=[
189
+ {
190
+ "id": "office-plug",
191
+ "name": "Office Plug",
192
+ "config_id": "core-config-uuid",
193
+ "device_id": "office-plug",
194
+ "device_class": "plug",
195
+ "entity_type": "switch",
196
+ "capabilities": ["switch", "power", "energy_today"],
197
+ "dashboard": {
198
+ "allowed_widgets": ["tile", "button", "stat"],
199
+ "default_widget": "tile",
200
+ },
201
+ }
202
+ ],
203
+ capabilities=manifest["capabilities"],
204
+ commands=manifest.get("commands", {}),
205
+ ).model_dump(exclude_none=True)
206
+ ```
207
+
208
+ If you are already using the starter object, the same response can be built with
209
+ `starter.entities_response(...)` instead of calling the standalone helper
210
+ directly.
211
+
140
212
  ## UI Config Endpoints
141
213
 
142
214
  Many integrations expose `/ui` or `/ui-config` so the PiPhi frontend knows how
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "piphi-runtime-kit-python"
3
- version = "0.3.0"
3
+ version = "0.4.1"
4
4
  description = "PiPhi Network runtime integration helpers"
5
5
  authors = [
6
6
  { name = "KelvinSan", email = "support@piphi.network" },
@@ -70,7 +70,7 @@ from .runtime.mqtt import (
70
70
  )
71
71
  from .runtime.registry import RuntimeRegistry
72
72
  from .runtime.state import RuntimeProcessState
73
- from .runtime.starter import RuntimeStarter, create_runtime_starter
73
+ from .runtime.starter import RuntimeStarter, build_entities_response, create_runtime_starter
74
74
  from .runtime.tasks import create_tracked_task, shutdown_background_tasks, track_background_task
75
75
  from .runtime.telemetry import TelemetryClient, build_core_auth_headers
76
76
  from .schemas import (
@@ -80,6 +80,9 @@ from .schemas import (
80
80
  IntegrationCommandRequest,
81
81
  IntegrationDiscoveryRequest,
82
82
  IntegrationDiscoveryResponse,
83
+ RuntimeEntityDashboardResponse,
84
+ RuntimeEntitiesResponse,
85
+ RuntimeEntityResponse,
83
86
  IntegrationEventIngestResponse,
84
87
  IntegrationEventListResponse,
85
88
  IntegrationEventRequest,
@@ -110,6 +113,9 @@ __all__ = [
110
113
  "IntegrationCommandRequest",
111
114
  "IntegrationDiscoveryRequest",
112
115
  "IntegrationDiscoveryResponse",
116
+ "RuntimeEntityDashboardResponse",
117
+ "RuntimeEntitiesResponse",
118
+ "RuntimeEntityResponse",
113
119
  "IntegrationEventRequest",
114
120
  "RuntimeAuthContext",
115
121
  "RuntimeAuthHeaders",
@@ -141,6 +147,7 @@ __all__ = [
141
147
  "build_core_auth_headers",
142
148
  "build_config_apply_response",
143
149
  "build_config_remove_response",
150
+ "build_entities_response",
144
151
  "build_runtime_auth_headers",
145
152
  "build_runtime_diagnostics_response",
146
153
  "build_runtime_health_response",
@@ -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,7 +6,7 @@ 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):
@@ -74,6 +74,68 @@ class IntegrationCommandRequest(BaseModel):
74
74
  args: dict[str, Any] = Field(default_factory=dict)
75
75
 
76
76
 
77
+ class RuntimeEntityDashboardResponse(BaseModel):
78
+ """Optional per-entity dashboard hints returned by a runtime."""
79
+
80
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
81
+
82
+ allowed_widgets: list[str] = Field(
83
+ default_factory=list,
84
+ validation_alias=AliasChoices("allowed_widgets", "allowedWidgets"),
85
+ )
86
+ default_widget: str | None = Field(
87
+ default=None,
88
+ validation_alias=AliasChoices("default_widget", "defaultWidget"),
89
+ )
90
+ recommended_widgets: list[str] = Field(
91
+ default_factory=list,
92
+ validation_alias=AliasChoices("recommended_widgets", "recommendedWidgets"),
93
+ )
94
+ metadata: dict[str, Any] = Field(default_factory=dict)
95
+
96
+
97
+ class RuntimeEntityResponse(BaseModel):
98
+ """One live runtime entity tied to a configured device/account."""
99
+
100
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
101
+
102
+ id: str
103
+ name: str
104
+ capabilities: list[str] = Field(default_factory=list)
105
+ config_id: str | None = Field(
106
+ default=None,
107
+ validation_alias=AliasChoices("config_id", "configId"),
108
+ )
109
+ device_id: str | None = Field(
110
+ default=None,
111
+ validation_alias=AliasChoices("device_id", "deviceId"),
112
+ )
113
+ device_type: str | None = Field(
114
+ default=None,
115
+ validation_alias=AliasChoices("device_type", "deviceType"),
116
+ )
117
+ device_class: str | None = Field(
118
+ default=None,
119
+ validation_alias=AliasChoices("device_class", "deviceClass"),
120
+ )
121
+ entity_type: str | None = Field(
122
+ default=None,
123
+ validation_alias=AliasChoices("entity_type", "entityType"),
124
+ )
125
+ dashboard: RuntimeEntityDashboardResponse | None = None
126
+ metadata: dict[str, Any] = Field(default_factory=dict)
127
+
128
+
129
+ class RuntimeEntitiesResponse(BaseModel):
130
+ """Standard response body returned by runtime /entities endpoints."""
131
+
132
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
133
+
134
+ entities: list[RuntimeEntityResponse] = Field(default_factory=list)
135
+ capabilities: dict[str, Any] = Field(default_factory=dict)
136
+ commands: dict[str, Any] = Field(default_factory=dict)
137
+
138
+
77
139
  class IntegrationEventRequest(BaseModel):
78
140
  """Generic event payload emitted by a runtime integration."""
79
141
  event_type: str
@@ -0,0 +1,53 @@
1
+ from piphi_runtime_kit_python import (
2
+ RuntimeEntitiesResponse,
3
+ RuntimeStarter,
4
+ create_runtime_starter,
5
+ )
6
+
7
+
8
+ def test_create_runtime_starter_bundles_common_runtime_parts() -> None:
9
+ starter = create_runtime_starter(
10
+ integration_id="demo-runtime",
11
+ integration_name="Demo Runtime",
12
+ version="0.1.0",
13
+ max_recent_events=25,
14
+ )
15
+
16
+ assert isinstance(starter, RuntimeStarter)
17
+ assert starter.integration_metadata == {
18
+ "id": "demo-runtime",
19
+ "name": "Demo Runtime",
20
+ "version": "0.1.0",
21
+ }
22
+ assert starter.registry.max_recent_events == 25
23
+ assert starter.telemetry_client.process_state is starter.runtime.process_state
24
+ assert starter.event_client.process_state is starter.runtime.process_state
25
+ assert starter.config_sync.process_state is starter.runtime.process_state
26
+
27
+
28
+ def test_runtime_starter_entities_response_wraps_runtime_entities() -> None:
29
+ starter = create_runtime_starter(
30
+ integration_id="demo-runtime",
31
+ integration_name="Demo Runtime",
32
+ )
33
+
34
+ response = starter.entities_response(
35
+ entities=[
36
+ {
37
+ "id": "office-plug",
38
+ "name": "Office Plug",
39
+ "capabilities": ["switch", "power"],
40
+ "config_id": "cfg-1",
41
+ "device_id": "office-plug",
42
+ "device_class": "plug",
43
+ }
44
+ ],
45
+ capabilities={"switch": {"kind": "action"}},
46
+ commands={"turn_on": {"description": "Turn on"}},
47
+ )
48
+
49
+ assert isinstance(response, RuntimeEntitiesResponse)
50
+ assert response.entities[0].name == "Office Plug"
51
+ assert response.entities[0].device_class == "plug"
52
+ assert response.capabilities == {"switch": {"kind": "action"}}
53
+ assert response.commands == {"turn_on": {"description": "Turn on"}}
@@ -16,6 +16,9 @@ from piphi_runtime_kit_python import (
16
16
  RuntimeConfigSnapshot,
17
17
  RuntimeConfigSyncResponse,
18
18
  RuntimeDiagnosticsResponse,
19
+ RuntimeEntitiesResponse,
20
+ RuntimeEntityDashboardResponse,
21
+ RuntimeEntityResponse,
19
22
  RuntimeHealthResponse,
20
23
  TelemetryPayload,
21
24
  )
@@ -72,6 +75,48 @@ def test_integration_event_request_defaults_payload() -> None:
72
75
  assert request.payload == {}
73
76
 
74
77
 
78
+ def test_runtime_entity_models_support_aliases_and_defaults() -> None:
79
+ entity = RuntimeEntityResponse.model_validate(
80
+ {
81
+ "id": "office-plug",
82
+ "name": "Office Plug",
83
+ "capabilities": ["switch", "power"],
84
+ "configId": "cfg-1",
85
+ "deviceId": "office-plug",
86
+ "deviceClass": "plug",
87
+ "entityType": "switch",
88
+ "dashboard": {
89
+ "allowedWidgets": ["tile", "stat"],
90
+ "defaultWidget": "tile",
91
+ "recommendedWidgets": ["tile"],
92
+ },
93
+ }
94
+ )
95
+
96
+ assert entity.config_id == "cfg-1"
97
+ assert entity.device_id == "office-plug"
98
+ assert entity.device_class == "plug"
99
+ assert entity.entity_type == "switch"
100
+ assert entity.dashboard is not None
101
+ assert entity.dashboard.allowed_widgets == ["tile", "stat"]
102
+ assert entity.dashboard.default_widget == "tile"
103
+ assert entity.dashboard.recommended_widgets == ["tile"]
104
+
105
+
106
+ def test_runtime_entities_response_defaults_cleanly() -> None:
107
+ response = RuntimeEntitiesResponse()
108
+ assert response.entities == []
109
+ assert response.capabilities == {}
110
+ assert response.commands == {}
111
+
112
+
113
+ def test_runtime_entity_dashboard_defaults_metadata() -> None:
114
+ dashboard = RuntimeEntityDashboardResponse()
115
+ assert dashboard.allowed_widgets == []
116
+ assert dashboard.recommended_widgets == []
117
+ assert dashboard.metadata == {}
118
+
119
+
75
120
  def test_event_list_and_ingest_responses_default_cleanly() -> None:
76
121
  assert IntegrationEventListResponse().events == []
77
122
  assert IntegrationEventIngestResponse().status == "accepted"
@@ -121,4 +166,3 @@ def test_core_event_payload_model_dump_json_serializes_enums_and_datetimes() ->
121
166
  assert dumped["severity"] == "error"
122
167
  assert dumped["transport"] == "mqtt"
123
168
  assert dumped["ts"].endswith("Z")
124
-
@@ -1,21 +0,0 @@
1
- from piphi_runtime_kit_python import RuntimeStarter, create_runtime_starter
2
-
3
-
4
- def test_create_runtime_starter_bundles_common_runtime_parts() -> None:
5
- starter = create_runtime_starter(
6
- integration_id="demo-runtime",
7
- integration_name="Demo Runtime",
8
- version="0.1.0",
9
- max_recent_events=25,
10
- )
11
-
12
- assert isinstance(starter, RuntimeStarter)
13
- assert starter.integration_metadata == {
14
- "id": "demo-runtime",
15
- "name": "Demo Runtime",
16
- "version": "0.1.0",
17
- }
18
- assert starter.registry.max_recent_events == 25
19
- assert starter.telemetry_client.process_state is starter.runtime.process_state
20
- assert starter.event_client.process_state is starter.runtime.process_state
21
- assert starter.config_sync.process_state is starter.runtime.process_state