phystack-hub-client 0.2.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.
- phystack/hub_client/__init__.py +90 -0
- phystack/hub_client/client.py +701 -0
- phystack/hub_client/instances/__init__.py +11 -0
- phystack/hub_client/instances/base_instance.py +225 -0
- phystack/hub_client/instances/edge_instance.py +212 -0
- phystack/hub_client/instances/peripheral_instance.py +186 -0
- phystack/hub_client/services/__init__.py +5 -0
- phystack/hub_client/services/phyhub_connection.py +272 -0
- phystack/hub_client/services/webrtc/__init__.py +23 -0
- phystack/hub_client/services/webrtc/data_channel.py +527 -0
- phystack/hub_client/services/webrtc/media_stream.py +571 -0
- phystack/hub_client/twin_messaging.py +287 -0
- phystack/hub_client/twin_registry.py +158 -0
- phystack/hub_client/types/__init__.py +15 -0
- phystack/hub_client/types/twin_types.py +136 -0
- phystack_hub_client-0.2.0.dist-info/METADATA +174 -0
- phystack_hub_client-0.2.0.dist-info/RECORD +18 -0
- phystack_hub_client-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Base instance class with common functionality for all twin types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
6
|
+
|
|
7
|
+
from phystack.hub_client.types.twin_types import (
|
|
8
|
+
TwinMessageResult,
|
|
9
|
+
TwinProperties,
|
|
10
|
+
TwinResponse,
|
|
11
|
+
)
|
|
12
|
+
from phystack.hub_client.twin_messaging import (
|
|
13
|
+
TwinMessaging,
|
|
14
|
+
TargetedEmitter,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from phystack.hub_client.client import PhyHubClient
|
|
19
|
+
from phystack.hub_client.services.webrtc.data_channel import (
|
|
20
|
+
PhygridDataChannel,
|
|
21
|
+
DataChannelOptions,
|
|
22
|
+
)
|
|
23
|
+
from phystack.hub_client.services.webrtc.media_stream import (
|
|
24
|
+
PhygridMediaStream,
|
|
25
|
+
MediaStreamOptions,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BaseInstance:
|
|
30
|
+
"""Base class for all twin instances with common messaging and WebRTC."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
twin_id: str,
|
|
35
|
+
device_id: str,
|
|
36
|
+
client: "PhyHubClient",
|
|
37
|
+
messaging: TwinMessaging,
|
|
38
|
+
twin_data: Optional[dict[str, Any]] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize base instance.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
twin_id: ID of this twin.
|
|
44
|
+
device_id: ID of the parent device.
|
|
45
|
+
client: PhyHubClient for API calls.
|
|
46
|
+
messaging: TwinMessaging instance for emit/on/to.
|
|
47
|
+
twin_data: Optional cached twin data.
|
|
48
|
+
"""
|
|
49
|
+
self.id = twin_id
|
|
50
|
+
self.device_id = device_id
|
|
51
|
+
self._client = client
|
|
52
|
+
self._messaging = messaging
|
|
53
|
+
self._twin_data = twin_data or {}
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def properties(self) -> TwinProperties:
|
|
57
|
+
"""Access twin properties (desired/reported)."""
|
|
58
|
+
props = self._twin_data.get("properties", {})
|
|
59
|
+
return TwinProperties.from_dict(props)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def tenant_id(self) -> str:
|
|
63
|
+
"""Get tenant ID."""
|
|
64
|
+
return self._twin_data.get("tenantId", "")
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def type(self) -> str:
|
|
68
|
+
"""Get twin type."""
|
|
69
|
+
return self._twin_data.get("type", "")
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def status(self) -> str:
|
|
73
|
+
"""Get twin status."""
|
|
74
|
+
return self._twin_data.get("status", "")
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def descriptors(self) -> dict[str, str]:
|
|
78
|
+
"""Get twin descriptors."""
|
|
79
|
+
return self._twin_data.get("descriptors", {})
|
|
80
|
+
|
|
81
|
+
def _update_twin_data(self, data: dict[str, Any]) -> None:
|
|
82
|
+
"""Update cached twin data."""
|
|
83
|
+
self._twin_data = {**self._twin_data, **data}
|
|
84
|
+
|
|
85
|
+
# =========================================================================
|
|
86
|
+
# Messaging API (delegated to TwinMessaging)
|
|
87
|
+
# =========================================================================
|
|
88
|
+
|
|
89
|
+
def emit(self, event_type: str, payload: Any = None) -> None:
|
|
90
|
+
"""Fire-and-forget broadcast to subscribers.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
event_type: Type of event to emit.
|
|
94
|
+
payload: Data to send.
|
|
95
|
+
"""
|
|
96
|
+
self._messaging.emit(event_type, payload)
|
|
97
|
+
|
|
98
|
+
def on(
|
|
99
|
+
self,
|
|
100
|
+
event_type: str,
|
|
101
|
+
callback: Callable[[Any, Optional[Callable[[TwinMessageResult], None]]], None],
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Listen for events, with optional respond callback for request-response.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
event_type: Type of event to listen for.
|
|
107
|
+
callback: Handler function (data, respond?) => None
|
|
108
|
+
"""
|
|
109
|
+
self._messaging.on(event_type, callback)
|
|
110
|
+
|
|
111
|
+
def to(self, target_twin_id: str) -> TargetedEmitter:
|
|
112
|
+
"""Target a specific twin for messaging.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
target_twin_id: ID of twin to target.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
TargetedEmitter for the specified twin.
|
|
119
|
+
"""
|
|
120
|
+
return self._messaging.to(target_twin_id)
|
|
121
|
+
|
|
122
|
+
# =========================================================================
|
|
123
|
+
# WebRTC API (delegated to client)
|
|
124
|
+
# =========================================================================
|
|
125
|
+
|
|
126
|
+
async def get_data_channel(
|
|
127
|
+
self,
|
|
128
|
+
target_twin_id: str,
|
|
129
|
+
channel_name: str = "default",
|
|
130
|
+
options: Optional["DataChannelOptions"] = None,
|
|
131
|
+
) -> "PhygridDataChannel":
|
|
132
|
+
"""Create a WebRTC DataChannel to another twin.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
target_twin_id: ID of the target twin.
|
|
136
|
+
channel_name: Name for the data channel.
|
|
137
|
+
options: Optional DataChannel configuration.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
PhygridDataChannel for sending/receiving data.
|
|
141
|
+
"""
|
|
142
|
+
return await self._client.create_data_channel(
|
|
143
|
+
target_twin_id=target_twin_id,
|
|
144
|
+
is_initiator=True,
|
|
145
|
+
options=options,
|
|
146
|
+
channel_name=channel_name,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def on_data_channel(
|
|
150
|
+
self,
|
|
151
|
+
target_twin_id: str,
|
|
152
|
+
callback: Callable[["PhygridDataChannel"], None],
|
|
153
|
+
channel_name: str = "default",
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Listen for incoming DataChannel from a twin.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
target_twin_id: ID of the twin to listen from.
|
|
159
|
+
callback: Handler for incoming channel.
|
|
160
|
+
channel_name: Name of the channel to listen for.
|
|
161
|
+
"""
|
|
162
|
+
channel = await self._client.create_data_channel(
|
|
163
|
+
target_twin_id=target_twin_id,
|
|
164
|
+
is_initiator=False,
|
|
165
|
+
channel_name=channel_name,
|
|
166
|
+
blocking=False,
|
|
167
|
+
)
|
|
168
|
+
# Call callback when channel is opened
|
|
169
|
+
if channel.is_open():
|
|
170
|
+
callback(channel)
|
|
171
|
+
else:
|
|
172
|
+
original_on_open = None
|
|
173
|
+
|
|
174
|
+
def on_open() -> None:
|
|
175
|
+
callback(channel)
|
|
176
|
+
|
|
177
|
+
channel._handler._on_connected = on_open
|
|
178
|
+
|
|
179
|
+
async def get_media_stream(
|
|
180
|
+
self,
|
|
181
|
+
target_twin_id: str,
|
|
182
|
+
stream_name: str = "default",
|
|
183
|
+
options: Optional["MediaStreamOptions"] = None,
|
|
184
|
+
) -> "PhygridMediaStream":
|
|
185
|
+
"""Create a WebRTC MediaStream to another twin.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
target_twin_id: ID of the target twin.
|
|
189
|
+
stream_name: Name for the media stream.
|
|
190
|
+
options: Optional MediaStream configuration.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
PhygridMediaStream for video/audio streaming.
|
|
194
|
+
"""
|
|
195
|
+
return await self._client.create_media_stream(
|
|
196
|
+
target_twin_id=target_twin_id,
|
|
197
|
+
is_initiator=True,
|
|
198
|
+
options=options,
|
|
199
|
+
stream_name=stream_name,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def on_media_stream(
|
|
203
|
+
self,
|
|
204
|
+
target_twin_id: str,
|
|
205
|
+
callback: Callable[["PhygridMediaStream"], None],
|
|
206
|
+
stream_name: str = "default",
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Listen for incoming MediaStream from a twin.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
target_twin_id: ID of the twin to listen from.
|
|
212
|
+
callback: Handler for incoming stream.
|
|
213
|
+
stream_name: Name of the stream to listen for.
|
|
214
|
+
"""
|
|
215
|
+
stream = await self._client.create_media_stream(
|
|
216
|
+
target_twin_id=target_twin_id,
|
|
217
|
+
is_initiator=False,
|
|
218
|
+
stream_name=stream_name,
|
|
219
|
+
blocking=False,
|
|
220
|
+
)
|
|
221
|
+
# Call callback when stream is opened
|
|
222
|
+
if stream.is_open():
|
|
223
|
+
callback(stream)
|
|
224
|
+
else:
|
|
225
|
+
stream._handler._on_connected = lambda: callback(stream)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""EdgeInstance class for edge twin management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Set
|
|
7
|
+
|
|
8
|
+
from phystack.hub_client.instances.base_instance import BaseInstance
|
|
9
|
+
from phystack.hub_client.types.twin_types import (
|
|
10
|
+
TwinResponse,
|
|
11
|
+
TwinTypeEnum,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from phystack.hub_client.client import PhyHubClient
|
|
16
|
+
from phystack.hub_client.twin_messaging import TwinMessaging
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EdgeInstance(BaseInstance):
|
|
20
|
+
"""Edge twin instance with peripheral and settings management."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
twin_id: str,
|
|
25
|
+
device_id: str,
|
|
26
|
+
client: "PhyHubClient",
|
|
27
|
+
messaging: "TwinMessaging",
|
|
28
|
+
twin_data: Optional[dict[str, Any]] = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Initialize edge instance.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
twin_id: ID of this edge twin.
|
|
34
|
+
device_id: ID of the parent device.
|
|
35
|
+
client: PhyHubClient for API calls.
|
|
36
|
+
messaging: TwinMessaging instance for emit/on/to.
|
|
37
|
+
twin_data: Optional cached twin data.
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(twin_id, device_id, client, messaging, twin_data)
|
|
40
|
+
self._update_reported_listeners: Set[Callable[[dict[str, Any]], None]] = set()
|
|
41
|
+
self._update_desired_listeners: Set[Callable[[dict[str, Any]], None]] = set()
|
|
42
|
+
self._previous_reported: Optional[str] = None
|
|
43
|
+
self._previous_desired: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
# =========================================================================
|
|
46
|
+
# Properties API
|
|
47
|
+
# =========================================================================
|
|
48
|
+
|
|
49
|
+
async def update_reported(self, properties: dict[str, Any]) -> dict[str, Any]:
|
|
50
|
+
"""Update reported properties for this edge twin.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
properties: Properties to update.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Updated twin data.
|
|
57
|
+
"""
|
|
58
|
+
result = await self._client.update_reported_properties(self.id, properties)
|
|
59
|
+
if result:
|
|
60
|
+
self._update_twin_data(result)
|
|
61
|
+
return result or {}
|
|
62
|
+
|
|
63
|
+
def on_update_reported(
|
|
64
|
+
self, callback: Callable[[dict[str, Any]], None]
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Subscribe to reported property changes.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
callback: Handler called with new reported properties when they change.
|
|
70
|
+
"""
|
|
71
|
+
self._update_reported_listeners.add(callback)
|
|
72
|
+
|
|
73
|
+
# Set up twin update listener if not already set
|
|
74
|
+
self._setup_property_listeners()
|
|
75
|
+
|
|
76
|
+
def on_update_desired(
|
|
77
|
+
self, callback: Callable[[dict[str, Any]], None]
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Subscribe to desired property changes.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
callback: Handler called with new desired properties when they change.
|
|
83
|
+
"""
|
|
84
|
+
self._update_desired_listeners.add(callback)
|
|
85
|
+
|
|
86
|
+
# Set up twin update listener if not already set
|
|
87
|
+
self._setup_property_listeners()
|
|
88
|
+
|
|
89
|
+
def _setup_property_listeners(self) -> None:
|
|
90
|
+
"""Set up twin update listener for property change detection."""
|
|
91
|
+
|
|
92
|
+
def on_twin_update(twin_data: dict[str, Any]) -> None:
|
|
93
|
+
if twin_data.get("id") != self.id:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
props = twin_data.get("properties", {})
|
|
97
|
+
|
|
98
|
+
# Check reported changes
|
|
99
|
+
reported = props.get("reported", {})
|
|
100
|
+
reported_str = json.dumps(reported, sort_keys=True)
|
|
101
|
+
if self._previous_reported is None:
|
|
102
|
+
self._previous_reported = reported_str
|
|
103
|
+
for listener in self._update_reported_listeners:
|
|
104
|
+
try:
|
|
105
|
+
listener(reported)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"[EdgeInstance] Error in reported listener: {e}")
|
|
108
|
+
elif reported_str != self._previous_reported:
|
|
109
|
+
self._previous_reported = reported_str
|
|
110
|
+
for listener in self._update_reported_listeners:
|
|
111
|
+
try:
|
|
112
|
+
listener(reported)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"[EdgeInstance] Error in reported listener: {e}")
|
|
115
|
+
|
|
116
|
+
# Check desired changes
|
|
117
|
+
desired = props.get("desired", {})
|
|
118
|
+
desired_str = json.dumps(desired, sort_keys=True)
|
|
119
|
+
if self._previous_desired is None:
|
|
120
|
+
self._previous_desired = desired_str
|
|
121
|
+
for listener in self._update_desired_listeners:
|
|
122
|
+
try:
|
|
123
|
+
listener(desired)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
print(f"[EdgeInstance] Error in desired listener: {e}")
|
|
126
|
+
elif desired_str != self._previous_desired:
|
|
127
|
+
self._previous_desired = desired_str
|
|
128
|
+
for listener in self._update_desired_listeners:
|
|
129
|
+
try:
|
|
130
|
+
listener(desired)
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"[EdgeInstance] Error in desired listener: {e}")
|
|
133
|
+
|
|
134
|
+
# Update cached twin data
|
|
135
|
+
self._update_twin_data(twin_data)
|
|
136
|
+
|
|
137
|
+
self._client.on_twin_update(self.id, on_twin_update)
|
|
138
|
+
|
|
139
|
+
# =========================================================================
|
|
140
|
+
# Peripheral Twin Management
|
|
141
|
+
# =========================================================================
|
|
142
|
+
|
|
143
|
+
async def create_peripheral_twin(
|
|
144
|
+
self,
|
|
145
|
+
name: str,
|
|
146
|
+
hardware_id: str,
|
|
147
|
+
desired_properties: Optional[dict[str, Any]] = None,
|
|
148
|
+
descriptors: Optional[dict[str, str]] = None,
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
"""Create a new peripheral twin associated with this edge.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
name: Name for the peripheral.
|
|
154
|
+
hardware_id: Unique hardware identifier.
|
|
155
|
+
desired_properties: Optional initial desired properties.
|
|
156
|
+
descriptors: Optional descriptors for the peripheral.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Created peripheral twin data.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
ValueError: If peripheral with same hardware_id already exists.
|
|
163
|
+
"""
|
|
164
|
+
# Check for existing peripheral with same hardware_id
|
|
165
|
+
existing = await self.get_peripheral_twins()
|
|
166
|
+
for twin in existing:
|
|
167
|
+
props = twin.get("properties", {}).get("desired", {})
|
|
168
|
+
if props.get("hardwareId") == hardware_id:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"Peripheral with hardwareId {hardware_id} already exists"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
payload = {
|
|
174
|
+
"data": {
|
|
175
|
+
"deviceId": self.device_id,
|
|
176
|
+
"tenantId": self.tenant_id,
|
|
177
|
+
"instanceId": self.id,
|
|
178
|
+
"peripheralName": name,
|
|
179
|
+
"hardwareId": hardware_id,
|
|
180
|
+
"desiredProperties": desired_properties,
|
|
181
|
+
"descriptors": descriptors,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
result = await self._client._connection.emit(
|
|
186
|
+
"createPeripheralTwin", payload, callback=True
|
|
187
|
+
)
|
|
188
|
+
return result.get("twin", {}) if result else {}
|
|
189
|
+
|
|
190
|
+
async def get_peripheral_twins(self) -> list[dict[str, Any]]:
|
|
191
|
+
"""Get all peripheral twins for this edge instance.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List of peripheral twin data.
|
|
195
|
+
"""
|
|
196
|
+
payload = {"data": {"instanceId": self.id}}
|
|
197
|
+
result = await self._client._connection.emit(
|
|
198
|
+
"getPeripheralTwins", payload, callback=True
|
|
199
|
+
)
|
|
200
|
+
return result.get("twins", []) if result else []
|
|
201
|
+
|
|
202
|
+
# =========================================================================
|
|
203
|
+
# Settings API
|
|
204
|
+
# =========================================================================
|
|
205
|
+
|
|
206
|
+
def get_settings(self) -> dict[str, Any]:
|
|
207
|
+
"""Get settings from desired properties.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Settings dictionary.
|
|
211
|
+
"""
|
|
212
|
+
return self.properties.desired.get("settings", {})
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""PeripheralInstance class for peripheral twin management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Set
|
|
7
|
+
|
|
8
|
+
from phystack.hub_client.instances.base_instance import BaseInstance
|
|
9
|
+
from phystack.hub_client.types.twin_types import TwinResponse
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from phystack.hub_client.client import PhyHubClient
|
|
13
|
+
from phystack.hub_client.instances.edge_instance import EdgeInstance
|
|
14
|
+
from phystack.hub_client.twin_messaging import TwinMessaging
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PeripheralInstance(BaseInstance):
|
|
18
|
+
"""Peripheral twin instance with full property management."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
twin_id: str,
|
|
23
|
+
device_id: str,
|
|
24
|
+
client: "PhyHubClient",
|
|
25
|
+
messaging: "TwinMessaging",
|
|
26
|
+
edge_instance: "EdgeInstance",
|
|
27
|
+
twin_data: Optional[dict[str, Any]] = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Initialize peripheral instance.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
twin_id: ID of this peripheral twin.
|
|
33
|
+
device_id: ID of the parent device.
|
|
34
|
+
client: PhyHubClient for API calls.
|
|
35
|
+
messaging: TwinMessaging instance for emit/on/to.
|
|
36
|
+
edge_instance: Parent edge instance.
|
|
37
|
+
twin_data: Optional cached twin data.
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(twin_id, device_id, client, messaging, twin_data)
|
|
40
|
+
self._edge_instance = edge_instance
|
|
41
|
+
self._update_reported_listeners: Set[Callable[[dict[str, Any]], None]] = set()
|
|
42
|
+
self._update_desired_listeners: Set[Callable[[dict[str, Any]], None]] = set()
|
|
43
|
+
self._previous_reported: Optional[str] = None
|
|
44
|
+
self._previous_desired: Optional[str] = None
|
|
45
|
+
self._listener_setup = False
|
|
46
|
+
|
|
47
|
+
# =========================================================================
|
|
48
|
+
# Properties API
|
|
49
|
+
# =========================================================================
|
|
50
|
+
|
|
51
|
+
async def update_reported(self, properties: dict[str, Any]) -> dict[str, Any]:
|
|
52
|
+
"""Update reported properties for this peripheral twin.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
properties: Properties to update.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Updated twin data.
|
|
59
|
+
"""
|
|
60
|
+
result = await self._client.update_reported_properties(self.id, properties)
|
|
61
|
+
if result:
|
|
62
|
+
self._update_twin_data(result)
|
|
63
|
+
return result or {}
|
|
64
|
+
|
|
65
|
+
async def update_desired(self, properties: dict[str, Any]) -> dict[str, Any]:
|
|
66
|
+
"""Update desired properties for this peripheral twin.
|
|
67
|
+
|
|
68
|
+
Peripherals can update their own desired properties unlike edge twins.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
properties: Properties to update.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Updated twin data.
|
|
75
|
+
"""
|
|
76
|
+
payload = {
|
|
77
|
+
"twinId": self.id,
|
|
78
|
+
"data": properties,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
result = await self._client._connection.emit(
|
|
82
|
+
"updatePeripheralTwinDesired", payload, callback=True
|
|
83
|
+
)
|
|
84
|
+
twin_data = result.get("twin", {}) if result else {}
|
|
85
|
+
if twin_data:
|
|
86
|
+
self._update_twin_data(twin_data)
|
|
87
|
+
return twin_data
|
|
88
|
+
|
|
89
|
+
def on_update_reported(
|
|
90
|
+
self, callback: Callable[[dict[str, Any]], None]
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Subscribe to reported property changes.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
callback: Handler called with new reported properties when they change.
|
|
96
|
+
"""
|
|
97
|
+
self._update_reported_listeners.add(callback)
|
|
98
|
+
self._setup_property_listeners()
|
|
99
|
+
|
|
100
|
+
def on_update_desired(
|
|
101
|
+
self, callback: Callable[[dict[str, Any]], None]
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Subscribe to desired property changes.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
callback: Handler called with new desired properties when they change.
|
|
107
|
+
"""
|
|
108
|
+
self._update_desired_listeners.add(callback)
|
|
109
|
+
self._setup_property_listeners()
|
|
110
|
+
|
|
111
|
+
def _setup_property_listeners(self) -> None:
|
|
112
|
+
"""Set up twin update listener for property change detection."""
|
|
113
|
+
if self._listener_setup:
|
|
114
|
+
return
|
|
115
|
+
self._listener_setup = True
|
|
116
|
+
|
|
117
|
+
def on_twin_update(twin_data: dict[str, Any]) -> None:
|
|
118
|
+
# Only process updates for this specific peripheral
|
|
119
|
+
if twin_data.get("id") != self.id:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
props = twin_data.get("properties", {})
|
|
123
|
+
|
|
124
|
+
# Check reported changes
|
|
125
|
+
reported = props.get("reported", {})
|
|
126
|
+
if reported:
|
|
127
|
+
reported_str = json.dumps(reported, sort_keys=True)
|
|
128
|
+
if self._previous_reported is None:
|
|
129
|
+
# First update - always trigger
|
|
130
|
+
self._previous_reported = reported_str
|
|
131
|
+
for listener in self._update_reported_listeners:
|
|
132
|
+
try:
|
|
133
|
+
listener(reported)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"[PeripheralInstance] Error in reported listener: {e}")
|
|
136
|
+
elif reported_str != self._previous_reported:
|
|
137
|
+
self._previous_reported = reported_str
|
|
138
|
+
for listener in self._update_reported_listeners:
|
|
139
|
+
try:
|
|
140
|
+
listener(reported)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f"[PeripheralInstance] Error in reported listener: {e}")
|
|
143
|
+
|
|
144
|
+
# Check desired changes
|
|
145
|
+
desired = props.get("desired", {})
|
|
146
|
+
if desired:
|
|
147
|
+
desired_str = json.dumps(desired, sort_keys=True)
|
|
148
|
+
if self._previous_desired is None:
|
|
149
|
+
# First update - always trigger
|
|
150
|
+
self._previous_desired = desired_str
|
|
151
|
+
for listener in self._update_desired_listeners:
|
|
152
|
+
try:
|
|
153
|
+
listener(desired)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
print(f"[PeripheralInstance] Error in desired listener: {e}")
|
|
156
|
+
elif desired_str != self._previous_desired:
|
|
157
|
+
self._previous_desired = desired_str
|
|
158
|
+
for listener in self._update_desired_listeners:
|
|
159
|
+
try:
|
|
160
|
+
listener(desired)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
print(f"[PeripheralInstance] Error in desired listener: {e}")
|
|
163
|
+
|
|
164
|
+
# Update cached twin data
|
|
165
|
+
self._update_twin_data(twin_data)
|
|
166
|
+
|
|
167
|
+
self._client.on_twin_update(self.id, on_twin_update)
|
|
168
|
+
|
|
169
|
+
# =========================================================================
|
|
170
|
+
# Lifecycle
|
|
171
|
+
# =========================================================================
|
|
172
|
+
|
|
173
|
+
async def remove(self) -> Optional[dict[str, Any]]:
|
|
174
|
+
"""Delete this peripheral twin.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Deleted twin data or None.
|
|
178
|
+
"""
|
|
179
|
+
payload = {
|
|
180
|
+
"data": {"twinId": self.id}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
result = await self._client._connection.emit(
|
|
184
|
+
"deletePeripheralTwin", payload, callback=True
|
|
185
|
+
)
|
|
186
|
+
return result.get("twin") if result else None
|