phystack-hub-client 0.2.1__tar.gz → 0.2.3__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 (24) hide show
  1. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/PKG-INFO +1 -1
  2. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/pyproject.toml +1 -1
  3. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/__init__.py +22 -3
  4. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/client.py +172 -74
  5. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/instances/base_instance.py +16 -8
  6. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/instances/edge_instance.py +7 -3
  7. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/instances/peripheral_instance.py +5 -1
  8. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/services/phyhub_connection.py +127 -60
  9. phystack_hub_client-0.2.3/src/phystack/hub_client/services/signals.py +690 -0
  10. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/services/webrtc/data_channel.py +50 -7
  11. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/services/webrtc/media_stream.py +50 -7
  12. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/twin_registry.py +4 -1
  13. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/.gitignore +0 -0
  14. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/README.md +0 -0
  15. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/examples/browser_test_backend.py +0 -0
  16. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/scripts/python-node-test.sh +0 -0
  17. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/scripts/webrtc-test.sh +0 -0
  18. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/instances/__init__.py +0 -0
  19. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/services/__init__.py +0 -0
  20. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/services/webrtc/__init__.py +0 -0
  21. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/twin_messaging.py +0 -0
  22. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/types/__init__.py +0 -0
  23. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/src/phystack/hub_client/types/twin_types.py +0 -0
  24. {phystack_hub_client-0.2.1 → phystack_hub_client-0.2.3}/tests/test_communication.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phystack-hub-client
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Python client for PhyHub - events, actions, DataChannel, and MediaStream
5
5
  Project-URL: Homepage, https://github.com/phystack/phystack-pypi
6
6
  Project-URL: Documentation, https://docs.phystack.com
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "phystack-hub-client"
7
- version = "0.2.1"
7
+ version = "0.2.3"
8
8
  description = "Python client for PhyHub - events, actions, DataChannel, and MediaStream"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,7 +7,7 @@ A Python client for PhyHub that provides:
7
7
  - WebRTC MediaStream for P2P video/audio
8
8
  - Twin instance management (Edge, Peripheral, etc.)
9
9
  - Properties API (reported/desired)
10
- - Settings API with subscriptions
10
+ - Settings API
11
11
 
12
12
  Usage:
13
13
  from phystack.hub_client import PhyHubClient
@@ -27,8 +27,8 @@ Usage:
27
27
  instance.on_update_desired(lambda props: print("Desired changed:", props))
28
28
 
29
29
  # Settings (for edge apps)
30
- settings = client.get_settings()
31
- client.on_settings_update(lambda s: print("Settings updated:", s))
30
+ settings = await client.get_settings()
31
+ instance.on_update_desired(lambda props: print("Desired changed:", props))
32
32
 
33
33
  # DataChannel
34
34
  dc = await instance.get_data_channel(peer_twin_id)
@@ -63,6 +63,16 @@ from phystack.hub_client.services.webrtc.media_stream import (
63
63
  PhygridMediaStream,
64
64
  WebcamVideoTrack,
65
65
  )
66
+ from phystack.hub_client.services.signals import (
67
+ SignalsService,
68
+ InitSignalPayload,
69
+ DataRequestTypeEnum,
70
+ DataResidencyEnum,
71
+ EventTypeEnum,
72
+ CheckoutProps,
73
+ CheckoutProduct,
74
+ PurchaseProps,
75
+ )
66
76
 
67
77
  __version__ = "0.1.0"
68
78
  __all__ = [
@@ -87,4 +97,13 @@ __all__ = [
87
97
  "MediaStreamOptions",
88
98
  "PhygridMediaStream",
89
99
  "WebcamVideoTrack",
100
+ # Signals
101
+ "SignalsService",
102
+ "InitSignalPayload",
103
+ "DataRequestTypeEnum",
104
+ "DataResidencyEnum",
105
+ "EventTypeEnum",
106
+ "CheckoutProps",
107
+ "CheckoutProduct",
108
+ "PurchaseProps",
90
109
  ]
@@ -10,8 +10,8 @@ from typing import Any, Callable, Optional, Set
10
10
 
11
11
  from phystack.hub_client.services.phyhub_connection import (
12
12
  PhyHubConnection,
13
- DirectConnectionConfig,
14
- is_direct_mode,
13
+ ConnectionConfig,
14
+ ConnectionMode,
15
15
  )
16
16
  from phystack.hub_client.twin_messaging import (
17
17
  TwinMessaging,
@@ -23,6 +23,7 @@ from phystack.hub_client.types.twin_types import (
23
23
  TwinMessageResultStatus,
24
24
  TwinResponse,
25
25
  TwinProperties,
26
+ TwinTypeEnum,
26
27
  EventPayload,
27
28
  )
28
29
  from phystack.hub_client.services.webrtc.data_channel import (
@@ -65,7 +66,12 @@ class PhyHubClient:
65
66
  TWIN_SUBSCRIBE = "twinSubscribe"
66
67
  TWIN_UPDATED = "twinUpdated"
67
68
  GET_TWIN_BY_ID = "getTwinById"
68
- REPORT_PROPERTIES = "reportPeripheralTwinProperties"
69
+ REPORT_SCREEN_PROPERTIES = "reportScreenTwinProperties"
70
+ REPORT_EDGE_PROPERTIES = "reportEdgeTwinProperties"
71
+ REPORT_PERIPHERAL_PROPERTIES = "reportPeripheralTwinProperties"
72
+ GET_DEVICE_STATUS = "getDeviceStatus"
73
+ GET_DEVICE_INSTANCE = "getDeviceInstance"
74
+ GET_DEVICE_NETWORKS = "getDeviceNetworks"
69
75
 
70
76
  def __init__(
71
77
  self,
@@ -88,17 +94,17 @@ class PhyHubClient:
88
94
  self._subscribed_twins: Set[str] = set()
89
95
  self._twin_message_listeners: dict[str, Set[Callable[[dict[str, Any]], None]]] = {}
90
96
  self._twin_update_listeners: dict[str, Set[Callable[[dict[str, Any]], None]]] = {}
91
- self._settings_update_listeners: Set[Callable[[dict[str, Any]], None]] = set()
92
97
  self._twin_data_cache: dict[str, dict[str, Any]] = {}
93
98
  self._connected = False
94
99
  self._registry: Optional[TwinRegistry] = None
95
100
 
96
101
  if device_id and access_key:
97
102
  self._connection = PhyHubConnection(
98
- DirectConnectionConfig(
103
+ ConnectionConfig(
104
+ mode=ConnectionMode.DIRECT,
105
+ url=phyhub_url or "https://phyhub.eu.omborigrid.net",
99
106
  device_id=device_id,
100
107
  access_key=access_key,
101
- phyhub_url=phyhub_url or "https://phyhub.eu.omborigrid.net",
102
108
  instance_id=instance_id,
103
109
  )
104
110
  )
@@ -147,8 +153,12 @@ class PhyHubClient:
147
153
  # Set up event handlers
148
154
  self._setup_socket_listeners()
149
155
 
150
- # Authenticate and connect device
151
- await self._initialize_device_twin()
156
+ # Only direct mode needs explicit authenticate/connectDevice handshake.
157
+ # PhyOS and Simulator handle auth at Socket.IO connection time.
158
+ if self._connection.is_direct:
159
+ await self._initialize_device_twin()
160
+ else:
161
+ print(f"[PhyHubClient] Connected ({self._connection.config.mode.value} mode)")
152
162
 
153
163
  return self
154
164
 
@@ -216,18 +226,6 @@ class PhyHubClient:
216
226
  except Exception as e:
217
227
  print(f"[PhyHubClient] Error in twin update listener: {e}")
218
228
 
219
- # Check for settings updates on edge instance
220
- if twin_id == self._instance_id and isinstance(twin_data, dict):
221
- props = twin_data.get("properties", {})
222
- desired = props.get("desired", {})
223
- settings = desired.get("settings", {})
224
- if settings and self._settings_update_listeners:
225
- for callback in self._settings_update_listeners:
226
- try:
227
- callback(settings)
228
- except Exception as e:
229
- print(f"[PhyHubClient] Error in settings update listener: {e}")
230
-
231
229
  self._connection.on(self.EVENTS.TWIN_UPDATED, on_twin_updated)
232
230
 
233
231
  async def _initialize_device_twin(self) -> None:
@@ -322,22 +320,34 @@ class PhyHubClient:
322
320
  # Fire and forget - use asyncio to schedule
323
321
  asyncio.create_task(self._emit_to_socket(self.EVENTS.TWIN_MESSAGE, event_payload))
324
322
 
325
- async def _emit_to_socket(self, method: str, payload: dict[str, Any]) -> None:
326
- """Emit to socket with proper mode handling.
323
+ async def _emit_to_socket(
324
+ self, method: str, payload: dict[str, Any], callback: bool = False
325
+ ) -> Any:
326
+ """Unified emit — routes through instance ID channel or direct, based on mode.
327
327
 
328
328
  In direct mode: socket.emit(method, payload)
329
- In PhyOS mode: socket.emit(instanceId, { method, ...payload })
329
+ In PhyOS/Simulator mode: socket.emit(instanceId, { method, ...payload })
330
+
331
+ Args:
332
+ method: Event/method name.
333
+ payload: Data to send.
334
+ callback: If True, wait for ack response (for RPC calls).
335
+
336
+ Returns:
337
+ Response from server if callback is True, else None.
330
338
  """
331
339
  if not self._connection:
332
- return
340
+ return None
333
341
 
334
- if is_direct_mode():
342
+ if self._connection.is_direct:
335
343
  # Direct mode: emit to event name directly
336
- await self._connection.emit(method, payload)
344
+ return await self._connection.emit(method, payload, callback=callback)
337
345
  else:
338
- # PhyOS mode: emit to instanceId channel with method in payload
346
+ # PhyOS/Simulator: wrap in instance ID channel with method field
339
347
  phyos_payload = {"method": method, **payload}
340
- await self._connection.emit(self._instance_id or "", phyos_payload)
348
+ return await self._connection.emit(
349
+ self._instance_id or "", phyos_payload, callback=callback
350
+ )
341
351
 
342
352
  def on_twin_message(
343
353
  self,
@@ -418,10 +428,7 @@ class PhyHubClient:
418
428
  Returns:
419
429
  Twin data dictionary.
420
430
  """
421
- if not self._connection:
422
- return None
423
-
424
- result = await self._connection.emit(
431
+ result = await self._emit_to_socket(
425
432
  self.EVENTS.GET_TWIN_BY_ID,
426
433
  {"data": {"twinId": twin_id}},
427
434
  callback=True,
@@ -444,43 +451,7 @@ class PhyHubClient:
444
451
  return instance.get_settings()
445
452
  return {}
446
453
 
447
- async def get_edge_settings(self) -> dict[str, Any]:
448
- """Alias for get_settings().
449
454
 
450
- Returns:
451
- Settings dictionary.
452
- """
453
- return await self.get_settings()
454
-
455
- def on_settings_update(
456
- self, callback: Callable[[dict[str, Any]], None]
457
- ) -> None:
458
- """Subscribe to settings changes in edge desired properties.
459
-
460
- Args:
461
- callback: Handler called with new settings when they change.
462
- """
463
- self._settings_update_listeners.add(callback)
464
-
465
- # Subscribe to edge twin updates if connected
466
- if self._instance_id:
467
- asyncio.create_task(self.subscribe_twin(self._instance_id))
468
-
469
- def off_settings_update(
470
- self, callback: Callable[[dict[str, Any]], None]
471
- ) -> bool:
472
- """Remove settings update callback.
473
-
474
- Args:
475
- callback: Callback to remove.
476
-
477
- Returns:
478
- True if removed, False otherwise.
479
- """
480
- if callback in self._settings_update_listeners:
481
- self._settings_update_listeners.discard(callback)
482
- return True
483
- return False
484
455
 
485
456
  # =========================================================================
486
457
  # Twin Update API
@@ -535,26 +506,153 @@ class PhyHubClient:
535
506
  # =========================================================================
536
507
 
537
508
  async def update_reported_properties(
538
- self, twin_id: str, properties: dict[str, Any]
509
+ self,
510
+ twin_id: str,
511
+ properties: dict[str, Any],
512
+ twin_type: Optional[TwinTypeEnum] = None,
539
513
  ) -> Optional[dict[str, Any]]:
540
514
  """Update reported properties for a twin.
541
515
 
542
516
  Args:
543
517
  twin_id: Twin ID to update.
544
518
  properties: Properties to set.
519
+ twin_type: Twin type to determine correct event. Defaults to Peripheral.
545
520
 
546
521
  Returns:
547
522
  Updated twin data.
548
523
  """
549
- if not self._connection:
550
- return None
524
+ # Dispatch to correct event based on twin type (matches Node.js behavior)
525
+ if twin_type == TwinTypeEnum.Screen:
526
+ event = self.EVENTS.REPORT_SCREEN_PROPERTIES
527
+ elif twin_type == TwinTypeEnum.Edge:
528
+ event = self.EVENTS.REPORT_EDGE_PROPERTIES
529
+ else:
530
+ event = self.EVENTS.REPORT_PERIPHERAL_PROPERTIES
551
531
 
552
532
  payload = {"twinId": twin_id, "data": properties}
553
- result = await self._connection.emit(
554
- self.EVENTS.REPORT_PROPERTIES, payload, callback=True
555
- )
533
+ result = await self._emit_to_socket(event, payload, callback=True)
556
534
  return result.get("twin") if result else None
557
535
 
536
+ # =========================================================================
537
+ # Signals / Analytics API
538
+ # =========================================================================
539
+
540
+ async def initialize_signals(
541
+ self, params: Optional["InitSignalPayload"] = None
542
+ ) -> "SignalsService":
543
+ """Initialize the signals/analytics service.
544
+
545
+ When called without params, auto-populates from device status and
546
+ instance twin data (matches Node.js hub-client behavior).
547
+
548
+ Args:
549
+ params: Optional signal initialization parameters. If None,
550
+ auto-populates from device status and edge instance.
551
+
552
+ Returns:
553
+ Initialized SignalsService instance.
554
+ """
555
+ from phystack.hub_client.services.signals import SignalsService, InitSignalPayload
556
+
557
+ # Auto-populate from device status + instance (matches Node.js)
558
+ device_status: dict[str, Any] = {}
559
+ instance_data: dict[str, Any] = {}
560
+
561
+ if not params:
562
+ device_status = await self.get_device_status() or {}
563
+ edge_instance = await self.get_instance()
564
+ if edge_instance:
565
+ instance_data = edge_instance._twin_data or {}
566
+
567
+ p = params or InitSignalPayload()
568
+
569
+ payload = InitSignalPayload(
570
+ tenant_id=p.tenant_id or device_status.get("tenantId", ""),
571
+ space_id=p.space_id or device_status.get("spaceId", ""),
572
+ device_id=p.device_id or instance_data.get("deviceId") or device_status.get("deviceId", ""),
573
+ installation_id=p.installation_id or (
574
+ instance_data.get("properties", {}).get("desired", {}).get("installationId", "")
575
+ ),
576
+ environment=p.environment or device_status.get("gridEnv", ""),
577
+ data_residency=p.data_residency or (
578
+ device_status.get("dataResidency", "")
579
+ ).upper(),
580
+ country=p.country or "SE",
581
+ app_id=p.app_id or "XXXXXXXXXXXXXXXXXXXXXXXX",
582
+ app_version=p.app_version or "XXXXXXXXXXXXXXXXXXXXXXXX",
583
+ installation_version=p.installation_version or "XXXXXXXXXXXXXXXXXXXXXXXX",
584
+ ip=p.ip or (
585
+ device_status.get("ip", [{}])[0].get("ipv4", "N/A")
586
+ if isinstance(device_status.get("ip"), list) and device_status.get("ip")
587
+ else "N/A"
588
+ ),
589
+ session_id=p.session_id,
590
+ client_id=p.client_id,
591
+ latitude=p.latitude,
592
+ longitude=p.longitude,
593
+ location_accuracy=p.location_accuracy,
594
+ create_new_session_after_last_activity_mins=p.create_new_session_after_last_activity_mins,
595
+ )
596
+
597
+ service = SignalsService(self, payload)
598
+ await service.initialize()
599
+ return service
600
+
601
+ async def send_signal(
602
+ self, data_type: "DataRequestTypeEnum", data: dict[str, Any]
603
+ ) -> Any:
604
+ """Send a signal to the server.
605
+
606
+ Args:
607
+ data_type: Type of signal (EVENT, SESSION, CLIENT, CHECKOUT, PURCHASE).
608
+ data: Signal data (already key-shortened).
609
+ """
610
+ from phystack.hub_client.services.signals import DataRequestTypeEnum
611
+
612
+ event_map = {
613
+ DataRequestTypeEnum.EVENT: "sendEventSignal",
614
+ DataRequestTypeEnum.CHECKOUT: "sendEventSignal",
615
+ DataRequestTypeEnum.PURCHASE: "sendEventSignal",
616
+ DataRequestTypeEnum.SESSION: "sendSessionSignal",
617
+ DataRequestTypeEnum.CLIENT: "sendClientSignal",
618
+ }
619
+ event = event_map.get(data_type, "sendEventSignal")
620
+ return await self._emit_to_socket(event, {"data": data})
621
+
622
+ # =========================================================================
623
+ # Device API
624
+ # =========================================================================
625
+
626
+ async def get_device_status(self) -> Optional[dict[str, Any]]:
627
+ """Get device status (IP, serial, env, etc.).
628
+
629
+ Returns:
630
+ Device status dictionary.
631
+ """
632
+ return await self._emit_to_socket(
633
+ self.EVENTS.GET_DEVICE_STATUS, {}, callback=True
634
+ )
635
+
636
+ async def get_device_instance(self) -> Optional[dict[str, Any]]:
637
+ """Get device instance data.
638
+
639
+ Returns:
640
+ Device instance dictionary.
641
+ """
642
+ return await self._emit_to_socket(
643
+ self.EVENTS.GET_DEVICE_INSTANCE, {}, callback=True
644
+ )
645
+
646
+ async def get_device_networks(self) -> Optional[dict[str, Any]]:
647
+ """Get available networks on the device.
648
+
649
+ Returns:
650
+ Device networks dictionary.
651
+ """
652
+ return await self._emit_to_socket(
653
+ self.EVENTS.GET_DEVICE_NETWORKS, {}, callback=True
654
+ )
655
+
558
656
  # =========================================================================
559
657
  # WebRTC Helpers
560
658
  # =========================================================================
@@ -148,20 +148,24 @@ class BaseInstance:
148
148
 
149
149
  async def on_data_channel(
150
150
  self,
151
- target_twin_id: str,
152
151
  callback: Callable[["PhygridDataChannel"], None],
153
152
  channel_name: str = "default",
154
153
  ) -> None:
155
- """Listen for incoming DataChannel from a twin.
154
+ """Listen for incoming DataChannel from any twin.
155
+
156
+ Accepts the first incoming connection and locks to that source
157
+ (matches Node.js hub-client behavior).
156
158
 
157
159
  Args:
158
- target_twin_id: ID of the twin to listen from.
159
160
  callback: Handler for incoming channel.
160
161
  channel_name: Name of the channel to listen for.
161
162
  """
163
+ from phystack.hub_client.services.webrtc.data_channel import DataChannelOptions
164
+
162
165
  channel = await self._client.create_data_channel(
163
- target_twin_id=target_twin_id,
166
+ target_twin_id="",
164
167
  is_initiator=False,
168
+ options=DataChannelOptions(accept_from_any_source=True),
165
169
  channel_name=channel_name,
166
170
  blocking=False,
167
171
  )
@@ -201,20 +205,24 @@ class BaseInstance:
201
205
 
202
206
  async def on_media_stream(
203
207
  self,
204
- target_twin_id: str,
205
208
  callback: Callable[["PhygridMediaStream"], None],
206
209
  stream_name: str = "default",
207
210
  ) -> None:
208
- """Listen for incoming MediaStream from a twin.
211
+ """Listen for incoming MediaStream from any twin.
212
+
213
+ Accepts the first incoming connection and locks to that source
214
+ (matches Node.js hub-client behavior).
209
215
 
210
216
  Args:
211
- target_twin_id: ID of the twin to listen from.
212
217
  callback: Handler for incoming stream.
213
218
  stream_name: Name of the stream to listen for.
214
219
  """
220
+ from phystack.hub_client.services.webrtc.media_stream import MediaStreamOptions
221
+
215
222
  stream = await self._client.create_media_stream(
216
- target_twin_id=target_twin_id,
223
+ target_twin_id="",
217
224
  is_initiator=False,
225
+ options=MediaStreamOptions(accept_from_any_source=True),
218
226
  stream_name=stream_name,
219
227
  blocking=False,
220
228
  )
@@ -55,7 +55,11 @@ class EdgeInstance(BaseInstance):
55
55
  Returns:
56
56
  Updated twin data.
57
57
  """
58
- result = await self._client.update_reported_properties(self.id, properties)
58
+ from phystack.hub_client.types.twin_types import TwinTypeEnum
59
+
60
+ result = await self._client.update_reported_properties(
61
+ self.id, properties, twin_type=TwinTypeEnum.Edge
62
+ )
59
63
  if result:
60
64
  self._update_twin_data(result)
61
65
  return result or {}
@@ -182,7 +186,7 @@ class EdgeInstance(BaseInstance):
182
186
  }
183
187
  }
184
188
 
185
- result = await self._client._connection.emit(
189
+ result = await self._client._emit_to_socket(
186
190
  "createPeripheralTwin", payload, callback=True
187
191
  )
188
192
  return result.get("twin", {}) if result else {}
@@ -194,7 +198,7 @@ class EdgeInstance(BaseInstance):
194
198
  List of peripheral twin data.
195
199
  """
196
200
  payload = {"data": {"instanceId": self.id}}
197
- result = await self._client._connection.emit(
201
+ result = await self._client._emit_to_socket(
198
202
  "getPeripheralTwins", payload, callback=True
199
203
  )
200
204
  return result.get("twins", []) if result else []
@@ -57,7 +57,11 @@ class PeripheralInstance(BaseInstance):
57
57
  Returns:
58
58
  Updated twin data.
59
59
  """
60
- result = await self._client.update_reported_properties(self.id, properties)
60
+ from phystack.hub_client.types.twin_types import TwinTypeEnum
61
+
62
+ result = await self._client.update_reported_properties(
63
+ self.id, properties, twin_type=TwinTypeEnum.Peripheral
64
+ )
61
65
  if result:
62
66
  self._update_twin_data(result)
63
67
  return result or {}