Skip to content

Device API Reference

xovis.api.device.client

Xovis SDK - Device Client

This module resides within the State & Topology Plane, providing the primary entry point for interacting with Xovis edge sensors. It implements the DeviceClient, a stateful and topology-aware asynchronous manager that orchestrates authentication, configuration caching, and fleet discovery.

Classes

DeviceClient

Bases: BaseControlPlane

Stateful & Topology-Aware Asynchronous Client for Xovis environments.

Orchestrates the lifecycle of a connection to a Xovis sensor, including automated background configuration caching, multisensor discovery, and graceful resource cleanup.

Source code in src/xovis/api/device/client.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
class DeviceClient(BaseControlPlane):
    """
    Stateful & Topology-Aware Asynchronous Client for Xovis environments.

    Orchestrates the lifecycle of a connection to a Xovis sensor, including
    automated background configuration caching, multisensor discovery, and
    graceful resource cleanup.
    """

    def __init__(
        self,
        host: str,
        username: str,
        password: str,
        use_ntlm: bool = False,
        timeout: float = 15.0,
        max_retries: int = 5,
        cache_strategy: CacheStrategy = CacheStrategy.MANUAL,
        cache_ttl_seconds: float = 60.0,
        cache_poll_interval: float = 10.0,
        **kwargs: Any,
    ) -> None:
        """
        Initializes the DeviceClient.

        Args:
            host (str): IP address or hostname of the sensor.
            username (str): Authentication username.
            password (str): Authentication password.
            use_ntlm (bool, optional): Whether to use NTLM authentication. Defaults to False.
            timeout (float, optional): Request timeout in seconds. Defaults to 15.0.
            max_retries (int, optional): Max retry attempts for transient failures. Defaults to 5.
            cache_strategy (CacheStrategy, optional): Config sync strategy. Defaults to MANUAL.
            cache_ttl_seconds (float, optional): TTL for cached entries. Defaults to 60.0.
            cache_poll_interval (float, optional): Polling frequency for state hash. Defaults to 10.0.
            **kwargs (Any): Additional keyword arguments passed to the HTTP client.
        """
        setup_optimal_loop()

        base_url = f"http://{host}" if not host.startswith("http") else host

        # Extract SDK-specific kwargs before passing to HTTP client
        auto_persist_path = kwargs.pop("auto_persist_path", None)
        persistence_dir = kwargs.pop("persistence_dir", None)
        cache_child_devices = kwargs.pop("cache_child_devices", False)

        self._auth = DeviceAuth(username=username, password=password, use_ntlm=use_ntlm)
        self._http_client = XovisHTTPClient(
            base_url=base_url,
            auth=self._auth,
            timeout=timeout,
            max_retries=max_retries,
            **kwargs,
        )

        self.cache = ConfigCacheManager(
            http_client=self._http_client,
            strategy=cache_strategy,
            ttl_seconds=cache_ttl_seconds,
            poll_interval=cache_poll_interval,
            auto_persist_path=auto_persist_path,
            persistence_dir=persistence_dir,
            cache_child_devices=cache_child_devices,
        )
        self.cache._parent_client = self

        self._singlesensor = SinglesensorContext(self)
        self._device_info = {}
        self.fw_version = "unknown"

        # Cached resource managers
        self._network = None
        self._system = None
        self._time = None
        self._itxpt = None
        self._update = None
        self._topology = None
        self._multisensors = None
        self._users = None
        self._privacy_manager = None

        # Dynamically linked models facade
        from xovis.models import device_auto

        self.models = device_auto

        self._capability_cache: dict[str, bool] = {}

    async def _probe_capability(self, key: str, endpoint: str) -> bool:
        """
        Lazy asynchronous probe for hardware capabilities.

        Args:
            key (str): The unique capability identifier.
            endpoint (str): The API endpoint to probe.

        Returns:
            bool: True if the capability is supported and authorized.
        """
        if key in self._capability_cache:
            return self._capability_cache[key]

        try:
            # FIX: Use configured max_retries
            resp = await self._http_client.get(endpoint, max_retries=self._http_client.max_retries)
            is_supported = resp.status_code == 200
        except EndpointNotFoundError as e:
            # Xovis sensors return HTML 403 (mapped to EndpointNotFoundError) when features are missing/restricted.
            # We interpret this as a definitive "False" for the capability.
            import logging

            logging.getLogger(__name__).debug(f"Capability '{key}' restricted or not found at {endpoint}: {e}")
            is_supported = False
        except ForbiddenError as e:
            # Authorization failure (not HTML 403)
            import logging

            logging.getLogger(__name__).debug(f"Capability '{key}' forbidden at {endpoint}: {e}")
            is_supported = False
        except XovisAuthError as e:
            # Standard 401 Auth error should still be False for capability
            import logging

            logging.getLogger(__name__).debug(f"Capability '{key}' auth failed at {endpoint}: {e}")
            is_supported = False
        except Exception as e:
            # FIX: Stop silently swallowing errors! Log the failure for diagnostics.
            import logging

            logging.getLogger(__name__).debug(f"Capability probe for '{key}' failed at {endpoint}: {e}")
            is_supported = False

        self._capability_cache[key] = is_supported
        return is_supported

    async def has_capability(self, endpoint: str) -> bool:
        """
        Ad-hoc probe for a specific endpoint capability.
        """
        try:
            # FIX: Use configured max_retries
            resp = await self._http_client.get(endpoint, max_retries=self._http_client.max_retries)
            return resp.status_code == 200
        except Exception as e:
            import logging

            logging.getLogger(__name__).debug(f"Ad-hoc capability probe failed at {endpoint}: {e}")
            return False

    async def get_privacy_state(self) -> str:
        """
        Retrieves the current privacy mode of the sensor.

        Returns:
            str: The privacy mode identifier (e.g., "0", "1", "2", "3", "4").
        """
        try:
            privacy_mode = await self.privacy.get_privacy_mode()
            # Handle both object-based and raw dictionary responses
            if hasattr(privacy_mode, "mode"):
                return str(privacy_mode.mode)
            elif isinstance(privacy_mode, dict) and "mode" in privacy_mode:
                return str(privacy_mode["mode"])
            return str(privacy_mode)
        except Exception as e:
            import logging

            logging.getLogger(__name__).debug(f"Failed to retrieve privacy state: {e}")
            return "unknown"

    @property
    async def has_wifi(self) -> bool:
        """
        Checks if the hardware supports WiFi/BT monitoring.

        Returns:
            bool: True if RF privacy endpoints are accessible.
        """
        return await self._probe_capability("wifi", "/api/v5/rf/privacy")

    @property
    async def has_itxpt(self) -> bool:
        """
        Checks if the hardware supports ITxPT (Public Transport).

        Returns:
            bool: True if ITxPT state endpoints are accessible.
        """
        return await self._probe_capability("itxpt", "/api/v5/itxpt/state")

    @property
    async def has_analytics(self) -> bool:
        """
        Checks if the sensor supports and is authorized for analytics.

        Returns:
            bool: True if analytics logics endpoints are accessible.
        """
        return await self._probe_capability("analytics", "/api/v5/singlesensor/analysis/logics")

    async def _probe_license(self, feature_id: int) -> bool:
        """
        Probes the device for a specific license feature by ID.

        Args:
            feature_id (int): The unique Xovis feature identifier.

        Returns:
            bool: True if the license is active (ENABLED or TEST_ENABLED).
        """
        cache_key = f"license_{feature_id}"
        if cache_key in self._capability_cache:
            return self._capability_cache[cache_key]

        try:
            # We use the system manager's license details endpoint
            details = await self.system.get_license_details()
            if not details or not details.licenses:
                is_active = False
            else:
                # Search for the feature ID and check state
                is_active = any(lic.id == feature_id and lic.state.value in ("ENABLED", "TEST_ENABLED") for lic in details.licenses)
        except Exception as e:
            import logging

            logging.getLogger(__name__).debug(f"License probe for ID {feature_id} failed: {e}")
            is_active = False

        self._capability_cache[cache_key] = is_active
        return is_active

    @property
    def has_object_detection(self) -> Any:
        """
        Checks for core Object Detection license (PIVID=116).

        Returns:
            Awaitable[bool]: True if the license is active.
        """
        return self._probe_license(116)

    @property
    def has_pram_detection(self) -> Any:
        """
        Checks for Pram extension license (PIGES=112).

        Returns:
            Awaitable[bool]: True if the license is active.
        """
        return self._probe_license(112)

    @property
    def has_wheelchair_detection(self) -> Any:
        """
        Checks for Wheelchair extension license (PISTE=113).

        Returns:
            Awaitable[bool]: True if the license is active.
        """
        return self._probe_license(113)

    @property
    def has_bicycle_detection(self) -> Any:
        """
        Checks for Bicycle extension license (PIBCL=114).

        Returns:
            Awaitable[bool]: True if the license is active.
        """
        return self._probe_license(114)

    @property
    def has_people_attributes(self) -> Any:
        """
        Checks for People Attributes license (PIFMD=117).

        Returns:
            Awaitable[bool]: True if the license is active.
        """
        return self._probe_license(117)

    @property
    def info(self) -> Optional[dict[str, Any]]:
        """
        Retrieves raw device information fetched during connection.

        Returns:
            Optional[Dict[str, Any]]: The hardware profile metadata.
        """
        return self._device_info

    @property
    def is_spider(self) -> bool:
        """
        Checks if the hardware is a lensless Spider NUC.

        Returns:
            bool: True if the device type contains 'Spider' or 'SPIDER',
                or if the product code indicates a Spider unit.
        """
        if not self._device_info:
            return False
        device_type = self._device_info.get("type", "").upper()
        prod_code = self._device_info.get("prod_code", "").upper()
        return "SPIDER" in device_type or "SPI-PU1" in prod_code or "SPI-PU2" in prod_code

    @property
    def active_contexts(self) -> list:
        """
        Returns a flat, iterable list of valid hardware-aware contexts.

        For standard sensors (PC/PF-series): Returns [singlesensor] + multisensors.
        For Spiders: Returns only the multisensor contexts.

        Returns:
            list: A list containing SinglesensorContext and/or MultisensorContext instances.
        """
        contexts = []
        if not self.is_spider:
            contexts.append(self.singlesensor)

        # Add all discovered multisensor contexts
        contexts.extend(list(self.multisensors._contexts))
        return contexts

    @property
    def singlesensor(self) -> SinglesensorContext:
        """
        Accessor for the physical lens context.

        Returns:
            SinglesensorContext: The physical hardware management context.
        """
        return self._singlesensor

    @property
    def datapush(self) -> DataPushManager:
        """
        Direct accessor for DataPush management (shortcut to singlesensor.datapush).

        Returns:
            DataPushManager: The DataPush manager for the physical device.

        Raises:
            HardwareNotSupportedError: If accessed on a lensless Spider NUC.
        """
        return self.singlesensor.datapush

    @property
    def analytics(self) -> AnalyticsManager:
        """
        Direct accessor for Analytics management (shortcut to singlesensor.analytics).

        Returns:
            AnalyticsManager: The Analytics manager for the physical device.

        Raises:
            HardwareNotSupportedError: If accessed on a lensless Spider NUC.
        """
        return self.singlesensor.analytics

    @property
    def scene(self) -> SceneManager:
        """
        Direct accessor for Scene management (shortcut to singlesensor.scene).

        Returns:
            SceneManager: The Scene manager for the physical device.

        Raises:
            HardwareNotSupportedError: If accessed on a lensless Spider NUC.
        """
        return self.singlesensor.scene

    @property
    def history(self) -> HistoryManager:
        """
        Direct accessor for History management (shortcut to singlesensor.history).

        Returns:
            HistoryManager: The History manager for the physical device.

        Raises:
            HardwareNotSupportedError: If accessed on a lensless Spider NUC.
        """
        return self.singlesensor.history

    @property
    def network(self) -> NetworkManager:
        """
        Accesses the Network configuration manager.

        Returns:
            NetworkManager: Manager for IP, DNS, and bridge settings.
        """
        if self._network is None:
            self._network = NetworkManager(self._http_client, client=self)
        return self._network

    @property
    def system(self) -> SystemManager:
        """
        Accesses the System management manager.

        Returns:
            SystemManager: Manager for reboot, reset, and license operations.
        """
        if self._system is None:
            self._system = SystemManager(self._http_client, client=self)
        return self._system

    @property
    def time(self) -> TimeManager:
        """
        Accesses the Time configuration manager.

        Returns:
            TimeManager: Manager for NTP and timezone settings.
        """
        if self._time is None:
            self._time = TimeManager(self._http_client, client=self)
        return self._time

    @property
    def itxpt(self) -> ITxPTManager:
        """
        Accesses the ITxPT management manager.

        Returns:
            ITxPTManager: Manager for public transport protocols.
        """
        if self._itxpt is None:
            self._itxpt = ITxPTManager(self._http_client, client=self)
        return self._itxpt

    @property
    def update(self) -> UpdateManager:
        """
        Accesses the Firmware Update manager.

        Returns:
            UpdateManager: Manager for OTA and local binary flashing.
        """
        if self._update is None:
            self._update = UpdateManager(self)
        return self._update

    @property
    def topology(self) -> TopologyManager:
        """
        Accesses the Topology and Graphing manager.

        Returns:
            TopologyManager: Manager for Multisensor node discovery.
        """
        if self._topology is None:
            self._topology = TopologyManager(self._http_client, parent_client=self)
        return self._topology

    @property
    def multisensors(self) -> MultisensorsManager:
        """
        Accesses the Multisensor cluster manager.

        Returns:
            MultisensorsManager: Manager for virtual stitched environments (Multisensors).
        """
        if self._multisensors is None:
            self._multisensors = MultisensorsManager(self)
        return self._multisensors

    @property
    def users(self) -> UsersManager:
        """
        Accesses the User management manager.

        Returns:
            UsersManager: Manager for local sensor accounts.
        """
        if self._users is None:
            self._users = UsersManager(self._http_client, client=self)
        return self._users

    @property
    def privacy(self) -> PrivacyManager:
        """
        Accesses the Privacy configuration manager.

        Returns:
            PrivacyManager: Manager for masking and blurring settings.
        """
        if self._privacy_manager is None:
            self._privacy_manager = PrivacyManager(self._http_client, client=self)
        return self._privacy_manager

    async def __aenter__(self) -> "DeviceClient":
        """
        Enters the asynchronous context and initializes background engines.

        Establishes connection pools, fetches hardware profiles, hydrates
        multisensor contexts, and starts the configuration cache watcher.

        Returns:
            DeviceClient: The active client instance.
        """
        self._setup_signal_handlers()
        await self._http_client.__aenter__()

        # Aggressive Hardware Probing
        try:
            resp = await self._http_client.get("/api/v5/device/info")
            self._device_info = resp.json()
            self.fw_version = self._device_info.get("fw_version", "unknown") if self._device_info else "unknown"
        except Exception:
            self._device_info = {}
            self.fw_version = "unknown"

        # Capability Probing
        self._capability_cache["advanced_zones"] = "5.9.2" in self.fw_version

        # Model Versioning Selection
        from xovis.models import device_auto

        self.models = device_auto

        if "5.9.2" in self.fw_version:
            self.models = self.models.v5_9_2_models
        else:
            self.models = self.models.stable_models

        # Sync multisensors and start cache watcher
        await asyncio.gather(self.multisensors.sync(), self.cache.start(), return_exceptions=True)

        # Ensure we try to load from disk
        await self.cache.load_from_disk()

        return self

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """
        Gracefully shuts down background engines and releases connections.

        Args:
            exc_type (Any): The exception type if an error occurred.
            exc_val (Any): The exception value.
            exc_tb (Any): The traceback object.
        """
        await self.cache.stop()
        await self._http_client.__aexit__(exc_type, exc_val, exc_tb)

    async def aclose(self) -> None:
        """
        Manual graceful shutdown fallback.
        """
        await self.cache.stop()
        await self._http_client.aclose()

    def _setup_signal_handlers(self) -> None:
        """
        Registers signal handlers for graceful shutdown on POSIX systems.
        """
        if sys.platform != "win32":
            loop = asyncio.get_running_loop()
            for sig in (signal.SIGINT, signal.SIGTERM):
                loop.add_signal_handler(sig, lambda: asyncio.create_task(self.aclose()))
Attributes
active_contexts property

Returns a flat, iterable list of valid hardware-aware contexts.

For standard sensors (PC/PF-series): Returns [singlesensor] + multisensors. For Spiders: Returns only the multisensor contexts.

Returns:

Name Type Description
list list

A list containing SinglesensorContext and/or MultisensorContext instances.

analytics property

Direct accessor for Analytics management (shortcut to singlesensor.analytics).

Returns:

Name Type Description
AnalyticsManager AnalyticsManager

The Analytics manager for the physical device.

Raises:

Type Description
HardwareNotSupportedError

If accessed on a lensless Spider NUC.

datapush property

Direct accessor for DataPush management (shortcut to singlesensor.datapush).

Returns:

Name Type Description
DataPushManager DataPushManager

The DataPush manager for the physical device.

Raises:

Type Description
HardwareNotSupportedError

If accessed on a lensless Spider NUC.

has_analytics async property

Checks if the sensor supports and is authorized for analytics.

Returns:

Name Type Description
bool bool

True if analytics logics endpoints are accessible.

has_bicycle_detection property

Checks for Bicycle extension license (PIBCL=114).

Returns:

Type Description
Any

Awaitable[bool]: True if the license is active.

has_itxpt async property

Checks if the hardware supports ITxPT (Public Transport).

Returns:

Name Type Description
bool bool

True if ITxPT state endpoints are accessible.

has_object_detection property

Checks for core Object Detection license (PIVID=116).

Returns:

Type Description
Any

Awaitable[bool]: True if the license is active.

has_people_attributes property

Checks for People Attributes license (PIFMD=117).

Returns:

Type Description
Any

Awaitable[bool]: True if the license is active.

has_pram_detection property

Checks for Pram extension license (PIGES=112).

Returns:

Type Description
Any

Awaitable[bool]: True if the license is active.

has_wheelchair_detection property

Checks for Wheelchair extension license (PISTE=113).

Returns:

Type Description
Any

Awaitable[bool]: True if the license is active.

has_wifi async property

Checks if the hardware supports WiFi/BT monitoring.

Returns:

Name Type Description
bool bool

True if RF privacy endpoints are accessible.

history property

Direct accessor for History management (shortcut to singlesensor.history).

Returns:

Name Type Description
HistoryManager HistoryManager

The History manager for the physical device.

Raises:

Type Description
HardwareNotSupportedError

If accessed on a lensless Spider NUC.

info property

Retrieves raw device information fetched during connection.

Returns:

Type Description
Optional[dict[str, Any]]

Optional[Dict[str, Any]]: The hardware profile metadata.

is_spider property

Checks if the hardware is a lensless Spider NUC.

Returns:

Name Type Description
bool bool

True if the device type contains 'Spider' or 'SPIDER', or if the product code indicates a Spider unit.

itxpt property

Accesses the ITxPT management manager.

Returns:

Name Type Description
ITxPTManager ITxPTManager

Manager for public transport protocols.

multisensors property

Accesses the Multisensor cluster manager.

Returns:

Name Type Description
MultisensorsManager MultisensorsManager

Manager for virtual stitched environments (Multisensors).

network property

Accesses the Network configuration manager.

Returns:

Name Type Description
NetworkManager NetworkManager

Manager for IP, DNS, and bridge settings.

privacy property

Accesses the Privacy configuration manager.

Returns:

Name Type Description
PrivacyManager PrivacyManager

Manager for masking and blurring settings.

scene property

Direct accessor for Scene management (shortcut to singlesensor.scene).

Returns:

Name Type Description
SceneManager SceneManager

The Scene manager for the physical device.

Raises:

Type Description
HardwareNotSupportedError

If accessed on a lensless Spider NUC.

singlesensor property

Accessor for the physical lens context.

Returns:

Name Type Description
SinglesensorContext SinglesensorContext

The physical hardware management context.

system property

Accesses the System management manager.

Returns:

Name Type Description
SystemManager SystemManager

Manager for reboot, reset, and license operations.

time property

Accesses the Time configuration manager.

Returns:

Name Type Description
TimeManager TimeManager

Manager for NTP and timezone settings.

topology property

Accesses the Topology and Graphing manager.

Returns:

Name Type Description
TopologyManager TopologyManager

Manager for Multisensor node discovery.

update property

Accesses the Firmware Update manager.

Returns:

Name Type Description
UpdateManager UpdateManager

Manager for OTA and local binary flashing.

users property

Accesses the User management manager.

Returns:

Name Type Description
UsersManager UsersManager

Manager for local sensor accounts.

Methods:
__aenter__() async

Enters the asynchronous context and initializes background engines.

Establishes connection pools, fetches hardware profiles, hydrates multisensor contexts, and starts the configuration cache watcher.

Returns:

Name Type Description
DeviceClient DeviceClient

The active client instance.

Source code in src/xovis/api/device/client.py
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
async def __aenter__(self) -> "DeviceClient":
    """
    Enters the asynchronous context and initializes background engines.

    Establishes connection pools, fetches hardware profiles, hydrates
    multisensor contexts, and starts the configuration cache watcher.

    Returns:
        DeviceClient: The active client instance.
    """
    self._setup_signal_handlers()
    await self._http_client.__aenter__()

    # Aggressive Hardware Probing
    try:
        resp = await self._http_client.get("/api/v5/device/info")
        self._device_info = resp.json()
        self.fw_version = self._device_info.get("fw_version", "unknown") if self._device_info else "unknown"
    except Exception:
        self._device_info = {}
        self.fw_version = "unknown"

    # Capability Probing
    self._capability_cache["advanced_zones"] = "5.9.2" in self.fw_version

    # Model Versioning Selection
    from xovis.models import device_auto

    self.models = device_auto

    if "5.9.2" in self.fw_version:
        self.models = self.models.v5_9_2_models
    else:
        self.models = self.models.stable_models

    # Sync multisensors and start cache watcher
    await asyncio.gather(self.multisensors.sync(), self.cache.start(), return_exceptions=True)

    # Ensure we try to load from disk
    await self.cache.load_from_disk()

    return self
__aexit__(exc_type, exc_val, exc_tb) async

Gracefully shuts down background engines and releases connections.

Parameters:

Name Type Description Default
exc_type Any

The exception type if an error occurred.

required
exc_val Any

The exception value.

required
exc_tb Any

The traceback object.

required
Source code in src/xovis/api/device/client.py
651
652
653
654
655
656
657
658
659
660
661
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    """
    Gracefully shuts down background engines and releases connections.

    Args:
        exc_type (Any): The exception type if an error occurred.
        exc_val (Any): The exception value.
        exc_tb (Any): The traceback object.
    """
    await self.cache.stop()
    await self._http_client.__aexit__(exc_type, exc_val, exc_tb)
__init__(host, username, password, use_ntlm=False, timeout=15.0, max_retries=5, cache_strategy=CacheStrategy.MANUAL, cache_ttl_seconds=60.0, cache_poll_interval=10.0, **kwargs)

Initializes the DeviceClient.

Parameters:

Name Type Description Default
host str

IP address or hostname of the sensor.

required
username str

Authentication username.

required
password str

Authentication password.

required
use_ntlm bool

Whether to use NTLM authentication. Defaults to False.

False
timeout float

Request timeout in seconds. Defaults to 15.0.

15.0
max_retries int

Max retry attempts for transient failures. Defaults to 5.

5
cache_strategy CacheStrategy

Config sync strategy. Defaults to MANUAL.

MANUAL
cache_ttl_seconds float

TTL for cached entries. Defaults to 60.0.

60.0
cache_poll_interval float

Polling frequency for state hash. Defaults to 10.0.

10.0
**kwargs Any

Additional keyword arguments passed to the HTTP client.

{}
Source code in src/xovis/api/device/client.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
def __init__(
    self,
    host: str,
    username: str,
    password: str,
    use_ntlm: bool = False,
    timeout: float = 15.0,
    max_retries: int = 5,
    cache_strategy: CacheStrategy = CacheStrategy.MANUAL,
    cache_ttl_seconds: float = 60.0,
    cache_poll_interval: float = 10.0,
    **kwargs: Any,
) -> None:
    """
    Initializes the DeviceClient.

    Args:
        host (str): IP address or hostname of the sensor.
        username (str): Authentication username.
        password (str): Authentication password.
        use_ntlm (bool, optional): Whether to use NTLM authentication. Defaults to False.
        timeout (float, optional): Request timeout in seconds. Defaults to 15.0.
        max_retries (int, optional): Max retry attempts for transient failures. Defaults to 5.
        cache_strategy (CacheStrategy, optional): Config sync strategy. Defaults to MANUAL.
        cache_ttl_seconds (float, optional): TTL for cached entries. Defaults to 60.0.
        cache_poll_interval (float, optional): Polling frequency for state hash. Defaults to 10.0.
        **kwargs (Any): Additional keyword arguments passed to the HTTP client.
    """
    setup_optimal_loop()

    base_url = f"http://{host}" if not host.startswith("http") else host

    # Extract SDK-specific kwargs before passing to HTTP client
    auto_persist_path = kwargs.pop("auto_persist_path", None)
    persistence_dir = kwargs.pop("persistence_dir", None)
    cache_child_devices = kwargs.pop("cache_child_devices", False)

    self._auth = DeviceAuth(username=username, password=password, use_ntlm=use_ntlm)
    self._http_client = XovisHTTPClient(
        base_url=base_url,
        auth=self._auth,
        timeout=timeout,
        max_retries=max_retries,
        **kwargs,
    )

    self.cache = ConfigCacheManager(
        http_client=self._http_client,
        strategy=cache_strategy,
        ttl_seconds=cache_ttl_seconds,
        poll_interval=cache_poll_interval,
        auto_persist_path=auto_persist_path,
        persistence_dir=persistence_dir,
        cache_child_devices=cache_child_devices,
    )
    self.cache._parent_client = self

    self._singlesensor = SinglesensorContext(self)
    self._device_info = {}
    self.fw_version = "unknown"

    # Cached resource managers
    self._network = None
    self._system = None
    self._time = None
    self._itxpt = None
    self._update = None
    self._topology = None
    self._multisensors = None
    self._users = None
    self._privacy_manager = None

    # Dynamically linked models facade
    from xovis.models import device_auto

    self.models = device_auto

    self._capability_cache: dict[str, bool] = {}
_probe_capability(key, endpoint) async

Lazy asynchronous probe for hardware capabilities.

Parameters:

Name Type Description Default
key str

The unique capability identifier.

required
endpoint str

The API endpoint to probe.

required

Returns:

Name Type Description
bool bool

True if the capability is supported and authorized.

Source code in src/xovis/api/device/client.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
async def _probe_capability(self, key: str, endpoint: str) -> bool:
    """
    Lazy asynchronous probe for hardware capabilities.

    Args:
        key (str): The unique capability identifier.
        endpoint (str): The API endpoint to probe.

    Returns:
        bool: True if the capability is supported and authorized.
    """
    if key in self._capability_cache:
        return self._capability_cache[key]

    try:
        # FIX: Use configured max_retries
        resp = await self._http_client.get(endpoint, max_retries=self._http_client.max_retries)
        is_supported = resp.status_code == 200
    except EndpointNotFoundError as e:
        # Xovis sensors return HTML 403 (mapped to EndpointNotFoundError) when features are missing/restricted.
        # We interpret this as a definitive "False" for the capability.
        import logging

        logging.getLogger(__name__).debug(f"Capability '{key}' restricted or not found at {endpoint}: {e}")
        is_supported = False
    except ForbiddenError as e:
        # Authorization failure (not HTML 403)
        import logging

        logging.getLogger(__name__).debug(f"Capability '{key}' forbidden at {endpoint}: {e}")
        is_supported = False
    except XovisAuthError as e:
        # Standard 401 Auth error should still be False for capability
        import logging

        logging.getLogger(__name__).debug(f"Capability '{key}' auth failed at {endpoint}: {e}")
        is_supported = False
    except Exception as e:
        # FIX: Stop silently swallowing errors! Log the failure for diagnostics.
        import logging

        logging.getLogger(__name__).debug(f"Capability probe for '{key}' failed at {endpoint}: {e}")
        is_supported = False

    self._capability_cache[key] = is_supported
    return is_supported
_probe_license(feature_id) async

Probes the device for a specific license feature by ID.

Parameters:

Name Type Description Default
feature_id int

The unique Xovis feature identifier.

required

Returns:

Name Type Description
bool bool

True if the license is active (ENABLED or TEST_ENABLED).

Source code in src/xovis/api/device/client.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
async def _probe_license(self, feature_id: int) -> bool:
    """
    Probes the device for a specific license feature by ID.

    Args:
        feature_id (int): The unique Xovis feature identifier.

    Returns:
        bool: True if the license is active (ENABLED or TEST_ENABLED).
    """
    cache_key = f"license_{feature_id}"
    if cache_key in self._capability_cache:
        return self._capability_cache[cache_key]

    try:
        # We use the system manager's license details endpoint
        details = await self.system.get_license_details()
        if not details or not details.licenses:
            is_active = False
        else:
            # Search for the feature ID and check state
            is_active = any(lic.id == feature_id and lic.state.value in ("ENABLED", "TEST_ENABLED") for lic in details.licenses)
    except Exception as e:
        import logging

        logging.getLogger(__name__).debug(f"License probe for ID {feature_id} failed: {e}")
        is_active = False

    self._capability_cache[cache_key] = is_active
    return is_active
_setup_signal_handlers()

Registers signal handlers for graceful shutdown on POSIX systems.

Source code in src/xovis/api/device/client.py
670
671
672
673
674
675
676
677
def _setup_signal_handlers(self) -> None:
    """
    Registers signal handlers for graceful shutdown on POSIX systems.
    """
    if sys.platform != "win32":
        loop = asyncio.get_running_loop()
        for sig in (signal.SIGINT, signal.SIGTERM):
            loop.add_signal_handler(sig, lambda: asyncio.create_task(self.aclose()))
aclose() async

Manual graceful shutdown fallback.

Source code in src/xovis/api/device/client.py
663
664
665
666
667
668
async def aclose(self) -> None:
    """
    Manual graceful shutdown fallback.
    """
    await self.cache.stop()
    await self._http_client.aclose()
get_privacy_state() async

Retrieves the current privacy mode of the sensor.

Returns:

Name Type Description
str str

The privacy mode identifier (e.g., "0", "1", "2", "3", "4").

Source code in src/xovis/api/device/client.py
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
async def get_privacy_state(self) -> str:
    """
    Retrieves the current privacy mode of the sensor.

    Returns:
        str: The privacy mode identifier (e.g., "0", "1", "2", "3", "4").
    """
    try:
        privacy_mode = await self.privacy.get_privacy_mode()
        # Handle both object-based and raw dictionary responses
        if hasattr(privacy_mode, "mode"):
            return str(privacy_mode.mode)
        elif isinstance(privacy_mode, dict) and "mode" in privacy_mode:
            return str(privacy_mode["mode"])
        return str(privacy_mode)
    except Exception as e:
        import logging

        logging.getLogger(__name__).debug(f"Failed to retrieve privacy state: {e}")
        return "unknown"
has_capability(endpoint) async

Ad-hoc probe for a specific endpoint capability.

Source code in src/xovis/api/device/client.py
248
249
250
251
252
253
254
255
256
257
258
259
260
async def has_capability(self, endpoint: str) -> bool:
    """
    Ad-hoc probe for a specific endpoint capability.
    """
    try:
        # FIX: Use configured max_retries
        resp = await self._http_client.get(endpoint, max_retries=self._http_client.max_retries)
        return resp.status_code == 200
    except Exception as e:
        import logging

        logging.getLogger(__name__).debug(f"Ad-hoc capability probe failed at {endpoint}: {e}")
        return False

SinglesensorContext

Isolated context strictly for the physical device lenses.

Provides access to core resource managers (DataPush, Scene, Analytics) bound to the physical sensor hardware. This context is disabled on lensless hardware profiles like the Spider NUC.

Source code in src/xovis/api/device/client.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class SinglesensorContext:
    """
    Isolated context strictly for the physical device lenses.

    Provides access to core resource managers (DataPush, Scene, Analytics)
    bound to the physical sensor hardware. This context is disabled on
    lensless hardware profiles like the Spider NUC.
    """

    def __init__(self, client: Any):
        """
        Initializes the SinglesensorContext.

        Args:
            client (DeviceClient): The parent device client instance.
        """
        self._client = client
        self._datapush = DataPushManager(client)
        self._scene = SceneManager(client)
        self._analytics = AnalyticsManager(client)
        self._history = HistoryManager(client)
        self.update = UpdateManager(client)
        self.images = ImagesManager(client)
        self._privacy = PrivacyManager(client._http_client, client=client)

    @property
    def datapush(self) -> DataPushManager:
        """Accesses the DataPush management manager."""
        if self._client.is_spider:
            raise HardwareNotSupportedError("Spider NUCs lack physical lenses. Access 'datapush' via the 'multisensors' context.")
        return self._datapush

    @property
    def analytics(self) -> AnalyticsManager:
        """Accesses the Analytics management manager."""
        if self._client.is_spider:
            raise HardwareNotSupportedError("Spider NUCs lack physical lenses. Access 'analytics' via the 'multisensors' context.")
        return self._analytics

    @property
    def history(self) -> HistoryManager:
        """Accesses the History management manager."""
        if self._client.is_spider:
            raise HardwareNotSupportedError("Spider NUCs lack physical lenses. Access 'history' via the 'multisensors' context.")
        return self._history

    @property
    def scene(self) -> SceneManager:
        """Accesses the Scene management manager."""
        if self._client.is_spider:
            raise HardwareNotSupportedError("Spider NUCs lack physical lenses. Access 'scene' via the 'multisensors' context.")
        return self._scene

    @property
    def privacy(self) -> PrivacyManager:
        """
        Accesses the Privacy management manager.

        Returns:
            PrivacyManager: Manager for masking and blurring.

        Raises:
            HardwareNotSupportedError: If accessed on a lensless Spider NUC.
        """
        if self._client.is_spider:
            raise HardwareNotSupportedError("Spider NUCs lack physical lenses. Access 'scene' and 'privacy' via the 'multisensors' context.")
        return self._privacy
Attributes
analytics property

Accesses the Analytics management manager.

datapush property

Accesses the DataPush management manager.

history property

Accesses the History management manager.

privacy property

Accesses the Privacy management manager.

Returns:

Name Type Description
PrivacyManager PrivacyManager

Manager for masking and blurring.

Raises:

Type Description
HardwareNotSupportedError

If accessed on a lensless Spider NUC.

scene property

Accesses the Scene management manager.

Methods:
__init__(client)

Initializes the SinglesensorContext.

Parameters:

Name Type Description Default
client DeviceClient

The parent device client instance.

required
Source code in src/xovis/api/device/client.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def __init__(self, client: Any):
    """
    Initializes the SinglesensorContext.

    Args:
        client (DeviceClient): The parent device client instance.
    """
    self._client = client
    self._datapush = DataPushManager(client)
    self._scene = SceneManager(client)
    self._analytics = AnalyticsManager(client)
    self._history = HistoryManager(client)
    self.update = UpdateManager(client)
    self.images = ImagesManager(client)
    self._privacy = PrivacyManager(client._http_client, client=client)

UnifiedDeviceClient

Enterprise-grade hybrid connection router for Xovis devices.

This class provides a hybrid routing strategy across the control and state planes. It supports three connection pathways: MAC-First (resolving MAC addresses with local IP handshake and Cloud HUB fallback), IP-First (local connection with HUB proxy fallback), and Named Resolution (dynamic name lookups with AmbiguousDeviceNameError handling).

Attributes:

Name Type Description
mac_address Optional[str]

Target MAC address.

host Optional[str]

Target IP address or hostname.

name Optional[str]

Target device name.

hub_client Optional[Any]

HubClient instance for Cloud proxy fallback.

username str

Local authentication username.

password str

Local authentication password.

kwargs Any

Additional options passed to DeviceClient.

Source code in src/xovis/api/device/client.py
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
class UnifiedDeviceClient:
    """
    Enterprise-grade hybrid connection router for Xovis devices.

    This class provides a hybrid routing strategy across the control and state planes.
    It supports three connection pathways: MAC-First (resolving MAC addresses with local IP
    handshake and Cloud HUB fallback), IP-First (local connection with HUB proxy fallback),
    and Named Resolution (dynamic name lookups with AmbiguousDeviceNameError handling).

    Attributes:
        mac_address (Optional[str]): Target MAC address.
        host (Optional[str]): Target IP address or hostname.
        name (Optional[str]): Target device name.
        hub_client (Optional[Any]): HubClient instance for Cloud proxy fallback.
        username (str): Local authentication username.
        password (str): Local authentication password.
        kwargs (Any): Additional options passed to DeviceClient.
    """

    def __init__(
        self,
        identifier: Optional[str] = None,
        mac_address: Optional[str] = None,
        host: Optional[str] = None,
        name: Optional[str] = None,
        hub_client: Optional[Any] = None,
        username: str = "admin",
        password: str = "pass",
        **kwargs: Any,
    ) -> None:
        """
        Initializes the UnifiedDeviceClient.

        Args:
            identifier (Optional[str]): Optional single positional identifier (MAC, IP, or Name).
            mac_address (Optional[str]): Target MAC address.
            host (Optional[str]): Target IP address.
            name (Optional[str]): Target device name.
            hub_client (Optional[Any]): Optional HubClient instance.
            username (str): Username for LAN authentication.
            password (str): Password for LAN authentication.
            **kwargs (Any): Extra connection options.
        """
        self.mac_address = mac_address
        self.host = host
        self.name = name
        self.hub_client = hub_client
        self.username = username
        self.password = password
        self.kwargs = kwargs
        self._client: Optional[DeviceClient] = None

        if identifier:
            is_mac = bool(re.match(r"^([0-9A-Fa-f]{2}[:.-]){5}([0-9A-Fa-f]{2})$", identifier))
            is_ip = False
            try:
                ipaddress.ip_address(identifier)
                is_ip = True
            except ValueError:
                pass

            if is_mac:
                self.mac_address = identifier
            elif is_ip:
                self.host = identifier
            else:
                self.name = identifier

    async def __aenter__(self) -> DeviceClient:
        """
        Establishes connection to the device using the optimal route.

        Returns:
            DeviceClient: An active, authenticated device client instance.

        Raises:
            AmbiguousDeviceNameError: If multiple devices share the same name.
            ConnectionError: If all connection attempts and fallbacks fail.
        """
        resolved_mac = self.mac_address
        resolved_host = self.host

        if self.name:
            if not self.hub_client:
                raise ConnectionError("Cannot resolve device name without a HubClient.")
            devices = getattr(self.hub_client.cache._state, "devices", [])
            matches = [d for d in devices if getattr(d, "device_name", None) == self.name]
            if not matches:
                raise ConnectionError(f"Device with name '{self.name}' not found in Hub cache.")
            if len(matches) > 1:
                macs = []
                for d in matches:
                    d_id = getattr(d, "id", None)
                    d_mac = (d_id.root if hasattr(d_id, "root") else d_id) if d_id else "unknown"
                    macs.append(str(d_mac))
                raise AmbiguousDeviceNameError(f"Multiple devices found with name '{self.name}': {', '.join(macs)}")
            matched_dev = matches[0]
            d_id = getattr(matched_dev, "id", None)
            resolved_mac = (d_id.root if hasattr(d_id, "root") else d_id) if d_id else None

        # Path 2: Explicit IP Address is available
        if resolved_host:
            probe_kwargs = self.kwargs.copy()
            probe_kwargs["timeout"] = 2.0
            probe_client = DeviceClient(
                host=resolved_host,
                username=self.username,
                password=self.password,
                **probe_kwargs,
            )
            try:
                async with probe_client:
                    pass
                self._client = DeviceClient(
                    host=resolved_host,
                    username=self.username,
                    password=self.password,
                    **self.kwargs,
                )
                await self._client.__aenter__()
                return self._client
            except Exception:
                pass

        # If IP failed or was not provided, but we don't have a MAC yet
        if not resolved_mac and resolved_host:
            # Cross-Reference for Hub Routing (Fallback)
            if self.hub_client:
                for d in getattr(self.hub_client.cache._state, "devices", []):
                    if getattr(d, "ip", None) == resolved_host:
                        d_id = getattr(d, "id", None)
                        resolved_mac = (d_id.root if hasattr(d_id, "root") else d_id) if d_id else None
                        break

            if not resolved_mac:
                raise ValueError(
                    f"IP {resolved_host} is unreachable on local LAN and cannot be safely routed via Hub without a matching cache entry. Please provide a MAC address."
                )

        # Path 1: Target is a MAC Address (or was resolved to a MAC)
        if resolved_mac:
            from xovis.api.device.network_discovery import NetworkDiscoveryService

            local_ip = await NetworkDiscoveryService.resolve_mac_to_ip(resolved_mac)

            if local_ip and local_ip != resolved_host:
                probe_kwargs = self.kwargs.copy()
                probe_kwargs["timeout"] = 2.0
                probe_client = DeviceClient(
                    host=local_ip,
                    username=self.username,
                    password=self.password,
                    **probe_kwargs,
                )
                try:
                    async with probe_client:
                        pass
                    self._client = DeviceClient(
                        host=local_ip,
                        username=self.username,
                        password=self.password,
                        **self.kwargs,
                    )
                    await self._client.__aenter__()
                    return self._client
                except Exception:
                    pass

            if self.hub_client:
                try:
                    self._client = await self.hub_client.connect_device(resolved_mac)
                    await self._client.__aenter__()
                    return self._client
                except Exception as e:
                    raise ConnectionError(f"Could not connect to device {resolved_mac} via LAN or via Cloud Hub Tunnel: {e}") from e

        desc = resolved_mac or resolved_host or self.name or "unknown"
        raise ConnectionError(f"Device {desc} offline/unreachable on LAN and no HubClient/MAC is available for fallback.")

    async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """
        Gracefully releases the connected device client resources.

        Args:
            exc_type (Any): The exception type if an error occurred.
            exc_val (Any): The exception value.
            exc_tb (Any): The traceback object.
        """
        if self._client:
            await self._client.__aexit__(exc_type, exc_val, exc_tb)
Methods:
__aenter__() async

Establishes connection to the device using the optimal route.

Returns:

Name Type Description
DeviceClient DeviceClient

An active, authenticated device client instance.

Raises:

Type Description
AmbiguousDeviceNameError

If multiple devices share the same name.

ConnectionError

If all connection attempts and fallbacks fail.

Source code in src/xovis/api/device/client.py
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
async def __aenter__(self) -> DeviceClient:
    """
    Establishes connection to the device using the optimal route.

    Returns:
        DeviceClient: An active, authenticated device client instance.

    Raises:
        AmbiguousDeviceNameError: If multiple devices share the same name.
        ConnectionError: If all connection attempts and fallbacks fail.
    """
    resolved_mac = self.mac_address
    resolved_host = self.host

    if self.name:
        if not self.hub_client:
            raise ConnectionError("Cannot resolve device name without a HubClient.")
        devices = getattr(self.hub_client.cache._state, "devices", [])
        matches = [d for d in devices if getattr(d, "device_name", None) == self.name]
        if not matches:
            raise ConnectionError(f"Device with name '{self.name}' not found in Hub cache.")
        if len(matches) > 1:
            macs = []
            for d in matches:
                d_id = getattr(d, "id", None)
                d_mac = (d_id.root if hasattr(d_id, "root") else d_id) if d_id else "unknown"
                macs.append(str(d_mac))
            raise AmbiguousDeviceNameError(f"Multiple devices found with name '{self.name}': {', '.join(macs)}")
        matched_dev = matches[0]
        d_id = getattr(matched_dev, "id", None)
        resolved_mac = (d_id.root if hasattr(d_id, "root") else d_id) if d_id else None

    # Path 2: Explicit IP Address is available
    if resolved_host:
        probe_kwargs = self.kwargs.copy()
        probe_kwargs["timeout"] = 2.0
        probe_client = DeviceClient(
            host=resolved_host,
            username=self.username,
            password=self.password,
            **probe_kwargs,
        )
        try:
            async with probe_client:
                pass
            self._client = DeviceClient(
                host=resolved_host,
                username=self.username,
                password=self.password,
                **self.kwargs,
            )
            await self._client.__aenter__()
            return self._client
        except Exception:
            pass

    # If IP failed or was not provided, but we don't have a MAC yet
    if not resolved_mac and resolved_host:
        # Cross-Reference for Hub Routing (Fallback)
        if self.hub_client:
            for d in getattr(self.hub_client.cache._state, "devices", []):
                if getattr(d, "ip", None) == resolved_host:
                    d_id = getattr(d, "id", None)
                    resolved_mac = (d_id.root if hasattr(d_id, "root") else d_id) if d_id else None
                    break

        if not resolved_mac:
            raise ValueError(
                f"IP {resolved_host} is unreachable on local LAN and cannot be safely routed via Hub without a matching cache entry. Please provide a MAC address."
            )

    # Path 1: Target is a MAC Address (or was resolved to a MAC)
    if resolved_mac:
        from xovis.api.device.network_discovery import NetworkDiscoveryService

        local_ip = await NetworkDiscoveryService.resolve_mac_to_ip(resolved_mac)

        if local_ip and local_ip != resolved_host:
            probe_kwargs = self.kwargs.copy()
            probe_kwargs["timeout"] = 2.0
            probe_client = DeviceClient(
                host=local_ip,
                username=self.username,
                password=self.password,
                **probe_kwargs,
            )
            try:
                async with probe_client:
                    pass
                self._client = DeviceClient(
                    host=local_ip,
                    username=self.username,
                    password=self.password,
                    **self.kwargs,
                )
                await self._client.__aenter__()
                return self._client
            except Exception:
                pass

        if self.hub_client:
            try:
                self._client = await self.hub_client.connect_device(resolved_mac)
                await self._client.__aenter__()
                return self._client
            except Exception as e:
                raise ConnectionError(f"Could not connect to device {resolved_mac} via LAN or via Cloud Hub Tunnel: {e}") from e

    desc = resolved_mac or resolved_host or self.name or "unknown"
    raise ConnectionError(f"Device {desc} offline/unreachable on LAN and no HubClient/MAC is available for fallback.")
__aexit__(exc_type, exc_val, exc_tb) async

Gracefully releases the connected device client resources.

Parameters:

Name Type Description Default
exc_type Any

The exception type if an error occurred.

required
exc_val Any

The exception value.

required
exc_tb Any

The traceback object.

required
Source code in src/xovis/api/device/client.py
859
860
861
862
863
864
865
866
867
868
869
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    """
    Gracefully releases the connected device client resources.

    Args:
        exc_type (Any): The exception type if an error occurred.
        exc_val (Any): The exception value.
        exc_tb (Any): The traceback object.
    """
    if self._client:
        await self._client.__aexit__(exc_type, exc_val, exc_tb)
__init__(identifier=None, mac_address=None, host=None, name=None, hub_client=None, username='admin', password='pass', **kwargs)

Initializes the UnifiedDeviceClient.

Parameters:

Name Type Description Default
identifier Optional[str]

Optional single positional identifier (MAC, IP, or Name).

None
mac_address Optional[str]

Target MAC address.

None
host Optional[str]

Target IP address.

None
name Optional[str]

Target device name.

None
hub_client Optional[Any]

Optional HubClient instance.

None
username str

Username for LAN authentication.

'admin'
password str

Password for LAN authentication.

'pass'
**kwargs Any

Extra connection options.

{}
Source code in src/xovis/api/device/client.py
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
def __init__(
    self,
    identifier: Optional[str] = None,
    mac_address: Optional[str] = None,
    host: Optional[str] = None,
    name: Optional[str] = None,
    hub_client: Optional[Any] = None,
    username: str = "admin",
    password: str = "pass",
    **kwargs: Any,
) -> None:
    """
    Initializes the UnifiedDeviceClient.

    Args:
        identifier (Optional[str]): Optional single positional identifier (MAC, IP, or Name).
        mac_address (Optional[str]): Target MAC address.
        host (Optional[str]): Target IP address.
        name (Optional[str]): Target device name.
        hub_client (Optional[Any]): Optional HubClient instance.
        username (str): Username for LAN authentication.
        password (str): Password for LAN authentication.
        **kwargs (Any): Extra connection options.
    """
    self.mac_address = mac_address
    self.host = host
    self.name = name
    self.hub_client = hub_client
    self.username = username
    self.password = password
    self.kwargs = kwargs
    self._client: Optional[DeviceClient] = None

    if identifier:
        is_mac = bool(re.match(r"^([0-9A-Fa-f]{2}[:.-]){5}([0-9A-Fa-f]{2})$", identifier))
        is_ip = False
        try:
            ipaddress.ip_address(identifier)
            is_ip = True
        except ValueError:
            pass

        if is_mac:
            self.mac_address = identifier
        elif is_ip:
            self.host = identifier
        else:
            self.name = identifier

Functions:

xovis.api.device.cache

Xovis SDK - Device Configuration Cache Manager

This module resides within the State & Topology Plane, providing a stateful, graph-aware synchronization engine for edge sensor configurations. It implements the ConfigCacheManager which abstracts complex nested topologies into convenient, dot-notation accessors for Jupyter/REPL environments.

Classes

BulkDeviceFacade

Dynamic broadcasting facade that executes operations concurrently across child managers.

Source code in src/xovis/api/device/cache.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
class BulkDeviceFacade:
    """Dynamic broadcasting facade that executes operations concurrently across child managers."""

    def __init__(self, child_clients: list[Any], path: tuple[str, ...] = ()):
        """Initializes the BulkDeviceFacade.

        Args:
            child_clients (list[Any]): List of child DeviceClient instances.
            path (tuple[str, ...]): The property path to traverse on each client.
        """
        self._child_clients = child_clients
        self._path = path

    def __getattr__(self, name: str) -> "BulkDeviceFacade":
        """Intercepts method calls and builds the attribute chain.

        Args:
            name (str): The method or attribute name to traverse.

        Returns:
            BulkDeviceFacade: A new facade wrapping the updated path.
        """
        return BulkDeviceFacade(self._child_clients, self._path + (name,))

    async def __call__(self, *args: Any, **kwargs: Any) -> Any:
        """Executes the method concurrently across all clients."""
        from xovis.api.device.topology import BulkResult

        async def single_call(client: Any) -> Any:
            async with client as c:
                obj = c
                for attr in self._path:
                    obj = getattr(obj, attr)
                return await obj(*args, **kwargs)

        tasks = [asyncio.create_task(single_call(c)) for c in self._child_clients]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        bulk = BulkResult()
        for res in results:
            if isinstance(res, Exception):
                bulk.exceptions.append(res)
            else:
                bulk.successes.append(res)
        return bulk
Methods:
__call__(*args, **kwargs) async

Executes the method concurrently across all clients.

Source code in src/xovis/api/device/cache.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
async def __call__(self, *args: Any, **kwargs: Any) -> Any:
    """Executes the method concurrently across all clients."""
    from xovis.api.device.topology import BulkResult

    async def single_call(client: Any) -> Any:
        async with client as c:
            obj = c
            for attr in self._path:
                obj = getattr(obj, attr)
            return await obj(*args, **kwargs)

    tasks = [asyncio.create_task(single_call(c)) for c in self._child_clients]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    bulk = BulkResult()
    for res in results:
        if isinstance(res, Exception):
            bulk.exceptions.append(res)
        else:
            bulk.successes.append(res)
    return bulk
__getattr__(name)

Intercepts method calls and builds the attribute chain.

Parameters:

Name Type Description Default
name str

The method or attribute name to traverse.

required

Returns:

Name Type Description
BulkDeviceFacade BulkDeviceFacade

A new facade wrapping the updated path.

Source code in src/xovis/api/device/cache.py
340
341
342
343
344
345
346
347
348
349
def __getattr__(self, name: str) -> "BulkDeviceFacade":
    """Intercepts method calls and builds the attribute chain.

    Args:
        name (str): The method or attribute name to traverse.

    Returns:
        BulkDeviceFacade: A new facade wrapping the updated path.
    """
    return BulkDeviceFacade(self._child_clients, self._path + (name,))
__init__(child_clients, path=())

Initializes the BulkDeviceFacade.

Parameters:

Name Type Description Default
child_clients list[Any]

List of child DeviceClient instances.

required
path tuple[str, ...]

The property path to traverse on each client.

()
Source code in src/xovis/api/device/cache.py
330
331
332
333
334
335
336
337
338
def __init__(self, child_clients: list[Any], path: tuple[str, ...] = ()):
    """Initializes the BulkDeviceFacade.

    Args:
        child_clients (list[Any]): List of child DeviceClient instances.
        path (tuple[str, ...]): The property path to traverse on each client.
    """
    self._child_clients = child_clients
    self._path = path

CacheCollection

Bases: list, Generic[T]

Enhanced list subclass exposing REPL accessors.

Wraps standard lists with .by_name and .by_id properties to facilitate rapid resource discovery in the State & Topology Plane.

Source code in src/xovis/api/device/cache.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
class CacheCollection(list, Generic[T]):
    """
    Enhanced list subclass exposing REPL accessors.

    Wraps standard lists with `.by_name` and `.by_id` properties to facilitate
    rapid resource discovery in the State & Topology Plane.
    """

    def __init__(self, items: Optional[list[T]] = None):
        """
        Initializes the CacheCollection.

        Args:
            items (Optional[List[T]], optional): Initial list of elements. Defaults to None.
        """
        super().__init__()
        if items:
            self.extend(items)

    @property
    def by_name(self) -> REPLAccessor[T]:
        """
        Exposes elements via their 'name' attribute for dot-notation access.

        Returns:
            REPLAccessor[T]: A name-mapped dynamic accessor facade.
        """
        return REPLAccessor(self, key_attr="name")

    @property
    def by_id(self) -> REPLAccessor[T]:
        """
        Exposes elements via their 'id' attribute for dot-notation access.

        Returns:
            REPLAccessor[T]: An ID-mapped dynamic accessor facade.
        """
        return REPLAccessor(self, key_attr="id")
Attributes
by_id property

Exposes elements via their 'id' attribute for dot-notation access.

Returns:

Type Description
REPLAccessor[T]

REPLAccessor[T]: An ID-mapped dynamic accessor facade.

by_name property

Exposes elements via their 'name' attribute for dot-notation access.

Returns:

Type Description
REPLAccessor[T]

REPLAccessor[T]: A name-mapped dynamic accessor facade.

Methods:
__init__(items=None)

Initializes the CacheCollection.

Parameters:

Name Type Description Default
items Optional[List[T]]

Initial list of elements. Defaults to None.

None
Source code in src/xovis/api/device/cache.py
205
206
207
208
209
210
211
212
213
214
def __init__(self, items: Optional[list[T]] = None):
    """
    Initializes the CacheCollection.

    Args:
        items (Optional[List[T]], optional): Initial list of elements. Defaults to None.
    """
    super().__init__()
    if items:
        self.extend(items)

CachePaths

Helper namespace providing autocomplete and auto-suggestions for all local resources.

Source code in src/xovis/api/device/cache.py
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
class CachePaths:
    """Helper namespace providing autocomplete and auto-suggestions for all local resources."""

    BASE_DIR = Path("_local_resources")
    STATES_DIR = BASE_DIR / "states"
    SCHEMAS_DIR = BASE_DIR / "schemas"
    SAMPLES_DIR = BASE_DIR / "samples"

    DEVICE_STATE = STATES_DIR / "device_state.json"
    FLEET_STATE = STATES_DIR / "hub_fleet_state.json"

    @classmethod
    def get_system_cache_dir(cls) -> Path:
        """Resolves the system-level global cache directory in a platform-independent manner.

        Returns:
            Path: The resolved path to the system cache directory.
        """
        if sys.platform == "win32":
            local_appdata = os.environ.get("LOCALAPPDATA")
            if local_appdata:
                return Path(local_appdata) / "xovis" / "Cache"
            return Path.home() / "AppData" / "Local" / "xovis" / "Cache"
        xdg_cache = os.environ.get("XDG_CACHE_HOME")
        if xdg_cache:
            return Path(xdg_cache) / "xovis"
        return Path.home() / ".cache" / "xovis"

    @classmethod
    def list_available_states(cls) -> list[str]:
        """Discovers all available local state files. Highly useful in REPL/Notebooks.

        Returns:
            list[str]: Filenames of all available local states.
        """
        states = []
        if cls.STATES_DIR.exists():
            states.extend([f.name for f in cls.STATES_DIR.glob("*.json")])

        sys_states_dir = cls.get_system_cache_dir() / "states"
        if sys_states_dir.exists():
            states.extend([f.name for f in sys_states_dir.glob("*.json")])

        return sorted(list(set(states)))

    @classmethod
    def get_latest_state(cls) -> Path:
        """Helper to get the most recently modified state cache for quick debugging.

        Returns:
            Path: The Path to the most recently modified state cache, falling back to DEVICE_STATE.
        """
        candidates = []

        if cls.STATES_DIR.exists():
            candidates.extend(cls.STATES_DIR.glob("*.json"))

        sys_states_dir = cls.get_system_cache_dir() / "states"
        if sys_states_dir.exists():
            candidates.extend(sys_states_dir.glob("*.json"))

        candidates.extend([cls.DEVICE_STATE, cls.FLEET_STATE])

        existing = [p for p in candidates if p.exists()]
        if not existing:
            return cls.DEVICE_STATE
        return max(existing, key=lambda p: p.stat().st_mtime)
Methods:
get_latest_state() classmethod

Helper to get the most recently modified state cache for quick debugging.

Returns:

Name Type Description
Path Path

The Path to the most recently modified state cache, falling back to DEVICE_STATE.

Source code in src/xovis/api/device/cache.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
@classmethod
def get_latest_state(cls) -> Path:
    """Helper to get the most recently modified state cache for quick debugging.

    Returns:
        Path: The Path to the most recently modified state cache, falling back to DEVICE_STATE.
    """
    candidates = []

    if cls.STATES_DIR.exists():
        candidates.extend(cls.STATES_DIR.glob("*.json"))

    sys_states_dir = cls.get_system_cache_dir() / "states"
    if sys_states_dir.exists():
        candidates.extend(sys_states_dir.glob("*.json"))

    candidates.extend([cls.DEVICE_STATE, cls.FLEET_STATE])

    existing = [p for p in candidates if p.exists()]
    if not existing:
        return cls.DEVICE_STATE
    return max(existing, key=lambda p: p.stat().st_mtime)
get_system_cache_dir() classmethod

Resolves the system-level global cache directory in a platform-independent manner.

Returns:

Name Type Description
Path Path

The resolved path to the system cache directory.

Source code in src/xovis/api/device/cache.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@classmethod
def get_system_cache_dir(cls) -> Path:
    """Resolves the system-level global cache directory in a platform-independent manner.

    Returns:
        Path: The resolved path to the system cache directory.
    """
    if sys.platform == "win32":
        local_appdata = os.environ.get("LOCALAPPDATA")
        if local_appdata:
            return Path(local_appdata) / "xovis" / "Cache"
        return Path.home() / "AppData" / "Local" / "xovis" / "Cache"
    xdg_cache = os.environ.get("XDG_CACHE_HOME")
    if xdg_cache:
        return Path(xdg_cache) / "xovis"
    return Path.home() / ".cache" / "xovis"
list_available_states() classmethod

Discovers all available local state files. Highly useful in REPL/Notebooks.

Returns:

Type Description
list[str]

list[str]: Filenames of all available local states.

Source code in src/xovis/api/device/cache.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@classmethod
def list_available_states(cls) -> list[str]:
    """Discovers all available local state files. Highly useful in REPL/Notebooks.

    Returns:
        list[str]: Filenames of all available local states.
    """
    states = []
    if cls.STATES_DIR.exists():
        states.extend([f.name for f in cls.STATES_DIR.glob("*.json")])

    sys_states_dir = cls.get_system_cache_dir() / "states"
    if sys_states_dir.exists():
        states.extend([f.name for f in sys_states_dir.glob("*.json")])

    return sorted(list(set(states)))

CacheResource

Bases: BaseModel

Lightweight Pydantic model for cached resource metadata.

Source code in src/xovis/api/device/cache.py
281
282
283
284
285
286
287
288
289
class CacheResource(BaseModel):
    """
    Lightweight Pydantic model for cached resource metadata.
    """

    model_config = ConfigDict(extra="allow")
    id: Optional[int] = None
    name: Optional[str] = None
    type: Optional[str] = None

ChildDevicesAccessor

Bases: BaseControlPlane

Smart accessor bridging individual child lookups with grouped/bulk execution.

Source code in src/xovis/api/device/cache.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
class ChildDevicesAccessor(BaseControlPlane):
    """Smart accessor bridging individual child lookups with grouped/bulk execution."""

    def __init__(self, parent_context: Any, child_clients: list[Any]):
        """Initializes the ChildDevicesAccessor.

        Args:
            parent_context (Any): The parent ContextAccessor.
            child_clients (list[Any]): Mapped physical child clients.
        """
        self._parent_context = parent_context
        self._child_clients = child_clients
        self.by_name = REPLAccessor(child_clients, key_attr="name")

    @property
    def connections(self) -> CacheCollection[CacheResource]:
        """Aggregates and flattens connections from all physical child caches.

        Returns:
            CacheCollection[CacheResource]: Combined connection configurations.
        """
        merged = []
        for client in self._child_clients:
            merged.extend(client.cache.singlesensor.connections)
        return CacheCollection(merged)

    @property
    def zones(self) -> CacheCollection[CacheResource]:
        """Aggregates and flattens scene zones from all physical child caches.

        Returns:
            CacheCollection[CacheResource]: Combined zone configurations.
        """
        merged = []
        for client in self._child_clients:
            merged.extend(client.cache.singlesensor.zones)
        return CacheCollection(merged)

    @property
    def lines(self) -> CacheCollection[CacheResource]:
        """Aggregates and flattens scene lines from all physical child caches.

        Returns:
            CacheCollection[CacheResource]: Combined line configurations.
        """
        merged = []
        for client in self._child_clients:
            merged.extend(client.cache.singlesensor.lines)
        return CacheCollection(merged)

    @property
    def agents(self) -> CacheCollection[CacheResource]:
        """Aggregates and flattens agents from all physical child caches.

        Returns:
            CacheCollection[CacheResource]: Combined agent configurations.
        """
        merged = []
        for client in self._child_clients:
            merged.extend(client.cache.singlesensor.agents)
        return CacheCollection(merged)

    @property
    def logics(self) -> CacheCollection[CacheResource]:
        """Aggregates and flattens logics from all physical child caches.

        Returns:
            CacheCollection[CacheResource]: Combined logic configurations.
        """
        merged = []
        for client in self._child_clients:
            merged.extend(client.cache.singlesensor.logics)
        return CacheCollection(merged)

    @property
    def modifiers(self) -> CacheCollection[CacheResource]:
        """Aggregates and flattens modifiers from all physical child caches.

        Returns:
            CacheCollection[CacheResource]: Combined modifier configurations.
        """
        merged = []
        for client in self._child_clients:
            merged.extend(client.cache.singlesensor.modifiers)
        return CacheCollection(merged)

    @property
    def counters(self) -> CacheCollection[CacheResource]:
        """Aggregates and flattens counters from all physical child caches.

        Returns:
            CacheCollection[CacheResource]: Combined counter configurations.
        """
        merged = []
        for client in self._child_clients:
            merged.extend(client.cache.singlesensor.counters)
        return CacheCollection(merged)

    @property
    def masks(self) -> CacheCollection[CacheResource]:
        """Aggregates and flattens masks from all physical child caches.

        Returns:
            CacheCollection[CacheResource]: Combined mask configurations.
        """
        merged = []
        for client in self._child_clients:
            merged.extend(client.cache.singlesensor.masks)
        return CacheCollection(merged)

    @property
    def layers(self) -> CacheCollection[CacheResource]:
        """Aggregates and flattens layers from all physical child caches.

        Returns:
            CacheCollection[CacheResource]: Combined layer configurations.
        """
        merged = []
        for client in self._child_clients:
            merged.extend(client.cache.singlesensor.layers)
        return CacheCollection(merged)

    def __getattr__(self, name: str) -> Any:
        """Provides direct access to bulk managers.

        Args:
            name (str): Attribute name to delegate.

        Returns:
            Any: BulkDeviceFacade wrapping the target manager attribute.

        Raises:
            AttributeError: If the manager is not a standard supported device manager.
        """
        standard_managers = {
            "datapush",
            "analytics",
            "system",
            "network",
            "time",
            "update",
            "scene",
            "history",
            "privacy",
            "topology",
            "users",
            "itxpt",
            "multisensors",
            "images",
        }
        if name in standard_managers:
            return BulkDeviceFacade(self._child_clients, (name,))
        raise AttributeError(f"Attribute '{name}' not found on ChildDevicesAccessor.")

    def __dir__(self) -> list[str]:
        """
        Provides IDE autocomplete for standard managers.
        """
        return list(
            set(super().__dir__())
            | {
                "datapush",
                "system",
                "network",
                "time",
                "update",
                "analytics",
                "scene",
                "history",
                "privacy",
                "topology",
                "users",
                "itxpt",
                "multisensors",
                "images",
            }
        )
Attributes
agents property

Aggregates and flattens agents from all physical child caches.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined agent configurations.

connections property

Aggregates and flattens connections from all physical child caches.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined connection configurations.

counters property

Aggregates and flattens counters from all physical child caches.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined counter configurations.

layers property

Aggregates and flattens layers from all physical child caches.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined layer configurations.

lines property

Aggregates and flattens scene lines from all physical child caches.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined line configurations.

logics property

Aggregates and flattens logics from all physical child caches.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined logic configurations.

masks property

Aggregates and flattens masks from all physical child caches.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined mask configurations.

modifiers property

Aggregates and flattens modifiers from all physical child caches.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined modifier configurations.

zones property

Aggregates and flattens scene zones from all physical child caches.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined zone configurations.

Methods:
__dir__()

Provides IDE autocomplete for standard managers.

Source code in src/xovis/api/device/cache.py
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
def __dir__(self) -> list[str]:
    """
    Provides IDE autocomplete for standard managers.
    """
    return list(
        set(super().__dir__())
        | {
            "datapush",
            "system",
            "network",
            "time",
            "update",
            "analytics",
            "scene",
            "history",
            "privacy",
            "topology",
            "users",
            "itxpt",
            "multisensors",
            "images",
        }
    )
__getattr__(name)

Provides direct access to bulk managers.

Parameters:

Name Type Description Default
name str

Attribute name to delegate.

required

Returns:

Name Type Description
Any Any

BulkDeviceFacade wrapping the target manager attribute.

Raises:

Type Description
AttributeError

If the manager is not a standard supported device manager.

Source code in src/xovis/api/device/cache.py
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
def __getattr__(self, name: str) -> Any:
    """Provides direct access to bulk managers.

    Args:
        name (str): Attribute name to delegate.

    Returns:
        Any: BulkDeviceFacade wrapping the target manager attribute.

    Raises:
        AttributeError: If the manager is not a standard supported device manager.
    """
    standard_managers = {
        "datapush",
        "analytics",
        "system",
        "network",
        "time",
        "update",
        "scene",
        "history",
        "privacy",
        "topology",
        "users",
        "itxpt",
        "multisensors",
        "images",
    }
    if name in standard_managers:
        return BulkDeviceFacade(self._child_clients, (name,))
    raise AttributeError(f"Attribute '{name}' not found on ChildDevicesAccessor.")
__init__(parent_context, child_clients)

Initializes the ChildDevicesAccessor.

Parameters:

Name Type Description Default
parent_context Any

The parent ContextAccessor.

required
child_clients list[Any]

Mapped physical child clients.

required
Source code in src/xovis/api/device/cache.py
377
378
379
380
381
382
383
384
385
386
def __init__(self, parent_context: Any, child_clients: list[Any]):
    """Initializes the ChildDevicesAccessor.

    Args:
        parent_context (Any): The parent ContextAccessor.
        child_clients (list[Any]): Mapped physical child clients.
    """
    self._parent_context = parent_context
    self._child_clients = child_clients
    self.by_name = REPLAccessor(child_clients, key_attr="name")

ConfigCacheManager

Stateful Configuration Synchronizer and Topology Graph Manager.

Coordinates the background polling of edge sensor state hashes and performs concurrent synchronization of the full configuration graph. Implements GC protection for long-running background tasks and partitions state by physical and virtual contexts.

Source code in src/xovis/api/device/cache.py
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
class ConfigCacheManager:
    """
    Stateful Configuration Synchronizer and Topology Graph Manager.

    Coordinates the background polling of edge sensor state hashes and performs
    concurrent synchronization of the full configuration graph. Implements
    GC protection for long-running background tasks and partitions state
    by physical and virtual contexts.
    """

    def __init__(
        self,
        http_client: XovisHTTPClient,
        strategy: CacheStrategy,
        ttl_seconds: float,
        poll_interval: float,
        auto_persist_path: Optional[str] = None,
        persistence_dir: Optional[str] = None,
        cache_child_devices: bool = False,
    ):
        """
        Initializes the ConfigCacheManager.

        Args:
            http_client (XovisHTTPClient): The Control Plane HTTP engine.
            strategy (CacheStrategy): The synchronization strategy (MANUAL, LAZY, or BACKGROUND).
            ttl_seconds (float): Time-to-live for cached entries.
            poll_interval (float): Frequency of background state checks.
            auto_persist_path (Optional[str], optional): Path to a JSON file for
                offline-first state persistence. Defaults to None.
            persistence_dir (Optional[str], optional): Path to a directory for
                automated state persistence. Defaults to None.
            cache_child_devices (bool, optional): Whether to recursively cache child devices. Defaults to False.
        """
        self._http_client = http_client
        self.strategy = strategy
        self.ttl_seconds = ttl_seconds
        self.poll_interval = poll_interval
        self.auto_persist_path = auto_persist_path
        self.persistence_dir = persistence_dir
        self.cache_child_devices = cache_child_devices

        self._background_tasks: set[asyncio.Task] = set()
        self._is_running = False
        self._parent_client: Optional[Any] = None

        self._state = HostStateBucket()

    @property
    def singlesensor(self) -> ContextAccessor:
        """
        Accessor for the physical lens context.

        Returns:
            ContextAccessor: Facade for the 'singlesensor' state bucket.
        """
        bucket = self._state.contexts.setdefault("singlesensor", ContextStateBucket())
        return ContextAccessor(bucket, name="singlesensor", parent_client=self._parent_client)

    @property
    def multisensors(self) -> REPLAccessor[ContextAccessor]:
        """
        Dynamic accessor for virtual stitched environments (Multisensors).

        Returns:
            REPLAccessor[ContextAccessor]: Mapped context IDs to state facades.
        """
        accessors = []
        for cid, cb in self._state.contexts.items():
            if cid == "singlesensor":
                continue
            accessors.append(ContextAccessor(cb, name=str(cid), parent_client=self._parent_client))

        return REPLAccessor(accessors, key_attr="name")

    @multisensors.setter
    def multisensors(self, contexts: list[Any]) -> None:
        """
        Updates the internal state buckets from discovered multisensor contexts.

        Args:
            contexts (List[MultisensorContext]): The list of discovered contexts.
        """
        # We preserve the 'singlesensor' bucket if it exists
        new_contexts = {}
        if "singlesensor" in self._state.contexts:
            new_contexts["singlesensor"] = self._state.contexts["singlesensor"]

        for ctx in contexts:
            ms_id = str(ctx.ms_id)
            # Carry over existing data if we already had a bucket for this ID
            if ms_id in self._state.contexts:
                new_contexts[ms_id] = self._state.contexts[ms_id]
            else:
                new_contexts[ms_id] = ContextStateBucket()

        self._state.contexts = new_contexts

    @property
    def buckets(self) -> REPLAccessor[ContextAccessor]:
        """
        Unified accessor for all state buckets (physical and virtual).

        Returns:
            REPLAccessor[ContextAccessor]: All contexts mapped by their ID/name.
        """
        accessors = []
        for cid, cb in self._state.contexts.items():
            accessors.append(ContextAccessor(cb, name=str(cid), parent_client=self._parent_client))
        return REPLAccessor(accessors, key_attr="name")

    @property
    def agents(self) -> CacheCollection[CacheResource]:
        """Legacy shim mapping to singlesensor.agents."""
        return self.singlesensor.agents

    @property
    def connections(self) -> CacheCollection[CacheResource]:
        """Legacy shim mapping to singlesensor.connections."""
        return self.singlesensor.connections

    @property
    def zones(self) -> CacheCollection[CacheResource]:
        """Legacy shim mapping to singlesensor.zones."""
        return self.singlesensor.zones

    @property
    def lines(self) -> CacheCollection[CacheResource]:
        """Legacy shim mapping to singlesensor.lines."""
        return self.singlesensor.lines

    @property
    def geometries(self) -> CacheCollection[CacheResource]:
        """
        Legacy shim combining zones and lines from the singlesensor context.

        Returns:
            CacheCollection[CacheResource]: Combined collection of zones and lines.
        """
        return CacheCollection(list(self.zones) + list(self.lines))

    @property
    def logics(self) -> CacheCollection[CacheResource]:
        """Legacy shim mapping to singlesensor.logics."""
        return self.singlesensor.logics

    @property
    def modifiers(self) -> CacheCollection[CacheResource]:
        """Legacy shim mapping to singlesensor.modifiers."""
        return self.singlesensor.modifiers

    @property
    def counters(self) -> CacheCollection[CacheResource]:
        """Legacy shim mapping to singlesensor.counters."""
        return self.singlesensor.counters

    @property
    def masks(self) -> CacheCollection[CacheResource]:
        """Legacy shim mapping to singlesensor.masks."""
        return self.singlesensor.masks

    @property
    def layers(self) -> CacheCollection[CacheResource]:
        """Legacy shim mapping to singlesensor.layers."""
        return self.singlesensor.layers

    async def start(self) -> None:
        """
        Starts the background configuration watcher.

        Performs an initial synchronization and spawns a background loop with
        hard-referenced tasks to prevent mid-execution garbage collection.
        Also handles auto-loading from disk if persistence is enabled.
        """
        if self.auto_persist_path:
            try:
                await self.load_from_disk()
            except Exception as e:
                logger.warning(f"Failed to load cache from disk: {e}")

        is_watcher = getattr(self.strategy, "value", str(self.strategy)) == "BACKGROUND_WATCHER"
        if config.CACHE_ENABLED or is_watcher:
            if not self._is_running:
                self._is_running = True

                await self.sync()

                task = asyncio.create_task(self._watcher_loop())
                self._background_tasks.add(task)
                task.add_done_callback(self._background_tasks.discard)

    async def stop(self) -> None:
        """
        Cleanly halts background synchronization loops.

        Ensures all tasks are cancelled and awaited to prevent socket leaks.
        """
        self._is_running = False
        for task in list(self._background_tasks):
            if not task.done():
                task.cancel()

        if self._background_tasks:
            await asyncio.gather(*self._background_tasks, return_exceptions=True)
        self._background_tasks.clear()

    async def _watcher_loop(self) -> None:
        """
        Polls the API config state hash for remote mutations.

        Triggered by the BACKGROUND_WATCHER strategy. If a checksum change is
        detected, initiates a full graph synchronization.
        """
        interval = self.poll_interval if self.poll_interval else config.POLL_INTERVAL_SECONDS
        while self._is_running:
            try:
                await asyncio.sleep(interval)
                response = await self._http_client.get("/api/v5/config/state")
                data = response.json()

                new_checksum = data.get("state", {}).get("checksum", data.get("checksum"))

                if new_checksum and new_checksum != self._state.checksum:
                    logger.info(f"Configuration change detected (checksum: {new_checksum}). Syncing cache...")
                    self._state.checksum = new_checksum
                    await self.sync()

                    if self.auto_persist_path or self.persistence_dir:
                        await self.save_to_disk()
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.warning(f"Background watcher failed to sync: {e}")

    async def sync(self) -> None:
        """
        Pulls the entire topology graph concurrently, isolating each context.

        Performs multisensor discovery, builds a concurrent task matrix for all
        endpoints (DataPush, Scene, Analytics), and populates partitioned
        state buckets.

        Raises:
            Exception: Logged as an error if the synchronization fails.
        """
        try:
            contexts_to_sync = ["singlesensor"]
            try:
                ms_resp = await self._http_client.get("/api/v5/multisensors/status")
                data = ms_resp.json()
                if isinstance(data, list):
                    ms_list = data
                else:
                    ms_list = data.get("multisensors_status") or data.get("multisensors")
                    if ms_list is None and isinstance(data, dict) and "multisensor_id" in data:
                        ms_list = [data]
                if isinstance(ms_list, list):
                    for ms in ms_list:
                        ms_id = ms.get("multisensor_id") or ms.get("id") or ms.get("custom_id")
                        if ms_id is not None:
                            contexts_to_sync.append(str(ms_id))
            except (XovisClientError, Exception) as e:
                logger.debug(f"Multisensor discovery skipped: {e}")

            tasks = []
            context_map = []

            endpoints = [
                ("agents", "/data/push/agents"),
                ("connections", "/data/push/connections"),
                ("geometries", "/scene/geometries"),
                ("logics", "/analysis/logics"),
                ("modifiers", "/analysis/modifiers"),
                ("counters", "/analysis/counters"),
                ("scene_masks", "/scene/masks"),
                ("layers", "/scene/layers"),
            ]

            for ctx in contexts_to_sync:
                prefix = f"/api/v5/multisensors/{ctx}" if ctx != "singlesensor" else "/api/v5/singlesensor"
                for _, ep in endpoints:
                    tasks.append(self._http_client.get(f"{prefix}{ep}"))
                context_map.append(ctx)

            results = await asyncio.gather(*tasks, return_exceptions=True)

            stride = len(endpoints)
            for i, ctx in enumerate(context_map):
                ctx_results = results[i * stride : (i + 1) * stride]
                bucket = ContextStateBucket()

                def extract_data(res, key, entity_name=None, endpoint=None):
                    if isinstance(res, Exception):
                        return []
                    try:
                        data = res.json()
                        raw_items = data.get(key, data.get("root", []))

                        # Discovery Crawler: Capture unknown fields
                        if entity_name and raw_items and isinstance(raw_items, list):
                            known_fields = CacheResource.model_fields.keys()
                            for item in raw_items:
                                if isinstance(item, dict):
                                    discovery_manager.capture(
                                        entity_name=entity_name,
                                        raw_data=item,
                                        known_fields=set(known_fields),
                                        endpoint=endpoint,
                                    )
                        return raw_items
                    except Exception:
                        return []

                bucket.agents = [
                    CacheResource.model_validate(a) for a in extract_data(ctx_results[0], "agents", "Agent", f"{prefix}/data/push/agents")
                ]
                bucket.connections = [
                    CacheResource.model_validate(c)
                    for c in extract_data(
                        ctx_results[1],
                        "connections",
                        "Connection",
                        f"{prefix}/data/push/connections",
                    )
                ]

                geoms_data = extract_data(ctx_results[2], "geometries", "Geometry", f"{prefix}/scene/geometries")
                geoms = [CacheResource.model_validate(g) for g in geoms_data]
                bucket.zones = [g for g in geoms if g.type == "ZONE"]
                bucket.lines = [g for g in geoms if g.type == "LINE"]

                bucket.logics = [
                    CacheResource.model_validate(l) for l in extract_data(ctx_results[3], "logics", "Logic", f"{prefix}/analysis/logics")
                ]
                bucket.modifiers = [
                    CacheResource.model_validate(m) for m in extract_data(ctx_results[4], "modifiers", "Modifier", f"{prefix}/analysis/modifiers")
                ]
                bucket.counters = [
                    CacheResource.model_validate(c) for c in extract_data(ctx_results[5], "counters", "Counter", f"{prefix}/analysis/counters")
                ]
                bucket.masks = [
                    CacheResource.model_validate(sm) for sm in extract_data(ctx_results[6], "scene_masks", "Mask", f"{prefix}/scene/masks")
                ]
                bucket.layers = [CacheResource.model_validate(ly) for ly in extract_data(ctx_results[7], "layers", "Layer", f"{prefix}/scene/layers")]

                self._state.contexts[ctx] = bucket

            ms_contexts = [ctx for ctx in context_map if ctx != "singlesensor"]
            if ms_contexts:
                sensor_tasks = [self._http_client.get(f"/api/v5/multisensors/{ctx}/sensors") for ctx in ms_contexts]
                sensor_results = await asyncio.gather(*sensor_tasks, return_exceptions=True)
                for ctx, res in zip(ms_contexts, sensor_results):
                    if not isinstance(res, Exception) and res.status_code == 200:
                        try:
                            from xovis.api.device.topology import MultisensorChildrenResponse

                            payload = MultisensorChildrenResponse.model_validate(res.json())
                            self._state.contexts[ctx].child_sensors = [
                                TopologyNodeInfo(
                                    mac_address=child.mac_address,
                                    ip_address=child.ip_address,
                                    name=child.name,
                                    group=child.group,
                                    status=child.status,
                                )
                                for child in payload.sensors
                            ]
                        except Exception as e:
                            logger.debug(f"Failed parsing child sensors for context {ctx}: {e}")

            if self.cache_child_devices:
                from xovis.api.device.client import DeviceClient

                child_sync_tasks = []
                for ctx in ms_contexts:
                    bucket = self._state.contexts.get(ctx)
                    if bucket and bucket.child_sensors:
                        for sensor in bucket.child_sensors:
                            if not sensor.ip_address:
                                continue
                            username = "admin"
                            password = ""
                            use_ntlm = False
                            if self._parent_client:
                                username = self._parent_client._auth.username
                                password = self._parent_client._auth.password
                                use_ntlm = self._parent_client._auth.use_ntlm
                            child_client = DeviceClient(
                                host=sensor.ip_address,
                                username=username,
                                password=password,
                                use_ntlm=use_ntlm,
                                cache_child_devices=False,
                            )
                            child_sync_tasks.append(child_client.cache.sync())
                if child_sync_tasks:
                    await asyncio.gather(*child_sync_tasks, return_exceptions=True)

            if True:
                await self.save_to_disk()

        except Exception as e:
            logger.error(f"Failed to synchronize cache: {e}")

    def export_to_file(self, file_path: str) -> None:
        """
        Exports the entire nested state graph to a JSON file.

        Args:
            file_path (str): The destination file path.
        """
        with open(file_path, "w") as f:
            f.write(self._state.model_dump_json())

    def load_from_file(self, file_path: str) -> None:
        """
        Loads a nested state graph from a JSON file into memory.

        Args:
            file_path (str): The source JSON file path.
        """
        with open(file_path) as f:
            self._state = HostStateBucket.model_validate_json(f.read())

    def _ensure_directory_or_fallback(self, path: Path) -> Optional[Path]:
        """Ensures the directory for the given path is writeable or falls back.

        Implements the 3-Tier cache folder creation and fallback strategy.

        Args:
            path (Path): The desired target file path.

        Returns:
            Optional[Path]: The writeable target path, or None if falling back
                to memory-only.
        """
        if getattr(self, "_memory_only", False):
            return None

        parent = path.parent
        try:
            parent.mkdir(parents=True, exist_ok=True)
            return path
        except (PermissionError, OSError) as exc:
            logger.warning(f"Local directory creation failed at {parent} ({exc}). Attempting global system cache fallback.")

        try:
            try:
                rel_parts = path.relative_to(CachePaths.BASE_DIR)
                sys_target = CachePaths.get_system_cache_dir() / rel_parts
            except ValueError:
                sys_target = CachePaths.get_system_cache_dir() / path.name

            sys_parent = sys_target.parent
            sys_parent.mkdir(parents=True, exist_ok=True)
            return sys_target
        except (PermissionError, OSError) as exc:
            logger.warning(
                f"Unable to write to system-level cache workspace ({exc}). "
                f"Falling back to temporary memory-only caching. "
                f"To persist cache, ensure write permissions exist."
            )

        self._memory_only = True
        return None

    async def _resolve_persist_path(self) -> Optional[str]:
        """Resolves the final persistence path, dynamically if persistence_dir is used.

        Returns:
            Optional[str]: The absolute path to the state file, or None.
        """
        if getattr(self, "_memory_only", False):
            return None

        from urllib.parse import urlparse

        parsed = urlparse(str(self._http_client.base_url))
        host = parsed.netloc or parsed.path
        host_clean = host.split(":")[0].replace(".", "_").replace(":", "_")

        resolved_path: Optional[Path] = None

        if self.auto_persist_path:
            resolved_path = Path(self.auto_persist_path)
        elif self.persistence_dir:
            try:
                resp = await self._http_client.get("/api/v5/device/info")
                info = resp.json()
                mac = info.get("macAddress", "unknown").replace(":", "-")
                resolved_path = Path(self.persistence_dir) / f"{mac}.json"
            except Exception as e:
                logger.warning(f"Could not resolve dynamic persistence path: {e}")
                return None
        else:
            host_state_path = CachePaths.STATES_DIR / f"state_{host_clean}.json"
            device_state_path = CachePaths.DEVICE_STATE

            if host_state_path.exists():
                resolved_path = host_state_path
            elif device_state_path.exists():
                resolved_path = device_state_path
            else:
                resolved_path = host_state_path

        if resolved_path:
            final_path = self._ensure_directory_or_fallback(resolved_path)
            return str(final_path) if final_path else None

        return None

    async def save_to_disk(self) -> None:
        """
        Safely serializes the HostStateBucket to disk.

        Uses asyncio.to_thread to prevent blocking the event loop during
        file I/O.
        """
        path = await self._resolve_persist_path()
        if not path:
            return

        def _save():
            with open(path, "w") as f:
                f.write(self._state.model_dump_json(indent=2))

        await asyncio.to_thread(_save)

    async def load_from_disk(self) -> None:
        """
        Deserializes the HostStateBucket from disk.

        Uses asyncio.to_thread to prevent blocking the event loop during
        file I/O.
        """
        path = await self._resolve_persist_path()
        if not path:
            return

        import os

        if not await asyncio.to_thread(os.path.exists, path):
            return

        def _load():
            with open(path) as f:
                return f.read()

        data = await asyncio.to_thread(_load)
        self._state = HostStateBucket.model_validate_json(data)
Attributes
agents property

Legacy shim mapping to singlesensor.agents.

buckets property

Unified accessor for all state buckets (physical and virtual).

Returns:

Type Description
REPLAccessor[ContextAccessor]

REPLAccessor[ContextAccessor]: All contexts mapped by their ID/name.

connections property

Legacy shim mapping to singlesensor.connections.

counters property

Legacy shim mapping to singlesensor.counters.

geometries property

Legacy shim combining zones and lines from the singlesensor context.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Combined collection of zones and lines.

layers property

Legacy shim mapping to singlesensor.layers.

lines property

Legacy shim mapping to singlesensor.lines.

logics property

Legacy shim mapping to singlesensor.logics.

masks property

Legacy shim mapping to singlesensor.masks.

modifiers property

Legacy shim mapping to singlesensor.modifiers.

multisensors property writable

Dynamic accessor for virtual stitched environments (Multisensors).

Returns:

Type Description
REPLAccessor[ContextAccessor]

REPLAccessor[ContextAccessor]: Mapped context IDs to state facades.

singlesensor property

Accessor for the physical lens context.

Returns:

Name Type Description
ContextAccessor ContextAccessor

Facade for the 'singlesensor' state bucket.

zones property

Legacy shim mapping to singlesensor.zones.

Methods:
__init__(http_client, strategy, ttl_seconds, poll_interval, auto_persist_path=None, persistence_dir=None, cache_child_devices=False)

Initializes the ConfigCacheManager.

Parameters:

Name Type Description Default
http_client XovisHTTPClient

The Control Plane HTTP engine.

required
strategy CacheStrategy

The synchronization strategy (MANUAL, LAZY, or BACKGROUND).

required
ttl_seconds float

Time-to-live for cached entries.

required
poll_interval float

Frequency of background state checks.

required
auto_persist_path Optional[str]

Path to a JSON file for offline-first state persistence. Defaults to None.

None
persistence_dir Optional[str]

Path to a directory for automated state persistence. Defaults to None.

None
cache_child_devices bool

Whether to recursively cache child devices. Defaults to False.

False
Source code in src/xovis/api/device/cache.py
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
def __init__(
    self,
    http_client: XovisHTTPClient,
    strategy: CacheStrategy,
    ttl_seconds: float,
    poll_interval: float,
    auto_persist_path: Optional[str] = None,
    persistence_dir: Optional[str] = None,
    cache_child_devices: bool = False,
):
    """
    Initializes the ConfigCacheManager.

    Args:
        http_client (XovisHTTPClient): The Control Plane HTTP engine.
        strategy (CacheStrategy): The synchronization strategy (MANUAL, LAZY, or BACKGROUND).
        ttl_seconds (float): Time-to-live for cached entries.
        poll_interval (float): Frequency of background state checks.
        auto_persist_path (Optional[str], optional): Path to a JSON file for
            offline-first state persistence. Defaults to None.
        persistence_dir (Optional[str], optional): Path to a directory for
            automated state persistence. Defaults to None.
        cache_child_devices (bool, optional): Whether to recursively cache child devices. Defaults to False.
    """
    self._http_client = http_client
    self.strategy = strategy
    self.ttl_seconds = ttl_seconds
    self.poll_interval = poll_interval
    self.auto_persist_path = auto_persist_path
    self.persistence_dir = persistence_dir
    self.cache_child_devices = cache_child_devices

    self._background_tasks: set[asyncio.Task] = set()
    self._is_running = False
    self._parent_client: Optional[Any] = None

    self._state = HostStateBucket()
_ensure_directory_or_fallback(path)

Ensures the directory for the given path is writeable or falls back.

Implements the 3-Tier cache folder creation and fallback strategy.

Parameters:

Name Type Description Default
path Path

The desired target file path.

required

Returns:

Type Description
Optional[Path]

Optional[Path]: The writeable target path, or None if falling back to memory-only.

Source code in src/xovis/api/device/cache.py
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
def _ensure_directory_or_fallback(self, path: Path) -> Optional[Path]:
    """Ensures the directory for the given path is writeable or falls back.

    Implements the 3-Tier cache folder creation and fallback strategy.

    Args:
        path (Path): The desired target file path.

    Returns:
        Optional[Path]: The writeable target path, or None if falling back
            to memory-only.
    """
    if getattr(self, "_memory_only", False):
        return None

    parent = path.parent
    try:
        parent.mkdir(parents=True, exist_ok=True)
        return path
    except (PermissionError, OSError) as exc:
        logger.warning(f"Local directory creation failed at {parent} ({exc}). Attempting global system cache fallback.")

    try:
        try:
            rel_parts = path.relative_to(CachePaths.BASE_DIR)
            sys_target = CachePaths.get_system_cache_dir() / rel_parts
        except ValueError:
            sys_target = CachePaths.get_system_cache_dir() / path.name

        sys_parent = sys_target.parent
        sys_parent.mkdir(parents=True, exist_ok=True)
        return sys_target
    except (PermissionError, OSError) as exc:
        logger.warning(
            f"Unable to write to system-level cache workspace ({exc}). "
            f"Falling back to temporary memory-only caching. "
            f"To persist cache, ensure write permissions exist."
        )

    self._memory_only = True
    return None
_resolve_persist_path() async

Resolves the final persistence path, dynamically if persistence_dir is used.

Returns:

Type Description
Optional[str]

Optional[str]: The absolute path to the state file, or None.

Source code in src/xovis/api/device/cache.py
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
async def _resolve_persist_path(self) -> Optional[str]:
    """Resolves the final persistence path, dynamically if persistence_dir is used.

    Returns:
        Optional[str]: The absolute path to the state file, or None.
    """
    if getattr(self, "_memory_only", False):
        return None

    from urllib.parse import urlparse

    parsed = urlparse(str(self._http_client.base_url))
    host = parsed.netloc or parsed.path
    host_clean = host.split(":")[0].replace(".", "_").replace(":", "_")

    resolved_path: Optional[Path] = None

    if self.auto_persist_path:
        resolved_path = Path(self.auto_persist_path)
    elif self.persistence_dir:
        try:
            resp = await self._http_client.get("/api/v5/device/info")
            info = resp.json()
            mac = info.get("macAddress", "unknown").replace(":", "-")
            resolved_path = Path(self.persistence_dir) / f"{mac}.json"
        except Exception as e:
            logger.warning(f"Could not resolve dynamic persistence path: {e}")
            return None
    else:
        host_state_path = CachePaths.STATES_DIR / f"state_{host_clean}.json"
        device_state_path = CachePaths.DEVICE_STATE

        if host_state_path.exists():
            resolved_path = host_state_path
        elif device_state_path.exists():
            resolved_path = device_state_path
        else:
            resolved_path = host_state_path

    if resolved_path:
        final_path = self._ensure_directory_or_fallback(resolved_path)
        return str(final_path) if final_path else None

    return None
_watcher_loop() async

Polls the API config state hash for remote mutations.

Triggered by the BACKGROUND_WATCHER strategy. If a checksum change is detected, initiates a full graph synchronization.

Source code in src/xovis/api/device/cache.py
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
async def _watcher_loop(self) -> None:
    """
    Polls the API config state hash for remote mutations.

    Triggered by the BACKGROUND_WATCHER strategy. If a checksum change is
    detected, initiates a full graph synchronization.
    """
    interval = self.poll_interval if self.poll_interval else config.POLL_INTERVAL_SECONDS
    while self._is_running:
        try:
            await asyncio.sleep(interval)
            response = await self._http_client.get("/api/v5/config/state")
            data = response.json()

            new_checksum = data.get("state", {}).get("checksum", data.get("checksum"))

            if new_checksum and new_checksum != self._state.checksum:
                logger.info(f"Configuration change detected (checksum: {new_checksum}). Syncing cache...")
                self._state.checksum = new_checksum
                await self.sync()

                if self.auto_persist_path or self.persistence_dir:
                    await self.save_to_disk()
        except asyncio.CancelledError:
            break
        except Exception as e:
            logger.warning(f"Background watcher failed to sync: {e}")
export_to_file(file_path)

Exports the entire nested state graph to a JSON file.

Parameters:

Name Type Description Default
file_path str

The destination file path.

required
Source code in src/xovis/api/device/cache.py
1166
1167
1168
1169
1170
1171
1172
1173
1174
def export_to_file(self, file_path: str) -> None:
    """
    Exports the entire nested state graph to a JSON file.

    Args:
        file_path (str): The destination file path.
    """
    with open(file_path, "w") as f:
        f.write(self._state.model_dump_json())
load_from_disk() async

Deserializes the HostStateBucket from disk.

Uses asyncio.to_thread to prevent blocking the event loop during file I/O.

Source code in src/xovis/api/device/cache.py
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
async def load_from_disk(self) -> None:
    """
    Deserializes the HostStateBucket from disk.

    Uses asyncio.to_thread to prevent blocking the event loop during
    file I/O.
    """
    path = await self._resolve_persist_path()
    if not path:
        return

    import os

    if not await asyncio.to_thread(os.path.exists, path):
        return

    def _load():
        with open(path) as f:
            return f.read()

    data = await asyncio.to_thread(_load)
    self._state = HostStateBucket.model_validate_json(data)
load_from_file(file_path)

Loads a nested state graph from a JSON file into memory.

Parameters:

Name Type Description Default
file_path str

The source JSON file path.

required
Source code in src/xovis/api/device/cache.py
1176
1177
1178
1179
1180
1181
1182
1183
1184
def load_from_file(self, file_path: str) -> None:
    """
    Loads a nested state graph from a JSON file into memory.

    Args:
        file_path (str): The source JSON file path.
    """
    with open(file_path) as f:
        self._state = HostStateBucket.model_validate_json(f.read())
save_to_disk() async

Safely serializes the HostStateBucket to disk.

Uses asyncio.to_thread to prevent blocking the event loop during file I/O.

Source code in src/xovis/api/device/cache.py
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
async def save_to_disk(self) -> None:
    """
    Safely serializes the HostStateBucket to disk.

    Uses asyncio.to_thread to prevent blocking the event loop during
    file I/O.
    """
    path = await self._resolve_persist_path()
    if not path:
        return

    def _save():
        with open(path, "w") as f:
            f.write(self._state.model_dump_json(indent=2))

    await asyncio.to_thread(_save)
start() async

Starts the background configuration watcher.

Performs an initial synchronization and spawns a background loop with hard-referenced tasks to prevent mid-execution garbage collection. Also handles auto-loading from disk if persistence is enabled.

Source code in src/xovis/api/device/cache.py
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
async def start(self) -> None:
    """
    Starts the background configuration watcher.

    Performs an initial synchronization and spawns a background loop with
    hard-referenced tasks to prevent mid-execution garbage collection.
    Also handles auto-loading from disk if persistence is enabled.
    """
    if self.auto_persist_path:
        try:
            await self.load_from_disk()
        except Exception as e:
            logger.warning(f"Failed to load cache from disk: {e}")

    is_watcher = getattr(self.strategy, "value", str(self.strategy)) == "BACKGROUND_WATCHER"
    if config.CACHE_ENABLED or is_watcher:
        if not self._is_running:
            self._is_running = True

            await self.sync()

            task = asyncio.create_task(self._watcher_loop())
            self._background_tasks.add(task)
            task.add_done_callback(self._background_tasks.discard)
stop() async

Cleanly halts background synchronization loops.

Ensures all tasks are cancelled and awaited to prevent socket leaks.

Source code in src/xovis/api/device/cache.py
953
954
955
956
957
958
959
960
961
962
963
964
965
966
async def stop(self) -> None:
    """
    Cleanly halts background synchronization loops.

    Ensures all tasks are cancelled and awaited to prevent socket leaks.
    """
    self._is_running = False
    for task in list(self._background_tasks):
        if not task.done():
            task.cancel()

    if self._background_tasks:
        await asyncio.gather(*self._background_tasks, return_exceptions=True)
    self._background_tasks.clear()
sync() async

Pulls the entire topology graph concurrently, isolating each context.

Performs multisensor discovery, builds a concurrent task matrix for all endpoints (DataPush, Scene, Analytics), and populates partitioned state buckets.

Raises:

Type Description
Exception

Logged as an error if the synchronization fails.

Source code in src/xovis/api/device/cache.py
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
async def sync(self) -> None:
    """
    Pulls the entire topology graph concurrently, isolating each context.

    Performs multisensor discovery, builds a concurrent task matrix for all
    endpoints (DataPush, Scene, Analytics), and populates partitioned
    state buckets.

    Raises:
        Exception: Logged as an error if the synchronization fails.
    """
    try:
        contexts_to_sync = ["singlesensor"]
        try:
            ms_resp = await self._http_client.get("/api/v5/multisensors/status")
            data = ms_resp.json()
            if isinstance(data, list):
                ms_list = data
            else:
                ms_list = data.get("multisensors_status") or data.get("multisensors")
                if ms_list is None and isinstance(data, dict) and "multisensor_id" in data:
                    ms_list = [data]
            if isinstance(ms_list, list):
                for ms in ms_list:
                    ms_id = ms.get("multisensor_id") or ms.get("id") or ms.get("custom_id")
                    if ms_id is not None:
                        contexts_to_sync.append(str(ms_id))
        except (XovisClientError, Exception) as e:
            logger.debug(f"Multisensor discovery skipped: {e}")

        tasks = []
        context_map = []

        endpoints = [
            ("agents", "/data/push/agents"),
            ("connections", "/data/push/connections"),
            ("geometries", "/scene/geometries"),
            ("logics", "/analysis/logics"),
            ("modifiers", "/analysis/modifiers"),
            ("counters", "/analysis/counters"),
            ("scene_masks", "/scene/masks"),
            ("layers", "/scene/layers"),
        ]

        for ctx in contexts_to_sync:
            prefix = f"/api/v5/multisensors/{ctx}" if ctx != "singlesensor" else "/api/v5/singlesensor"
            for _, ep in endpoints:
                tasks.append(self._http_client.get(f"{prefix}{ep}"))
            context_map.append(ctx)

        results = await asyncio.gather(*tasks, return_exceptions=True)

        stride = len(endpoints)
        for i, ctx in enumerate(context_map):
            ctx_results = results[i * stride : (i + 1) * stride]
            bucket = ContextStateBucket()

            def extract_data(res, key, entity_name=None, endpoint=None):
                if isinstance(res, Exception):
                    return []
                try:
                    data = res.json()
                    raw_items = data.get(key, data.get("root", []))

                    # Discovery Crawler: Capture unknown fields
                    if entity_name and raw_items and isinstance(raw_items, list):
                        known_fields = CacheResource.model_fields.keys()
                        for item in raw_items:
                            if isinstance(item, dict):
                                discovery_manager.capture(
                                    entity_name=entity_name,
                                    raw_data=item,
                                    known_fields=set(known_fields),
                                    endpoint=endpoint,
                                )
                    return raw_items
                except Exception:
                    return []

            bucket.agents = [
                CacheResource.model_validate(a) for a in extract_data(ctx_results[0], "agents", "Agent", f"{prefix}/data/push/agents")
            ]
            bucket.connections = [
                CacheResource.model_validate(c)
                for c in extract_data(
                    ctx_results[1],
                    "connections",
                    "Connection",
                    f"{prefix}/data/push/connections",
                )
            ]

            geoms_data = extract_data(ctx_results[2], "geometries", "Geometry", f"{prefix}/scene/geometries")
            geoms = [CacheResource.model_validate(g) for g in geoms_data]
            bucket.zones = [g for g in geoms if g.type == "ZONE"]
            bucket.lines = [g for g in geoms if g.type == "LINE"]

            bucket.logics = [
                CacheResource.model_validate(l) for l in extract_data(ctx_results[3], "logics", "Logic", f"{prefix}/analysis/logics")
            ]
            bucket.modifiers = [
                CacheResource.model_validate(m) for m in extract_data(ctx_results[4], "modifiers", "Modifier", f"{prefix}/analysis/modifiers")
            ]
            bucket.counters = [
                CacheResource.model_validate(c) for c in extract_data(ctx_results[5], "counters", "Counter", f"{prefix}/analysis/counters")
            ]
            bucket.masks = [
                CacheResource.model_validate(sm) for sm in extract_data(ctx_results[6], "scene_masks", "Mask", f"{prefix}/scene/masks")
            ]
            bucket.layers = [CacheResource.model_validate(ly) for ly in extract_data(ctx_results[7], "layers", "Layer", f"{prefix}/scene/layers")]

            self._state.contexts[ctx] = bucket

        ms_contexts = [ctx for ctx in context_map if ctx != "singlesensor"]
        if ms_contexts:
            sensor_tasks = [self._http_client.get(f"/api/v5/multisensors/{ctx}/sensors") for ctx in ms_contexts]
            sensor_results = await asyncio.gather(*sensor_tasks, return_exceptions=True)
            for ctx, res in zip(ms_contexts, sensor_results):
                if not isinstance(res, Exception) and res.status_code == 200:
                    try:
                        from xovis.api.device.topology import MultisensorChildrenResponse

                        payload = MultisensorChildrenResponse.model_validate(res.json())
                        self._state.contexts[ctx].child_sensors = [
                            TopologyNodeInfo(
                                mac_address=child.mac_address,
                                ip_address=child.ip_address,
                                name=child.name,
                                group=child.group,
                                status=child.status,
                            )
                            for child in payload.sensors
                        ]
                    except Exception as e:
                        logger.debug(f"Failed parsing child sensors for context {ctx}: {e}")

        if self.cache_child_devices:
            from xovis.api.device.client import DeviceClient

            child_sync_tasks = []
            for ctx in ms_contexts:
                bucket = self._state.contexts.get(ctx)
                if bucket and bucket.child_sensors:
                    for sensor in bucket.child_sensors:
                        if not sensor.ip_address:
                            continue
                        username = "admin"
                        password = ""
                        use_ntlm = False
                        if self._parent_client:
                            username = self._parent_client._auth.username
                            password = self._parent_client._auth.password
                            use_ntlm = self._parent_client._auth.use_ntlm
                        child_client = DeviceClient(
                            host=sensor.ip_address,
                            username=username,
                            password=password,
                            use_ntlm=use_ntlm,
                            cache_child_devices=False,
                        )
                        child_sync_tasks.append(child_client.cache.sync())
            if child_sync_tasks:
                await asyncio.gather(*child_sync_tasks, return_exceptions=True)

        if True:
            await self.save_to_disk()

    except Exception as e:
        logger.error(f"Failed to synchronize cache: {e}")

ContextAccessor

Facade providing high-level access to a ContextStateBucket.

Wraps raw state buckets in CacheCollection instances to enable dot-notation resource discovery (e.g., ctx.agents.by_name.MyAgent).

Source code in src/xovis/api/device/cache.py
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
class ContextAccessor:
    """
    Facade providing high-level access to a ContextStateBucket.

    Wraps raw state buckets in `CacheCollection` instances to enable
    dot-notation resource discovery (e.g., `ctx.agents.by_name.MyAgent`).
    """

    def __init__(self, bucket: ContextStateBucket, name: Optional[str] = None, parent_client: Optional[Any] = None):
        """
        Initializes the ContextAccessor for a specific bucket.

        Args:
            bucket (ContextStateBucket): The raw state bucket to wrap.
            name (Optional[str]): Descriptive name for the context.
            parent_client (Optional[Any]): The host device client instance.
        """
        self._bucket = bucket
        self.name = name
        self._parent_client = parent_client

    @property
    def child_devices(self) -> ChildDevicesAccessor:
        """Provides instant autosuggestion/REPL access to physical child clients.

        Returns:
            ChildDevicesAccessor: High-level child client and bulk operations accessor.
        """
        from xovis.api.device.client import DeviceClient

        clients = []
        sensors = getattr(self._bucket, "child_sensors", []) or []
        for sensor in sensors:
            if not sensor.ip_address:
                continue
            username = "admin"
            password = ""
            use_ntlm = False
            if self._parent_client:
                username = self._parent_client._auth.username
                password = self._parent_client._auth.password
                use_ntlm = self._parent_client._auth.use_ntlm

            client = DeviceClient(
                host=sensor.ip_address,
                username=username,
                password=password,
                use_ntlm=use_ntlm,
                cache_child_devices=False,
            )
            client.name = sensor.name or f"sensor_{sensor.mac_address.replace(':', '_')}"
            clients.append(client)

        return ChildDevicesAccessor(self, clients)

    @property
    def child_caches(self) -> REPLAccessor["ContextAccessor"]:
        """Provides REPL access to the offline-first cache states of those child sensors.

        Returns:
            REPLAccessor[ContextAccessor]: Facade for child configurations.
        """
        caches = []
        child_devs = self.child_devices
        for client in child_devs._child_clients:
            accessor = ContextAccessor(client.cache.singlesensor._bucket, name=client.name, parent_client=client)
            caches.append(accessor)
        return REPLAccessor(caches, key_attr="name")

    def __repr__(self) -> str:
        """
        Returns a string representation of the ContextAccessor.

        Returns:
            str: Description of the context.
        """
        return f"<ContextAccessor name='{self.name}'>"

    @property
    def agents(self) -> CacheCollection[CacheResource]:
        """
        Accesses the cached DataPush agents.

        Returns:
            CacheCollection[CacheResource]: Collection of agent metadata.
        """
        return CacheCollection(self._bucket.agents)

    @agents.setter
    def agents(self, value: Optional[list[CacheResource]]) -> None:
        """Sets the cached DataPush agents."""
        self._bucket.agents = value

    @property
    def connections(self) -> CacheCollection[CacheResource]:
        """
        Accesses the cached DataPush connections.

        Returns:
            CacheCollection[CacheResource]: Collection of connection metadata.
        """
        return CacheCollection(self._bucket.connections)

    @connections.setter
    def connections(self, value: Optional[list[CacheResource]]) -> None:
        """Sets the cached DataPush connections."""
        self._bucket.connections = value

    @property
    def zones(self) -> CacheCollection[CacheResource]:
        """
        Accesses the cached scene zones.

        Returns:
            CacheCollection[CacheResource]: Collection of zone metadata.
        """
        return CacheCollection(self._bucket.zones)

    @zones.setter
    def zones(self, value: Optional[list[CacheResource]]) -> None:
        """Sets the cached scene zones."""
        self._bucket.zones = value

    @property
    def lines(self) -> CacheCollection[CacheResource]:
        """
        Accesses the cached scene lines.

        Returns:
            CacheCollection[CacheResource]: Collection of line metadata.
        """
        return CacheCollection(self._bucket.lines)

    @lines.setter
    def lines(self, value: Optional[list[CacheResource]]) -> None:
        """Sets the cached scene lines."""
        self._bucket.lines = value

    @property
    def logics(self) -> CacheCollection[CacheResource]:
        """
        Accesses the cached analysis logics.

        Returns:
            CacheCollection[CacheResource]: Collection of logic metadata.
        """
        return CacheCollection(self._bucket.logics)

    @logics.setter
    def logics(self, value: Optional[list[CacheResource]]) -> None:
        """Sets the cached analysis logics."""
        self._bucket.logics = value

    @property
    def modifiers(self) -> CacheCollection[CacheResource]:
        """
        Accesses the cached analysis modifiers.

        Returns:
            CacheCollection[CacheResource]: Collection of modifier metadata.
        """
        return CacheCollection(self._bucket.modifiers)

    @modifiers.setter
    def modifiers(self, value: Optional[list[CacheResource]]) -> None:
        """Sets the cached analysis modifiers."""
        self._bucket.modifiers = value

    @property
    def counters(self) -> CacheCollection[CacheResource]:
        """
        Accesses the cached analysis counters.

        Returns:
            CacheCollection[CacheResource]: Collection of counter metadata.
        """
        return CacheCollection(self._bucket.counters)

    @counters.setter
    def counters(self, value: Optional[list[CacheResource]]) -> None:
        """Sets the cached analysis counters."""
        self._bucket.counters = value

    @property
    def masks(self) -> CacheCollection[CacheResource]:
        """
        Accesses the cached scene masks.

        Returns:
            CacheCollection[CacheResource]: Collection of mask metadata.
        """
        return CacheCollection(self._bucket.masks)

    @masks.setter
    def masks(self, value: Optional[list[CacheResource]]) -> None:
        """Sets the cached scene masks."""
        self._bucket.masks = value

    @property
    def layers(self) -> CacheCollection[CacheResource]:
        """
        Accesses the cached scene layers.

        Returns:
            CacheCollection[CacheResource]: Collection of layer metadata.
        """
        return CacheCollection(self._bucket.layers)
Attributes
agents property writable

Accesses the cached DataPush agents.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Collection of agent metadata.

child_caches property

Provides REPL access to the offline-first cache states of those child sensors.

Returns:

Type Description
REPLAccessor[ContextAccessor]

REPLAccessor[ContextAccessor]: Facade for child configurations.

child_devices property

Provides instant autosuggestion/REPL access to physical child clients.

Returns:

Name Type Description
ChildDevicesAccessor ChildDevicesAccessor

High-level child client and bulk operations accessor.

connections property writable

Accesses the cached DataPush connections.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Collection of connection metadata.

counters property writable

Accesses the cached analysis counters.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Collection of counter metadata.

layers property

Accesses the cached scene layers.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Collection of layer metadata.

lines property writable

Accesses the cached scene lines.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Collection of line metadata.

logics property writable

Accesses the cached analysis logics.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Collection of logic metadata.

masks property writable

Accesses the cached scene masks.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Collection of mask metadata.

modifiers property writable

Accesses the cached analysis modifiers.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Collection of modifier metadata.

zones property writable

Accesses the cached scene zones.

Returns:

Type Description
CacheCollection[CacheResource]

CacheCollection[CacheResource]: Collection of zone metadata.

Methods:
__init__(bucket, name=None, parent_client=None)

Initializes the ContextAccessor for a specific bucket.

Parameters:

Name Type Description Default
bucket ContextStateBucket

The raw state bucket to wrap.

required
name Optional[str]

Descriptive name for the context.

None
parent_client Optional[Any]

The host device client instance.

None
Source code in src/xovis/api/device/cache.py
561
562
563
564
565
566
567
568
569
570
571
572
def __init__(self, bucket: ContextStateBucket, name: Optional[str] = None, parent_client: Optional[Any] = None):
    """
    Initializes the ContextAccessor for a specific bucket.

    Args:
        bucket (ContextStateBucket): The raw state bucket to wrap.
        name (Optional[str]): Descriptive name for the context.
        parent_client (Optional[Any]): The host device client instance.
    """
    self._bucket = bucket
    self.name = name
    self._parent_client = parent_client
__repr__()

Returns a string representation of the ContextAccessor.

Returns:

Name Type Description
str str

Description of the context.

Source code in src/xovis/api/device/cache.py
622
623
624
625
626
627
628
629
def __repr__(self) -> str:
    """
    Returns a string representation of the ContextAccessor.

    Returns:
        str: Description of the context.
    """
    return f"<ContextAccessor name='{self.name}'>"

ContextStateBucket

Bases: BaseModel

Isolated state container for a specific lens context or virtual environment.

Partitions resources (agents, connections, geometries, etc.) by their operational context to prevent configuration leakage between physical sensors ("singlesensor") and virtual stitched environments ("multisensor").

Source code in src/xovis/api/device/cache.py
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
class ContextStateBucket(BaseModel):
    """
    Isolated state container for a specific lens context or virtual environment.

    Partitions resources (agents, connections, geometries, etc.) by their
    operational context to prevent configuration leakage between physical
    sensors ("singlesensor") and virtual stitched environments ("multisensor").
    """

    model_config = ConfigDict(extra="ignore")
    agents: list[CacheResource] = Field(default_factory=list)
    connections: list[CacheResource] = Field(default_factory=list)
    zones: list[CacheResource] = Field(default_factory=list)
    lines: list[CacheResource] = Field(default_factory=list)
    logics: list[CacheResource] = Field(default_factory=list)
    modifiers: list[CacheResource] = Field(default_factory=list)
    counters: list[CacheResource] = Field(default_factory=list)
    masks: list[CacheResource] = Field(default_factory=list)
    layers: list[CacheResource] = Field(default_factory=list)
    child_sensors: list[TopologyNodeInfo] = Field(default_factory=list)

HostStateBucket

Bases: BaseModel

Root state container for the entire physical device host.

Aggregates checksum metadata and partitioned context buckets to represent the complete topology of a Xovis sensor or Spider NUC.

Source code in src/xovis/api/device/cache.py
314
315
316
317
318
319
320
321
322
323
324
class HostStateBucket(BaseModel):
    """
    Root state container for the entire physical device host.

    Aggregates checksum metadata and partitioned context buckets to represent
    the complete topology of a Xovis sensor or Spider NUC.
    """

    model_config = ConfigDict(extra="ignore")
    checksum: Optional[str] = None
    contexts: dict[str, ContextStateBucket] = Field(default_factory=dict)

REPLAccessor

Bases: Generic[T]

Read-only dynamic accessor allowing object discovery via dot-notation.

This utility provides a hyper-optimized developer experience in interactive environments (Jupyter/REPL) by mapping resource names to object attributes using robust regex sanitization.

Source code in src/xovis/api/device/cache.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
class REPLAccessor(Generic[T]):
    """
    Read-only dynamic accessor allowing object discovery via dot-notation.

    This utility provides a hyper-optimized developer experience in interactive
    environments (Jupyter/REPL) by mapping resource names to object attributes
    using robust regex sanitization.
    """

    def __init__(self, elements: list[T], key_attr: str = "name"):
        """
        Initializes the REPLAccessor with a list of elements.

        Args:
            elements (List[T]): The collection of resources to expose.
            key_attr (str, optional): The attribute used for mapping (e.g., "name", "id").
                Defaults to "name".
        """
        self._items = {}
        for item in elements:
            key = getattr(item, key_attr, None)
            if key is not None:
                key = str(key)
                safe_key = re.sub(r"\W+", "_", key).strip("_")
                if safe_key and safe_key[0].isdigit():
                    safe_key = f"_{safe_key}"
                self._items[safe_key] = item
                if key != safe_key:
                    self._items[key] = item

    def __getattr__(self, name: str) -> T:
        """
        Retrieves a resource via dot-notation.

        Args:
            name (str): The sanitized or raw name of the resource.

        Returns:
            T: The mapped resource element.

        Raises:
            AttributeError: If the resource name is not found in the accessor.
        """
        if name in self._items:
            return self._items[name]
        raise AttributeError(f"Resource '{name}' not found. Available: {list(self._items.keys())}")

    def __getitem__(self, key: str) -> T:
        """
        Retrieves a resource via bracket-notation.

        Args:
            key (str): The sanitized or raw name of the resource.

        Returns:
            T: The mapped resource element.

        Raises:
            KeyError: If the resource name is not found in the accessor.
        """
        if key in self._items:
            return self._items[key]
        raise KeyError(f"Resource '{key}' not found. Available: {list(self._items.keys())}")

    def __dir__(self) -> list[str]:
        """
        Enables IDE/REPL auto-completion for mapped resources.

        Returns:
            List[str]: Combined list of class members and dynamic resource keys.
        """
        return list(super().__dir__()) + list(self._items.keys())

    def __repr__(self) -> str:
        """
        Returns a string representation of the REPLAccessor.

        Returns:
            str: Description of available resources.
        """
        return f"<REPLAccessor resources={list(self._items.keys())}>"
Methods:
__dir__()

Enables IDE/REPL auto-completion for mapped resources.

Returns:

Type Description
list[str]

List[str]: Combined list of class members and dynamic resource keys.

Source code in src/xovis/api/device/cache.py
178
179
180
181
182
183
184
185
def __dir__(self) -> list[str]:
    """
    Enables IDE/REPL auto-completion for mapped resources.

    Returns:
        List[str]: Combined list of class members and dynamic resource keys.
    """
    return list(super().__dir__()) + list(self._items.keys())
__getattr__(name)

Retrieves a resource via dot-notation.

Parameters:

Name Type Description Default
name str

The sanitized or raw name of the resource.

required

Returns:

Name Type Description
T T

The mapped resource element.

Raises:

Type Description
AttributeError

If the resource name is not found in the accessor.

Source code in src/xovis/api/device/cache.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def __getattr__(self, name: str) -> T:
    """
    Retrieves a resource via dot-notation.

    Args:
        name (str): The sanitized or raw name of the resource.

    Returns:
        T: The mapped resource element.

    Raises:
        AttributeError: If the resource name is not found in the accessor.
    """
    if name in self._items:
        return self._items[name]
    raise AttributeError(f"Resource '{name}' not found. Available: {list(self._items.keys())}")
__getitem__(key)

Retrieves a resource via bracket-notation.

Parameters:

Name Type Description Default
key str

The sanitized or raw name of the resource.

required

Returns:

Name Type Description
T T

The mapped resource element.

Raises:

Type Description
KeyError

If the resource name is not found in the accessor.

Source code in src/xovis/api/device/cache.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def __getitem__(self, key: str) -> T:
    """
    Retrieves a resource via bracket-notation.

    Args:
        key (str): The sanitized or raw name of the resource.

    Returns:
        T: The mapped resource element.

    Raises:
        KeyError: If the resource name is not found in the accessor.
    """
    if key in self._items:
        return self._items[key]
    raise KeyError(f"Resource '{key}' not found. Available: {list(self._items.keys())}")
__init__(elements, key_attr='name')

Initializes the REPLAccessor with a list of elements.

Parameters:

Name Type Description Default
elements List[T]

The collection of resources to expose.

required
key_attr str

The attribute used for mapping (e.g., "name", "id"). Defaults to "name".

'name'
Source code in src/xovis/api/device/cache.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def __init__(self, elements: list[T], key_attr: str = "name"):
    """
    Initializes the REPLAccessor with a list of elements.

    Args:
        elements (List[T]): The collection of resources to expose.
        key_attr (str, optional): The attribute used for mapping (e.g., "name", "id").
            Defaults to "name".
    """
    self._items = {}
    for item in elements:
        key = getattr(item, key_attr, None)
        if key is not None:
            key = str(key)
            safe_key = re.sub(r"\W+", "_", key).strip("_")
            if safe_key and safe_key[0].isdigit():
                safe_key = f"_{safe_key}"
            self._items[safe_key] = item
            if key != safe_key:
                self._items[key] = item
__repr__()

Returns a string representation of the REPLAccessor.

Returns:

Name Type Description
str str

Description of available resources.

Source code in src/xovis/api/device/cache.py
187
188
189
190
191
192
193
194
def __repr__(self) -> str:
    """
    Returns a string representation of the REPLAccessor.

    Returns:
        str: Description of available resources.
    """
    return f"<REPLAccessor resources={list(self._items.keys())}>"

Functions:

_by_name_property(self)

Safely extracts lists from Pydantic V2 Models or RootModels for REPL access.

This helper is monkey-patched onto auto-generated models to provide the .by_name accessor pattern consistently.

Returns:

Name Type Description
REPLAccessor REPLAccessor

A dynamic accessor for the identified collection.

Source code in src/xovis/api/device/cache.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def _by_name_property(self) -> REPLAccessor:
    """
    Safely extracts lists from Pydantic V2 Models or RootModels for REPL access.

    This helper is monkey-patched onto auto-generated models to provide
    the `.by_name` accessor pattern consistently.

    Returns:
        REPLAccessor: A dynamic accessor for the identified collection.
    """
    keys_to_check = [
        "agents",
        "connections",
        "geometries",
        "logics",
        "modifiers",
        "counters",
        "scene_masks",
        "layers",
        "root",
    ]
    for key in keys_to_check:
        if hasattr(self, key):
            return REPLAccessor(getattr(self, key))
    return REPLAccessor([])

Resources

xovis.api.device.resources.analytics

Xovis SDK - Analytics Management Resource

Operates within the Control Plane. Provides the implementation for managing complex analytics structures (logics, modifiers, counters, and templates) on local edge sensors. Integrates atomic transactions, capacity limits, and cascading deletes for autonomous agent workflows.

Classes

AnalyticsManager

Manages analytics logics, modifiers, counters, and atomic transactions.

This manager orchestrates the mathematical and spatial rules of the sensor. It supports querying hardware limits to prevent resource exhaustion and allows autonomous agents to execute complex configuration changes atomically.

Source code in src/xovis/api/device/resources/analytics.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
class AnalyticsManager:
    """
    Manages analytics logics, modifiers, counters, and atomic transactions.

    This manager orchestrates the mathematical and spatial rules of the sensor.
    It supports querying hardware limits to prevent resource exhaustion and
    allows autonomous agents to execute complex configuration changes atomically.
    """

    def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
        """
        Initializes the AnalyticsManager.

        Args:
            client (DeviceClient): The parent device client instance.
            target_id (Optional[str]): The multisensor target ID, if applicable.
        """
        self._client = client
        self._http = client._http_client
        self.target_id = target_id

    @property
    def models(self) -> Any:
        """
        Returns the strictly validated Pydantic models for the current firmware.

        Returns:
            Any: The collection of auto-generated Pydantic V2 models
                synchronized with the current device firmware.
        """
        return self._client.models if self._client else stable_models

    def _resolve_path(self) -> str:
        """
        Resolves the base API path based on the current isolated context.

        Returns:
            str: The resolved API endpoint path.
        """
        if self.target_id:
            return f"/api/v5/multisensors/{self.target_id}/analysis"
        return "/api/v5/singlesensor/analysis"

    async def _resolve_logic_id(self, id_or_name: Union[int, str]) -> int:
        """
        Resolves a logic ID from either an ID or a human-readable name.

        Args:
            id_or_name (Union[int, str]): The integer ID or string name to resolve.

        Returns:
            int: The resolved numeric logic identifier.

        Raises:
            ResourceNotFoundError: If no logic matches the provided name.
            MultipleResourcesFoundError: If the name is ambiguous.
        """
        if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
            return int(id_or_name)

        multisensors = self._client.cache.multisensors
        if self.target_id:
            target_str = str(self.target_id)
            if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
                await self._client.multisensors.sync()
            context = multisensors[target_str]
        else:
            context = self._client.cache.singlesensor

        if context.logics is None or not any(l.name == id_or_name for l in context.logics):
            await self.get_all_logics()

        matches = [l for l in context.logics if l.name == id_or_name]
        if not matches:
            raise ResourceNotFoundError(f"No logic found with name '{id_or_name}'.")
        if len(matches) > 1:
            raise MultipleResourcesFoundError(f"Found {len(matches)} logics named '{id_or_name}'.")
        return int(matches[0].id)

    async def _resolve_modifier_id(self, id_or_name: Union[int, str]) -> int:
        """
        Resolves a modifier ID from either an ID or a human-readable name.

        Args:
            id_or_name (Union[int, str]): The integer ID or string name to resolve.

        Returns:
            int: The resolved numeric modifier identifier.

        Raises:
            ResourceNotFoundError: If no modifier matches the provided name.
            MultipleResourcesFoundError: If the name is ambiguous.
        """
        if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
            return int(id_or_name)

        multisensors = self._client.cache.multisensors
        if self.target_id:
            target_str = str(self.target_id)
            if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
                await self._client.multisensors.sync()
            context = multisensors[target_str]
        else:
            context = self._client.cache.singlesensor

        if context.modifiers is None or not any(m.name == id_or_name for m in context.modifiers):
            await self.get_all_modifiers()

        matches = [m for m in context.modifiers if m.name == id_or_name]
        if not matches:
            raise ResourceNotFoundError(f"No modifier found with name '{id_or_name}'.")
        if len(matches) > 1:
            raise MultipleResourcesFoundError(f"Found {len(matches)} modifiers named '{id_or_name}'.")
        return int(matches[0].id)

    async def _resolve_counter_id(self, id_or_name: Union[int, str]) -> int:
        """
        Resolves a counter ID from either an ID or a human-readable name.

        Args:
            id_or_name (Union[int, str]): The integer ID or string name to resolve.

        Returns:
            int: The resolved numeric counter identifier.

        Raises:
            ResourceNotFoundError: If no counter matches the provided name.
            MultipleResourcesFoundError: If the name is ambiguous.
        """
        if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
            return int(id_or_name)

        multisensors = self._client.cache.multisensors
        if self.target_id:
            target_str = str(self.target_id)
            if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
                await self._client.multisensors.sync()
            context = multisensors[target_str]
        else:
            context = self._client.cache.singlesensor

        if context.counters is None or not any(c.name == id_or_name for c in context.counters):
            await self.get_all_counters()

        matches = [c for c in context.counters if c.name == id_or_name]
        if not matches:
            raise ResourceNotFoundError(f"No counter found with name '{id_or_name}'.")
        if len(matches) > 1:
            raise MultipleResourcesFoundError(f"Found {len(matches)} counters named '{id_or_name}'.")
        return int(matches[0].id)

    async def _pacing_delay(self) -> None:
        """
        Implements intra-mutation pacing to prevent sensor configuration OOM or service crashes.
        Mandatory for FW 5.9.2+ where rapid REST mutations can lead to hardware reboots.
        """
        await asyncio.sleep(2.0)

    # --- ATOMIC TRANSACTIONS ---
    async def execute_transaction(self, transaction: "Transaction", id_mode: str = "SERVER") -> "Transaction":
        """
        Executes a bundle of requests as a single atomic transaction.

        Args:
            transaction (Transaction): The transaction model containing a list
                of configuration mutations to execute atomically.
            id_mode (str): ID assignment strategy ("SERVER" or "CLIENT").

        Returns:
            Transaction: The executed transaction with updated IDs and statuses.
        """
        await self._pacing_delay()
        params = {"id_mode": id_mode}
        response = await self._http.post(f"{self._resolve_path()}/transaction", params=params, json=transaction)
        return self.models.Transaction.model_validate(response.json())

    # --- LOGICS ---
    async def get_logic_limits(self) -> "ElementsLimits":
        """
        Retrieves the maximum allowed number of logics for the device.
        """
        response = await self._http.get(f"{self._resolve_path()}/logics/limits")
        return self.models.ElementsLimits.model_validate(response.json())

    async def get_all_logics(self) -> "LogicCollection":
        """
        Retrieves all analytics logics from the sensor.

        Returns:
            LogicCollection: The collection of all configured analytics logics.
        """
        response = await self._http.get(f"{self._resolve_path()}/logics")
        collection = self.models.LogicCollection.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        context.logics = collection.logics

        return collection

    async def get_logic(self, id_or_name: Union[int, str]) -> "Logic":
        """
        Retrieves a specific analytics logic.

        Args:
            id_or_name (Union[int, str]): The ID or name of the logic to retrieve.

        Returns:
            Logic: The retrieved analytics logic model.
        """
        logic_id = await self._resolve_logic_id(id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/logics/{logic_id}")
        return self.models.Logic.model_validate(response.json())

    async def create_logic(self, logic: "Logic", id_mode: str = "SERVER") -> "Logic":
        """
        Creates a new analytics logic.

        Args:
            logic (Logic): The logic model to create.
            id_mode (str): ID assignment strategy ("SERVER" or "CLIENT").

        Returns:
            Logic: The created analytics logic with its assigned ID.
        """
        await self._pacing_delay()
        params = {"id_mode": id_mode}
        response = await self._http.post(f"{self._resolve_path()}/logics", params=params, json=logic)
        return self.models.Logic.model_validate(response.json())

    async def update_logic(self, id_or_name: Union[int, str], logic: "Logic") -> "Logic":
        """
        Updates an existing analytics logic.

        Args:
            id_or_name (Union[int, str]): The ID or name of the logic to update.
            logic (Logic): The updated logic model.

        Returns:
            Logic: The updated analytics logic.
        """
        await self._pacing_delay()
        logic_id = await self._resolve_logic_id(id_or_name)
        response = await self._http.put(f"{self._resolve_path()}/logics/{logic_id}", json=logic)
        return self.models.Logic.model_validate(response.json())

    async def delete_logic(self, id_or_name: Union[int, str], force: bool = False) -> None:
        """
        Deletes an analytics logic.

        Args:
            id_or_name (Union[int, str]): The ID or name of the logic to delete.
            force (bool): If True, forces deletion even if dependencies exist.
        """
        await self._pacing_delay()
        logic_id = await self._resolve_logic_id(id_or_name)
        params = {"force": "true" if force else "false"}
        await self._http.delete(f"{self._resolve_path()}/logics/{logic_id}", params=params)

    async def delete_all_logics(self, force: bool = False) -> None:
        """Deletes all analytics logics and optionally cascades dependencies."""
        await self._pacing_delay()
        params = {"force": "true" if force else "false"}
        await self._http.delete(f"{self._resolve_path()}/logics", params=params)

    async def reset_logic_counters(self, id_or_name: Union[int, str]) -> None:
        """
        Resets the relative/live values of all counters associated with this logic to zero.
        Does not affect the persisted historical database.
        """
        logic_id = await self._resolve_logic_id(id_or_name)
        await self._http.post(f"{self._resolve_path()}/logics/{logic_id}/reset")

    # --- LOGIC TEMPLATES ---
    async def get_all_logic_templates(self) -> "LogicTemplateCollection":
        """
        Retrieves all pre-configured logic templates.

        Returns:
            LogicTemplateCollection: The collection of available logic templates.
        """
        response = await self._http.get(f"{self._resolve_path()}/logics/templates")
        return self.models.LogicTemplateCollection.model_validate(response.json())

    async def create_logic_template(self, template: "LogicTemplate", id_mode: str = "SERVER") -> "LogicTemplate":
        """
        Instantiates a new logic template.

        Args:
            template (LogicTemplate): The template model to instantiate.
            id_mode (str): ID assignment strategy.

        Returns:
            LogicTemplate: The created logic template instance.
        """
        await self._pacing_delay()
        params = {"id_mode": id_mode}
        response = await self._http.post(f"{self._resolve_path()}/logics/templates", params=params, json=template)
        return self.models.LogicTemplate.model_validate(response.json())

    async def delete_logic_template(self, id_or_name: Union[int, str]) -> None:
        """
        Deletes a specific logic template.

        Args:
            id_or_name (Union[int, str]): The ID or name of the template to delete.
        """
        await self._pacing_delay()
        template_id = await self._resolve_logic_id(id_or_name)
        await self._http.delete(f"{self._resolve_path()}/logics/templates/{template_id}")

    # --- MODIFIERS ---
    async def get_modifier_limits(self) -> "ElementsLimits":
        """Retrieves the maximum allowed number of modifiers for the device."""
        response = await self._http.get(f"{self._resolve_path()}/modifiers/limits")
        return self.models.ElementsLimits.model_validate(response.json())

    async def get_all_modifiers(self, logic_id: Optional[int] = None, counter_id: Optional[int] = None) -> "ModifierCollection":
        """
        Retrieves analytics modifiers, optionally filtered by parent relationships.

        Args:
            logic_id (Optional[int]): Restrict output to modifiers of a specific logic.
            counter_id (Optional[int]): Restrict output to modifiers manipulating a specific counter.
        """
        params = {}
        if logic_id is not None:
            params["logic_id"] = str(logic_id)
        if counter_id is not None:
            params["counter_id"] = str(counter_id)

        response = await self._http.get(f"{self._resolve_path()}/modifiers", params=params)
        collection = self.models.ModifierCollection.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        context.modifiers = collection.modifiers

        return collection

    async def get_modifier(self, id_or_name: Union[int, str]) -> "Modifier":
        """
        Retrieves a specific analytics modifier.

        Args:
            id_or_name (Union[int, str]): The ID or name of the modifier to retrieve.

        Returns:
            Modifier: The retrieved analytics modifier model.
        """
        modifier_id = await self._resolve_modifier_id(id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/modifiers/{modifier_id}")
        return self.models.Modifier.model_validate(response.json())

    async def create_modifier(self, modifier: "Modifier", id_mode: str = "SERVER") -> "Modifier":
        """
        Creates a new analytics modifier.

        Args:
            modifier (Modifier): The modifier model to create.
            id_mode (str): ID assignment strategy.

        Returns:
            Modifier: The created analytics modifier.
        """
        await self._pacing_delay()
        params = {"id_mode": id_mode}
        response = await self._http.post(f"{self._resolve_path()}/modifiers", params=params, json=modifier)
        return self.models.Modifier.model_validate(response.json())

    async def update_modifier(self, id_or_name: Union[int, str], modifier: "Modifier") -> "Modifier":
        """
        Updates an existing analytics modifier.

        Args:
            id_or_name (Union[int, str]): The ID or name of the modifier to update.
            modifier (Modifier): The updated modifier model.

        Returns:
            Modifier: The updated analytics modifier.
        """
        await self._pacing_delay()
        modifier_id = await self._resolve_modifier_id(id_or_name)
        response = await self._http.put(f"{self._resolve_path()}/modifiers/{modifier_id}", json=modifier)
        return self.models.Modifier.model_validate(response.json())

    async def delete_modifier(self, id_or_name: Union[int, str]) -> None:
        """
        Deletes an analytics modifier.

        Args:
            id_or_name (Union[int, str]): The ID or name of the modifier to delete.
        """
        await self._pacing_delay()
        modifier_id = await self._resolve_modifier_id(id_or_name)
        await self._http.delete(f"{self._resolve_path()}/modifiers/{modifier_id}")

    async def delete_all_modifiers(self) -> None:
        """
        Deletes all analytics modifiers.
        """
        await self._pacing_delay()
        await self._http.delete(f"{self._resolve_path()}/modifiers")

    # --- COUNTERS ---
    async def get_counter_limits(self) -> "ElementsLimits":
        """Retrieves the maximum allowed number of counters for the device."""
        response = await self._http.get(f"{self._resolve_path()}/counters/limits")
        return self.models.ElementsLimits.model_validate(response.json())

    async def get_all_counters(self, logic_id: Optional[int] = None) -> "CounterCollection":
        """
        Retrieves analytics counters, optionally restricted to a specific logic.
        """
        params = {}
        if logic_id is not None:
            params["logic_id"] = str(logic_id)

        response = await self._http.get(f"{self._resolve_path()}/counters", params=params)
        collection = self.models.CounterCollection.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        context.counters = collection.counters

        return collection

    async def get_counter(self, id_or_name: Union[int, str]) -> "Counter":
        """
        Retrieves a specific analytics counter.

        Args:
            id_or_name (Union[int, str]): The ID or name of the counter to retrieve.

        Returns:
            Counter: The retrieved analytics counter model.
        """
        counter_id = await self._resolve_counter_id(id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/counters/{counter_id}")
        return self.models.Counter.model_validate(response.json())

    async def create_counter(self, counter: "Counter", id_mode: str = "SERVER") -> "Counter":
        """
        Creates a new analytics counter.

        Args:
            counter (Counter): The counter model to create.
            id_mode (str): ID assignment strategy.

        Returns:
            Counter: The created analytics counter.
        """
        await self._pacing_delay()
        params = {"id_mode": id_mode}
        response = await self._http.post(f"{self._resolve_path()}/counters", params=params, json=counter)
        return self.models.Counter.model_validate(response.json())

    async def update_counter(self, id_or_name: Union[int, str], counter: "Counter") -> "Counter":
        """
        Updates an existing analytics counter.

        Args:
            id_or_name (Union[int, str]): The ID or name of the counter to update.
            counter (Counter): The updated counter model.

        Returns:
            Counter: The updated analytics counter.
        """
        await self._pacing_delay()
        counter_id = await self._resolve_counter_id(id_or_name)
        response = await self._http.put(f"{self._resolve_path()}/counters/{counter_id}", json=counter)
        return self.models.Counter.model_validate(response.json())

    async def delete_counter(self, id_or_name: Union[int, str], force: bool = False) -> None:
        """
        Deletes an analytics counter.
        """
        await self._pacing_delay()
        counter_id = await self._resolve_counter_id(id_or_name)
        params = {"force": "true" if force else "false"}
        await self._http.delete(f"{self._resolve_path()}/counters/{counter_id}", params=params)

    async def delete_all_counters(self, force: bool = False) -> None:
        """Deletes all analytics counters and optionally cascades dependencies."""
        await self._pacing_delay()
        params = {"force": "true" if force else "false"}
        await self._http.delete(f"{self._resolve_path()}/counters", params=params)

    async def reset_counter(self, id_or_name: Union[int, str]) -> None:
        """
        Resets the relative/live value of a specific counter to zero.
        """
        counter_id = await self._resolve_counter_id(id_or_name)
        await self._http.post(f"{self._resolve_path()}/counters/{counter_id}/reset")

    async def reset_all_counters(self) -> None:
        """
        Resets the relative/live values of all counters to zero.
        """
        await self._http.post(f"{self._resolve_path()}/counters/reset")
Attributes
models property

Returns the strictly validated Pydantic models for the current firmware.

Returns:

Name Type Description
Any Any

The collection of auto-generated Pydantic V2 models synchronized with the current device firmware.

Methods:
__init__(client, target_id=None)

Initializes the AnalyticsManager.

Parameters:

Name Type Description Default
client DeviceClient

The parent device client instance.

required
target_id Optional[str]

The multisensor target ID, if applicable.

None
Source code in src/xovis/api/device/resources/analytics.py
48
49
50
51
52
53
54
55
56
57
58
def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
    """
    Initializes the AnalyticsManager.

    Args:
        client (DeviceClient): The parent device client instance.
        target_id (Optional[str]): The multisensor target ID, if applicable.
    """
    self._client = client
    self._http = client._http_client
    self.target_id = target_id
_pacing_delay() async

Implements intra-mutation pacing to prevent sensor configuration OOM or service crashes. Mandatory for FW 5.9.2+ where rapid REST mutations can lead to hardware reboots.

Source code in src/xovis/api/device/resources/analytics.py
190
191
192
193
194
195
async def _pacing_delay(self) -> None:
    """
    Implements intra-mutation pacing to prevent sensor configuration OOM or service crashes.
    Mandatory for FW 5.9.2+ where rapid REST mutations can lead to hardware reboots.
    """
    await asyncio.sleep(2.0)
_resolve_counter_id(id_or_name) async

Resolves a counter ID from either an ID or a human-readable name.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The integer ID or string name to resolve.

required

Returns:

Name Type Description
int int

The resolved numeric counter identifier.

Raises:

Type Description
ResourceNotFoundError

If no counter matches the provided name.

MultipleResourcesFoundError

If the name is ambiguous.

Source code in src/xovis/api/device/resources/analytics.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
async def _resolve_counter_id(self, id_or_name: Union[int, str]) -> int:
    """
    Resolves a counter ID from either an ID or a human-readable name.

    Args:
        id_or_name (Union[int, str]): The integer ID or string name to resolve.

    Returns:
        int: The resolved numeric counter identifier.

    Raises:
        ResourceNotFoundError: If no counter matches the provided name.
        MultipleResourcesFoundError: If the name is ambiguous.
    """
    if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
        return int(id_or_name)

    multisensors = self._client.cache.multisensors
    if self.target_id:
        target_str = str(self.target_id)
        if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
            await self._client.multisensors.sync()
        context = multisensors[target_str]
    else:
        context = self._client.cache.singlesensor

    if context.counters is None or not any(c.name == id_or_name for c in context.counters):
        await self.get_all_counters()

    matches = [c for c in context.counters if c.name == id_or_name]
    if not matches:
        raise ResourceNotFoundError(f"No counter found with name '{id_or_name}'.")
    if len(matches) > 1:
        raise MultipleResourcesFoundError(f"Found {len(matches)} counters named '{id_or_name}'.")
    return int(matches[0].id)
_resolve_logic_id(id_or_name) async

Resolves a logic ID from either an ID or a human-readable name.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The integer ID or string name to resolve.

required

Returns:

Name Type Description
int int

The resolved numeric logic identifier.

Raises:

Type Description
ResourceNotFoundError

If no logic matches the provided name.

MultipleResourcesFoundError

If the name is ambiguous.

Source code in src/xovis/api/device/resources/analytics.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
async def _resolve_logic_id(self, id_or_name: Union[int, str]) -> int:
    """
    Resolves a logic ID from either an ID or a human-readable name.

    Args:
        id_or_name (Union[int, str]): The integer ID or string name to resolve.

    Returns:
        int: The resolved numeric logic identifier.

    Raises:
        ResourceNotFoundError: If no logic matches the provided name.
        MultipleResourcesFoundError: If the name is ambiguous.
    """
    if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
        return int(id_or_name)

    multisensors = self._client.cache.multisensors
    if self.target_id:
        target_str = str(self.target_id)
        if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
            await self._client.multisensors.sync()
        context = multisensors[target_str]
    else:
        context = self._client.cache.singlesensor

    if context.logics is None or not any(l.name == id_or_name for l in context.logics):
        await self.get_all_logics()

    matches = [l for l in context.logics if l.name == id_or_name]
    if not matches:
        raise ResourceNotFoundError(f"No logic found with name '{id_or_name}'.")
    if len(matches) > 1:
        raise MultipleResourcesFoundError(f"Found {len(matches)} logics named '{id_or_name}'.")
    return int(matches[0].id)
_resolve_modifier_id(id_or_name) async

Resolves a modifier ID from either an ID or a human-readable name.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The integer ID or string name to resolve.

required

Returns:

Name Type Description
int int

The resolved numeric modifier identifier.

Raises:

Type Description
ResourceNotFoundError

If no modifier matches the provided name.

MultipleResourcesFoundError

If the name is ambiguous.

Source code in src/xovis/api/device/resources/analytics.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
async def _resolve_modifier_id(self, id_or_name: Union[int, str]) -> int:
    """
    Resolves a modifier ID from either an ID or a human-readable name.

    Args:
        id_or_name (Union[int, str]): The integer ID or string name to resolve.

    Returns:
        int: The resolved numeric modifier identifier.

    Raises:
        ResourceNotFoundError: If no modifier matches the provided name.
        MultipleResourcesFoundError: If the name is ambiguous.
    """
    if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
        return int(id_or_name)

    multisensors = self._client.cache.multisensors
    if self.target_id:
        target_str = str(self.target_id)
        if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
            await self._client.multisensors.sync()
        context = multisensors[target_str]
    else:
        context = self._client.cache.singlesensor

    if context.modifiers is None or not any(m.name == id_or_name for m in context.modifiers):
        await self.get_all_modifiers()

    matches = [m for m in context.modifiers if m.name == id_or_name]
    if not matches:
        raise ResourceNotFoundError(f"No modifier found with name '{id_or_name}'.")
    if len(matches) > 1:
        raise MultipleResourcesFoundError(f"Found {len(matches)} modifiers named '{id_or_name}'.")
    return int(matches[0].id)
_resolve_path()

Resolves the base API path based on the current isolated context.

Returns:

Name Type Description
str str

The resolved API endpoint path.

Source code in src/xovis/api/device/resources/analytics.py
71
72
73
74
75
76
77
78
79
80
def _resolve_path(self) -> str:
    """
    Resolves the base API path based on the current isolated context.

    Returns:
        str: The resolved API endpoint path.
    """
    if self.target_id:
        return f"/api/v5/multisensors/{self.target_id}/analysis"
    return "/api/v5/singlesensor/analysis"
create_counter(counter, id_mode='SERVER') async

Creates a new analytics counter.

Parameters:

Name Type Description Default
counter Counter

The counter model to create.

required
id_mode str

ID assignment strategy.

'SERVER'

Returns:

Name Type Description
Counter Counter

The created analytics counter.

Source code in src/xovis/api/device/resources/analytics.py
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
async def create_counter(self, counter: "Counter", id_mode: str = "SERVER") -> "Counter":
    """
    Creates a new analytics counter.

    Args:
        counter (Counter): The counter model to create.
        id_mode (str): ID assignment strategy.

    Returns:
        Counter: The created analytics counter.
    """
    await self._pacing_delay()
    params = {"id_mode": id_mode}
    response = await self._http.post(f"{self._resolve_path()}/counters", params=params, json=counter)
    return self.models.Counter.model_validate(response.json())
create_logic(logic, id_mode='SERVER') async

Creates a new analytics logic.

Parameters:

Name Type Description Default
logic Logic

The logic model to create.

required
id_mode str

ID assignment strategy ("SERVER" or "CLIENT").

'SERVER'

Returns:

Name Type Description
Logic Logic

The created analytics logic with its assigned ID.

Source code in src/xovis/api/device/resources/analytics.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
async def create_logic(self, logic: "Logic", id_mode: str = "SERVER") -> "Logic":
    """
    Creates a new analytics logic.

    Args:
        logic (Logic): The logic model to create.
        id_mode (str): ID assignment strategy ("SERVER" or "CLIENT").

    Returns:
        Logic: The created analytics logic with its assigned ID.
    """
    await self._pacing_delay()
    params = {"id_mode": id_mode}
    response = await self._http.post(f"{self._resolve_path()}/logics", params=params, json=logic)
    return self.models.Logic.model_validate(response.json())
create_logic_template(template, id_mode='SERVER') async

Instantiates a new logic template.

Parameters:

Name Type Description Default
template LogicTemplate

The template model to instantiate.

required
id_mode str

ID assignment strategy.

'SERVER'

Returns:

Name Type Description
LogicTemplate LogicTemplate

The created logic template instance.

Source code in src/xovis/api/device/resources/analytics.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
async def create_logic_template(self, template: "LogicTemplate", id_mode: str = "SERVER") -> "LogicTemplate":
    """
    Instantiates a new logic template.

    Args:
        template (LogicTemplate): The template model to instantiate.
        id_mode (str): ID assignment strategy.

    Returns:
        LogicTemplate: The created logic template instance.
    """
    await self._pacing_delay()
    params = {"id_mode": id_mode}
    response = await self._http.post(f"{self._resolve_path()}/logics/templates", params=params, json=template)
    return self.models.LogicTemplate.model_validate(response.json())
create_modifier(modifier, id_mode='SERVER') async

Creates a new analytics modifier.

Parameters:

Name Type Description Default
modifier Modifier

The modifier model to create.

required
id_mode str

ID assignment strategy.

'SERVER'

Returns:

Name Type Description
Modifier Modifier

The created analytics modifier.

Source code in src/xovis/api/device/resources/analytics.py
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
async def create_modifier(self, modifier: "Modifier", id_mode: str = "SERVER") -> "Modifier":
    """
    Creates a new analytics modifier.

    Args:
        modifier (Modifier): The modifier model to create.
        id_mode (str): ID assignment strategy.

    Returns:
        Modifier: The created analytics modifier.
    """
    await self._pacing_delay()
    params = {"id_mode": id_mode}
    response = await self._http.post(f"{self._resolve_path()}/modifiers", params=params, json=modifier)
    return self.models.Modifier.model_validate(response.json())
delete_all_counters(force=False) async

Deletes all analytics counters and optionally cascades dependencies.

Source code in src/xovis/api/device/resources/analytics.py
536
537
538
539
540
async def delete_all_counters(self, force: bool = False) -> None:
    """Deletes all analytics counters and optionally cascades dependencies."""
    await self._pacing_delay()
    params = {"force": "true" if force else "false"}
    await self._http.delete(f"{self._resolve_path()}/counters", params=params)
delete_all_logics(force=False) async

Deletes all analytics logics and optionally cascades dependencies.

Source code in src/xovis/api/device/resources/analytics.py
303
304
305
306
307
async def delete_all_logics(self, force: bool = False) -> None:
    """Deletes all analytics logics and optionally cascades dependencies."""
    await self._pacing_delay()
    params = {"force": "true" if force else "false"}
    await self._http.delete(f"{self._resolve_path()}/logics", params=params)
delete_all_modifiers() async

Deletes all analytics modifiers.

Source code in src/xovis/api/device/resources/analytics.py
446
447
448
449
450
451
async def delete_all_modifiers(self) -> None:
    """
    Deletes all analytics modifiers.
    """
    await self._pacing_delay()
    await self._http.delete(f"{self._resolve_path()}/modifiers")
delete_counter(id_or_name, force=False) async

Deletes an analytics counter.

Source code in src/xovis/api/device/resources/analytics.py
527
528
529
530
531
532
533
534
async def delete_counter(self, id_or_name: Union[int, str], force: bool = False) -> None:
    """
    Deletes an analytics counter.
    """
    await self._pacing_delay()
    counter_id = await self._resolve_counter_id(id_or_name)
    params = {"force": "true" if force else "false"}
    await self._http.delete(f"{self._resolve_path()}/counters/{counter_id}", params=params)
delete_logic(id_or_name, force=False) async

Deletes an analytics logic.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the logic to delete.

required
force bool

If True, forces deletion even if dependencies exist.

False
Source code in src/xovis/api/device/resources/analytics.py
290
291
292
293
294
295
296
297
298
299
300
301
async def delete_logic(self, id_or_name: Union[int, str], force: bool = False) -> None:
    """
    Deletes an analytics logic.

    Args:
        id_or_name (Union[int, str]): The ID or name of the logic to delete.
        force (bool): If True, forces deletion even if dependencies exist.
    """
    await self._pacing_delay()
    logic_id = await self._resolve_logic_id(id_or_name)
    params = {"force": "true" if force else "false"}
    await self._http.delete(f"{self._resolve_path()}/logics/{logic_id}", params=params)
delete_logic_template(id_or_name) async

Deletes a specific logic template.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the template to delete.

required
Source code in src/xovis/api/device/resources/analytics.py
344
345
346
347
348
349
350
351
352
353
async def delete_logic_template(self, id_or_name: Union[int, str]) -> None:
    """
    Deletes a specific logic template.

    Args:
        id_or_name (Union[int, str]): The ID or name of the template to delete.
    """
    await self._pacing_delay()
    template_id = await self._resolve_logic_id(id_or_name)
    await self._http.delete(f"{self._resolve_path()}/logics/templates/{template_id}")
delete_modifier(id_or_name) async

Deletes an analytics modifier.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the modifier to delete.

required
Source code in src/xovis/api/device/resources/analytics.py
435
436
437
438
439
440
441
442
443
444
async def delete_modifier(self, id_or_name: Union[int, str]) -> None:
    """
    Deletes an analytics modifier.

    Args:
        id_or_name (Union[int, str]): The ID or name of the modifier to delete.
    """
    await self._pacing_delay()
    modifier_id = await self._resolve_modifier_id(id_or_name)
    await self._http.delete(f"{self._resolve_path()}/modifiers/{modifier_id}")
execute_transaction(transaction, id_mode='SERVER') async

Executes a bundle of requests as a single atomic transaction.

Parameters:

Name Type Description Default
transaction Transaction

The transaction model containing a list of configuration mutations to execute atomically.

required
id_mode str

ID assignment strategy ("SERVER" or "CLIENT").

'SERVER'

Returns:

Name Type Description
Transaction Transaction

The executed transaction with updated IDs and statuses.

Source code in src/xovis/api/device/resources/analytics.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
async def execute_transaction(self, transaction: "Transaction", id_mode: str = "SERVER") -> "Transaction":
    """
    Executes a bundle of requests as a single atomic transaction.

    Args:
        transaction (Transaction): The transaction model containing a list
            of configuration mutations to execute atomically.
        id_mode (str): ID assignment strategy ("SERVER" or "CLIENT").

    Returns:
        Transaction: The executed transaction with updated IDs and statuses.
    """
    await self._pacing_delay()
    params = {"id_mode": id_mode}
    response = await self._http.post(f"{self._resolve_path()}/transaction", params=params, json=transaction)
    return self.models.Transaction.model_validate(response.json())
get_all_counters(logic_id=None) async

Retrieves analytics counters, optionally restricted to a specific logic.

Source code in src/xovis/api/device/resources/analytics.py
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
async def get_all_counters(self, logic_id: Optional[int] = None) -> "CounterCollection":
    """
    Retrieves analytics counters, optionally restricted to a specific logic.
    """
    params = {}
    if logic_id is not None:
        params["logic_id"] = str(logic_id)

    response = await self._http.get(f"{self._resolve_path()}/counters", params=params)
    collection = self.models.CounterCollection.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    context.counters = collection.counters

    return collection
get_all_logic_templates() async

Retrieves all pre-configured logic templates.

Returns:

Name Type Description
LogicTemplateCollection LogicTemplateCollection

The collection of available logic templates.

Source code in src/xovis/api/device/resources/analytics.py
318
319
320
321
322
323
324
325
326
async def get_all_logic_templates(self) -> "LogicTemplateCollection":
    """
    Retrieves all pre-configured logic templates.

    Returns:
        LogicTemplateCollection: The collection of available logic templates.
    """
    response = await self._http.get(f"{self._resolve_path()}/logics/templates")
    return self.models.LogicTemplateCollection.model_validate(response.json())
get_all_logics() async

Retrieves all analytics logics from the sensor.

Returns:

Name Type Description
LogicCollection LogicCollection

The collection of all configured analytics logics.

Source code in src/xovis/api/device/resources/analytics.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
async def get_all_logics(self) -> "LogicCollection":
    """
    Retrieves all analytics logics from the sensor.

    Returns:
        LogicCollection: The collection of all configured analytics logics.
    """
    response = await self._http.get(f"{self._resolve_path()}/logics")
    collection = self.models.LogicCollection.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    context.logics = collection.logics

    return collection
get_all_modifiers(logic_id=None, counter_id=None) async

Retrieves analytics modifiers, optionally filtered by parent relationships.

Parameters:

Name Type Description Default
logic_id Optional[int]

Restrict output to modifiers of a specific logic.

None
counter_id Optional[int]

Restrict output to modifiers manipulating a specific counter.

None
Source code in src/xovis/api/device/resources/analytics.py
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
async def get_all_modifiers(self, logic_id: Optional[int] = None, counter_id: Optional[int] = None) -> "ModifierCollection":
    """
    Retrieves analytics modifiers, optionally filtered by parent relationships.

    Args:
        logic_id (Optional[int]): Restrict output to modifiers of a specific logic.
        counter_id (Optional[int]): Restrict output to modifiers manipulating a specific counter.
    """
    params = {}
    if logic_id is not None:
        params["logic_id"] = str(logic_id)
    if counter_id is not None:
        params["counter_id"] = str(counter_id)

    response = await self._http.get(f"{self._resolve_path()}/modifiers", params=params)
    collection = self.models.ModifierCollection.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    context.modifiers = collection.modifiers

    return collection
get_counter(id_or_name) async

Retrieves a specific analytics counter.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the counter to retrieve.

required

Returns:

Name Type Description
Counter Counter

The retrieved analytics counter model.

Source code in src/xovis/api/device/resources/analytics.py
481
482
483
484
485
486
487
488
489
490
491
492
493
async def get_counter(self, id_or_name: Union[int, str]) -> "Counter":
    """
    Retrieves a specific analytics counter.

    Args:
        id_or_name (Union[int, str]): The ID or name of the counter to retrieve.

    Returns:
        Counter: The retrieved analytics counter model.
    """
    counter_id = await self._resolve_counter_id(id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/counters/{counter_id}")
    return self.models.Counter.model_validate(response.json())
get_counter_limits() async

Retrieves the maximum allowed number of counters for the device.

Source code in src/xovis/api/device/resources/analytics.py
454
455
456
457
async def get_counter_limits(self) -> "ElementsLimits":
    """Retrieves the maximum allowed number of counters for the device."""
    response = await self._http.get(f"{self._resolve_path()}/counters/limits")
    return self.models.ElementsLimits.model_validate(response.json())
get_logic(id_or_name) async

Retrieves a specific analytics logic.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the logic to retrieve.

required

Returns:

Name Type Description
Logic Logic

The retrieved analytics logic model.

Source code in src/xovis/api/device/resources/analytics.py
244
245
246
247
248
249
250
251
252
253
254
255
256
async def get_logic(self, id_or_name: Union[int, str]) -> "Logic":
    """
    Retrieves a specific analytics logic.

    Args:
        id_or_name (Union[int, str]): The ID or name of the logic to retrieve.

    Returns:
        Logic: The retrieved analytics logic model.
    """
    logic_id = await self._resolve_logic_id(id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/logics/{logic_id}")
    return self.models.Logic.model_validate(response.json())
get_logic_limits() async

Retrieves the maximum allowed number of logics for the device.

Source code in src/xovis/api/device/resources/analytics.py
216
217
218
219
220
221
async def get_logic_limits(self) -> "ElementsLimits":
    """
    Retrieves the maximum allowed number of logics for the device.
    """
    response = await self._http.get(f"{self._resolve_path()}/logics/limits")
    return self.models.ElementsLimits.model_validate(response.json())
get_modifier(id_or_name) async

Retrieves a specific analytics modifier.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the modifier to retrieve.

required

Returns:

Name Type Description
Modifier Modifier

The retrieved analytics modifier model.

Source code in src/xovis/api/device/resources/analytics.py
389
390
391
392
393
394
395
396
397
398
399
400
401
async def get_modifier(self, id_or_name: Union[int, str]) -> "Modifier":
    """
    Retrieves a specific analytics modifier.

    Args:
        id_or_name (Union[int, str]): The ID or name of the modifier to retrieve.

    Returns:
        Modifier: The retrieved analytics modifier model.
    """
    modifier_id = await self._resolve_modifier_id(id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/modifiers/{modifier_id}")
    return self.models.Modifier.model_validate(response.json())
get_modifier_limits() async

Retrieves the maximum allowed number of modifiers for the device.

Source code in src/xovis/api/device/resources/analytics.py
356
357
358
359
async def get_modifier_limits(self) -> "ElementsLimits":
    """Retrieves the maximum allowed number of modifiers for the device."""
    response = await self._http.get(f"{self._resolve_path()}/modifiers/limits")
    return self.models.ElementsLimits.model_validate(response.json())
reset_all_counters() async

Resets the relative/live values of all counters to zero.

Source code in src/xovis/api/device/resources/analytics.py
549
550
551
552
553
async def reset_all_counters(self) -> None:
    """
    Resets the relative/live values of all counters to zero.
    """
    await self._http.post(f"{self._resolve_path()}/counters/reset")
reset_counter(id_or_name) async

Resets the relative/live value of a specific counter to zero.

Source code in src/xovis/api/device/resources/analytics.py
542
543
544
545
546
547
async def reset_counter(self, id_or_name: Union[int, str]) -> None:
    """
    Resets the relative/live value of a specific counter to zero.
    """
    counter_id = await self._resolve_counter_id(id_or_name)
    await self._http.post(f"{self._resolve_path()}/counters/{counter_id}/reset")
reset_logic_counters(id_or_name) async

Resets the relative/live values of all counters associated with this logic to zero. Does not affect the persisted historical database.

Source code in src/xovis/api/device/resources/analytics.py
309
310
311
312
313
314
315
async def reset_logic_counters(self, id_or_name: Union[int, str]) -> None:
    """
    Resets the relative/live values of all counters associated with this logic to zero.
    Does not affect the persisted historical database.
    """
    logic_id = await self._resolve_logic_id(id_or_name)
    await self._http.post(f"{self._resolve_path()}/logics/{logic_id}/reset")
update_counter(id_or_name, counter) async

Updates an existing analytics counter.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the counter to update.

required
counter Counter

The updated counter model.

required

Returns:

Name Type Description
Counter Counter

The updated analytics counter.

Source code in src/xovis/api/device/resources/analytics.py
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
async def update_counter(self, id_or_name: Union[int, str], counter: "Counter") -> "Counter":
    """
    Updates an existing analytics counter.

    Args:
        id_or_name (Union[int, str]): The ID or name of the counter to update.
        counter (Counter): The updated counter model.

    Returns:
        Counter: The updated analytics counter.
    """
    await self._pacing_delay()
    counter_id = await self._resolve_counter_id(id_or_name)
    response = await self._http.put(f"{self._resolve_path()}/counters/{counter_id}", json=counter)
    return self.models.Counter.model_validate(response.json())
update_logic(id_or_name, logic) async

Updates an existing analytics logic.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the logic to update.

required
logic Logic

The updated logic model.

required

Returns:

Name Type Description
Logic Logic

The updated analytics logic.

Source code in src/xovis/api/device/resources/analytics.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
async def update_logic(self, id_or_name: Union[int, str], logic: "Logic") -> "Logic":
    """
    Updates an existing analytics logic.

    Args:
        id_or_name (Union[int, str]): The ID or name of the logic to update.
        logic (Logic): The updated logic model.

    Returns:
        Logic: The updated analytics logic.
    """
    await self._pacing_delay()
    logic_id = await self._resolve_logic_id(id_or_name)
    response = await self._http.put(f"{self._resolve_path()}/logics/{logic_id}", json=logic)
    return self.models.Logic.model_validate(response.json())
update_modifier(id_or_name, modifier) async

Updates an existing analytics modifier.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the modifier to update.

required
modifier Modifier

The updated modifier model.

required

Returns:

Name Type Description
Modifier Modifier

The updated analytics modifier.

Source code in src/xovis/api/device/resources/analytics.py
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
async def update_modifier(self, id_or_name: Union[int, str], modifier: "Modifier") -> "Modifier":
    """
    Updates an existing analytics modifier.

    Args:
        id_or_name (Union[int, str]): The ID or name of the modifier to update.
        modifier (Modifier): The updated modifier model.

    Returns:
        Modifier: The updated analytics modifier.
    """
    await self._pacing_delay()
    modifier_id = await self._resolve_modifier_id(id_or_name)
    response = await self._http.put(f"{self._resolve_path()}/modifiers/{modifier_id}", json=modifier)
    return self.models.Modifier.model_validate(response.json())

Functions:

_recursive_none_filter(data)

Recursively removes None values from dictionaries and lists.

Source code in src/xovis/api/device/resources/analytics.py
30
31
32
33
34
35
36
def _recursive_none_filter(data: Any) -> Any:
    """Recursively removes None values from dictionaries and lists."""
    if isinstance(data, dict):
        return {k: _recursive_none_filter(v) for k, v in data.items() if v is not None}
    elif isinstance(data, list):
        return [_recursive_none_filter(v) for v in data if v is not None]
    return data

xovis.api.device.resources.datapush

Xovis SDK - DataPush Management Resource

Operates within the Control Plane. Provides the implementation for managing high-frequency telemetry pipelines (DataPush agents and connections) on local edge sensors. Integrates advanced trigger mechanics for autonomous data recovery and fault remediation.

Classes

DataPushManager

Manages DataPush pipelines (Agents and Connections) on a Xovis device.

This manager provisions the telemetry datapush that feed downstream engines. It supports full CRUD operations, connection diagnostics, and crucially, autonomous data recovery triggers to maintain zero-downtime SLAs.

Source code in src/xovis/api/device/resources/datapush.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
class DataPushManager:
    """
    Manages DataPush pipelines (Agents and Connections) on a Xovis device.

    This manager provisions the telemetry datapush that feed downstream
    engines. It supports full CRUD operations, connection diagnostics, and
    crucially, autonomous data recovery triggers to maintain zero-downtime SLAs.
    """

    def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
        """
        Initializes the DataPushManager.

        Args:
            client (DeviceClient): The parent device client instance.
            target_id (Optional[str]): The multisensor target ID, if applicable.
                If None, defaults to the physical singlesensor context.
        """
        self._client = client
        self._http = client._http_client
        self.target_id = target_id

    @property
    def models(self) -> Any:
        """
        Returns the strictly validated Pydantic models for the current firmware.

        Returns:
            Any: The collection of auto-generated Pydantic V2 models
                synchronized with the current device firmware.
        """
        return self._client.models if self._client else stable_models

    def _resolve_path(self) -> str:
        """
        Resolves the base API path based on the current isolated context.

        Returns:
            str: The resolved API endpoint path.
        """
        if self.target_id:
            return f"/api/v5/multisensors/{self.target_id}/data/push"
        return "/api/v5/singlesensor/data/push"

    async def _resolve_agent_id(self, id_or_name: Union[str, uuid.UUID, int]) -> str:
        """
        Resolves an agent ID from either a UUID string, integer, or human-readable name.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The ID or name of the agent.

        Returns:
            str: The resolved agent ID string.

        Raises:
            ResourceNotFoundError: If the name cannot be resolved in the cache.
            MultipleResourcesFoundError: If the name is ambiguous.
        """
        if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
            return str(id_or_name)

        try:
            return str(uuid.UUID(str(id_or_name)))
        except ValueError:
            pass

        # Use cache for name resolution
        multisensors = self._client.cache.multisensors
        if self.target_id:
            target_str = str(self.target_id)
            if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
                # Proactive sync of multisensors if context is missing
                await self._client.multisensors.sync()
            context = multisensors[target_str]
        else:
            context = self._client.cache.singlesensor

        if context.agents is None or not any(a.name == id_or_name for a in context.agents):
            await self.get_all_agents()

        matches = [a for a in context.agents if a.name == id_or_name]

        if not matches:
            raise ResourceNotFoundError(f"No agent found with name '{id_or_name}'.")
        if len(matches) > 1:
            raise MultipleResourcesFoundError(f"Found {len(matches)} agents named '{id_or_name}'. Use exact ID.")
        return str(matches[0].id)

    async def _resolve_connection_id(self, id_or_name: Union[str, uuid.UUID, int]) -> str:
        """
        Resolves a connection ID from either a UUID string, integer, or human-readable name.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The ID or name of the connection.

        Returns:
            str: The resolved connection ID string.

        Raises:
            ResourceNotFoundError: If the name cannot be resolved.
            MultipleResourcesFoundError: If the name is ambiguous.
        """
        if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
            return str(id_or_name)

        try:
            return str(uuid.UUID(str(id_or_name)))
        except ValueError:
            pass

        # Use cache for name resolution
        multisensors = self._client.cache.multisensors
        if self.target_id:
            target_str = str(self.target_id)
            if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
                # Proactive sync of multisensors if context is missing
                await self._client.multisensors.sync()
            context = multisensors[target_str]
        else:
            context = self._client.cache.singlesensor

        # Proactive sync if cache is empty or name is missing
        if not context.connections or not any(c.name == id_or_name for c in context.connections):
            await self.get_all_connections()

        matches = [c for c in context.connections if c.name == id_or_name]

        if not matches:
            raise ResourceNotFoundError(f"No connection found with name '{id_or_name}'.")
        if len(matches) > 1:
            raise MultipleResourcesFoundError(f"Found {len(matches)} connections named '{id_or_name}'.")
        return str(matches[0].id)

    async def _pacing_delay(self) -> None:
        """
        Implements intra-mutation pacing to prevent sensor configuration OOM or service crashes.

        Mandatory for FW 5.9.2+ where rapid REST mutations can lead to internal service
        desynchronization and subsequent hardware reboots.
        """
        await asyncio.sleep(2.0)

    # --- AGENTS ---
    async def get_all_agents(self, volatile: bool = False) -> DataPushAgentCollection:
        """
        Retrieves all configured DataPush agents from the sensor.

        Args:
            volatile (bool): If true, returns volatile agents stored in RAM.
                Defaults to False.

        Returns:
            DataPushAgentCollection: A validated collection of agent configurations.
        """
        params = {"volatile": "true" if volatile else "false"}
        response = await self._http.get(f"{self._resolve_path()}/agents", params=params)
        collection = DataPushAgentCollection.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        context.agents = collection.agents

        return collection

    async def create_agent(self, agent: DataPushAgent, volatile: bool = False, id_mode: str = "SERVER") -> DataPushAgent:
        """
        Provisions a new DataPush agent telemetry stream.

        Args:
            agent (DataPushAgent): The validated agent configuration to create.
            volatile (bool): If true, creates a volatile agent in RAM (does not survive reboot).
            id_mode (str): The ID assignment mode ('SERVER' or 'CLIENT').

        Returns:
            DataPushAgent: The actively provisioned agent configuration.
        """
        await self._pacing_delay()
        params = {"volatile": "true" if volatile else "false", "id_mode": id_mode}

        # Force enabled to True in the model if it is unset to ensure activation.
        # Xovis firmware defaults new agents to deactivated if this field is missing.
        if agent.enabled is None:
            agent.enabled = True

        response = await self._http.post(f"{self._resolve_path()}/agents", params=params, json=agent)
        created_agent = DataPushAgent.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        if context.agents is not None:
            # Proactive sync if cache is empty to avoid list mismatch
            if not context.agents:
                await self.get_all_agents()

            # Replace or append
            new_agents = [a for a in context.agents if str(a.id) != str(created_agent.id)]
            new_agents.append(created_agent)
            context.agents = new_agents

        return created_agent

    async def delete_all_agents(self, volatile: bool = False) -> None:
        """
        Destructively removes all DataPush agents from the context.

        Args:
            volatile (bool): If true, deletes only volatile agents stored in RAM.
                Defaults to False.
        """
        await self._pacing_delay()
        params = {"volatile": "true" if volatile else "false"}
        await self._http.delete(f"{self._resolve_path()}/agents", params=params)

    async def get_agent(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushAgent:
        """
        Retrieves the exact configuration of a specific DataPush agent.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The ID or logical name of the agent.

        Returns:
            DataPushAgent: The validated agent configuration.
        """
        agent_id = await self._resolve_agent_id(id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/agents/{agent_id}")
        return DataPushAgent.model_validate(response.json())

    async def update_agent(self, id_or_name: Union[str, uuid.UUID, int], agent: DataPushAgent) -> DataPushAgent:
        """
        Replaces an existing DataPush agent configuration entirely.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The target agent.
            agent (DataPushAgent): The full updated agent configuration payload.

        Returns:
            DataPushAgent: The successfully updated agent configuration.
        """
        await self._pacing_delay()
        agent_id = await self._resolve_agent_id(id_or_name)
        response = await self._http.put(f"{self._resolve_path()}/agents/{agent_id}", json=agent)
        updated_agent = DataPushAgent.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        if context.agents is not None:
            new_agents = [a for a in context.agents if str(a.id) != str(updated_agent.id)]
            new_agents.append(updated_agent)
            context.agents = new_agents

        return updated_agent

    async def patch_agent(self, id_or_name: Union[str, uuid.UUID, int], updates: dict[str, Any]) -> DataPushAgent:
        """
        Applies a partial update to an existing DataPush agent.

        Crucial for autonomous agents modifying specific parameters (e.g., retry logic)
        without risking overwriting the entire complex payload.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The target agent.
            updates (Dict[str, Any]): A partial dictionary of modifications.

        Returns:
            DataPushAgent: The updated agent configuration.
        """
        await self._pacing_delay()
        agent_id = await self._resolve_agent_id(id_or_name)
        response = await self._http.patch(f"{self._resolve_path()}/agents/{agent_id}", json=updates)
        updated_agent = DataPushAgent.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        if context.agents is not None:
            new_agents = [a for a in context.agents if str(a.id) != str(updated_agent.id)]
            new_agents.append(updated_agent)
            context.agents = new_agents

        return updated_agent

    async def enable_agent(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushAgent:
        """Enables a specific DataPush agent on a single or multisensor context.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The target agent ID or name.

        Returns:
            DataPushAgent: The updated agent configuration.
        """
        return await self.patch_agent(id_or_name, {"enabled": True})

    async def disable_agent(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushAgent:
        """Disables a specific DataPush agent on a single or multisensor context.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The target agent ID or name.

        Returns:
            DataPushAgent: The updated agent configuration.
        """
        return await self.patch_agent(id_or_name, {"enabled": False})

    async def delete_agent(self, id_or_name: Union[str, uuid.UUID, int]) -> None:
        """Removes a specific DataPush agent."""
        await self._pacing_delay()
        agent_id = await self._resolve_agent_id(id_or_name)
        await self._http.delete(f"{self._resolve_path()}/agents/{agent_id}")

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        if context.agents is not None:
            context.agents = [a for a in context.agents if str(a.id) != str(agent_id)]

    async def get_agents_status(self, volatile: bool = False) -> DataPushStatusCollection:
        """
        Retrieves the runtime networking status for all DataPush agents.

        Args:
            volatile (bool): If true, retrieves status for volatile agents.
                Defaults to False.

        Returns:
            DataPushStatusCollection: Contains transmit speeds, dropped packet counts,
                and HTTP/MQTT failure codes across the fleet.
        """
        params = {"volatile": "true" if volatile else "false"}
        response = await self._http.get(f"{self._resolve_path()}/agents/status", params=params)
        return DataPushStatusCollection.model_validate(response.json())

    async def get_agent_status(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushStatus:
        """
        Retrieves the runtime status for a specific DataPush agent.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The ID or logical name
                of the target agent.

        Returns:
            DataPushStatus: The runtime status of the specific agent.
        """
        agent_id = await self._resolve_agent_id(id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/agents/{agent_id}/status")
        return DataPushStatus.model_validate(response.json())

    # --- AGENT TRIGGERS (DATA RECOVERY) ---
    async def trigger_agent_push(self, id_or_name: Union[str, uuid.UUID, int], trigger_config: DataPushTriggerConfig) -> DataPushTriggerInfo:
        """
        Forces the agent to immediately flush or recover data.

        Essential for Autonomous Maintenance. If a sensor was offline, an agent can
        use a 'TIME_RANGE' trigger configuration to datapush the missing edge-cached data
        up to the downstream processing engine.

        NOTE: 'STATUS' and 'RECORDING' agents do not support manual retriggering.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The target agent.
            trigger_config (DataPushTriggerConfig): Validated Pydantic configuration defining
                'ALL', 'LAST_PACKAGE', or 'TIME_RANGE' (with XovisTime) rules.

        Returns:
            DataPushTriggerInfo: The active trigger execution status.
        """
        await self._pacing_delay()

        # PROACTIVE BLOCK: Status and Recording agents do not support retriggering.
        # This prevents hardware-level rejections or instability.
        from xovis.models.device import DataPushType

        agent = await self.get_agent(id_or_name)
        if agent.type in (DataPushType.STATUS, DataPushType.RECORDING):
            import logging

            logger = logging.getLogger(__name__)
            logger.warning(f"Manual retriggering is not supported for agent type: {agent.type}")
            # We return an IDLE status instead of raising to avoid breaking autonomous loops
            return DataPushTriggerInfo(status="IDLE", trigger_config=trigger_config)

        agent_id = agent.id
        response = await self._http.post(f"{self._resolve_path()}/agents/{agent_id}/trigger", json=trigger_config)

        # FIRMWARE BUG WORKAROUND:
        # Some firmware versions return a list containing the status object instead of the object itself.
        # Or a list of strings for errors.
        # Or the trigger config itself instead of status (seen on some Multisensor firmware).
        data = response.json()
        if isinstance(data, list) and len(data) > 0:
            if isinstance(data[0], dict):
                data = data[0]

        # If the firmware returned the config instead of status, synthesize an IDLE status
        if isinstance(data, dict) and "status" not in data and "type" in data:
            data = {"status": "IDLE", "trigger_config": data}

        return DataPushTriggerInfo.model_validate(data)

    async def get_agent_trigger_status(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushTriggerInfo:
        """
        Polls the execution state of an actively running trigger datapush (BUSY or IDLE).
        """
        agent_id = await self._resolve_agent_id(id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/agents/{agent_id}/trigger")

        # FIRMWARE BUG WORKAROUND:
        # Some firmware versions return a list containing the status object instead of the object itself.
        # Or a list of strings for errors.
        # Or the trigger config itself instead of status (seen on some Multisensor firmware).
        data = response.json()
        if isinstance(data, list) and len(data) > 0:
            if isinstance(data[0], dict):
                data = data[0]

        # If the firmware returned the config instead of status, synthesize an IDLE status
        if isinstance(data, dict) and "status" not in data and "type" in data:
            data = {"status": "IDLE", "trigger_config": data}

        return DataPushTriggerInfo.model_validate(data)

    async def abort_agent_trigger(self, id_or_name: Union[str, uuid.UUID, int]) -> None:
        """Aborts a currently running trigger datapush."""
        await self._pacing_delay()
        agent_id = await self._resolve_agent_id(id_or_name)
        await self._http.delete(f"{self._resolve_path()}/agents/{agent_id}/trigger")

    # --- CONNECTIONS ---
    async def get_all_connections(self, volatile: bool = False) -> DataPushConnectionCollection:
        """Retrieves all defined network targets (Connections) for agents."""
        params = {"volatile": "true" if volatile else "false"}
        response = await self._http.get(f"{self._resolve_path()}/connections", params=params)
        collection = DataPushConnectionCollection.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        context.connections = collection.connections

        return collection

    async def create_connection(self, connection: DataPushConnection, volatile: bool = False, id_mode: str = "SERVER") -> DataPushConnection:
        """Provisions a new network target (HTTP, MQTT, TCP, etc.)."""
        await self._pacing_delay()
        params = {"volatile": "true" if volatile else "false", "id_mode": id_mode}
        response = await self._http.post(f"{self._resolve_path()}/connections", params=params, json=connection)
        created_conn = DataPushConnection.model_validate(response.json())

        # Sync cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        if context.connections is not None:
            # Proactive sync if cache is empty to avoid list mismatch
            if not context.connections:
                await self.get_all_connections()

            new_conns = [c for c in context.connections if str(c.id) != str(created_conn.id)]
            new_conns.append(created_conn)
            context.connections = new_conns

        return created_conn

    async def delete_all_connections(self, volatile: bool = False) -> None:
        """
        Destructively removes all connections from the context.

        Args:
            volatile (bool): If true, deletes only volatile connections.
                Defaults to False.
        """
        await self._pacing_delay()
        params = {"volatile": "true" if volatile else "false"}
        await self._http.delete(f"{self._resolve_path()}/connections", params=params)

    async def get_connection(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushConnection:
        """
        Retrieves a specific connection configuration.

        Args:
            id_or_name (Union[str, uuid.UUID, int]): The ID or logical name
                of the target connection.

        Returns:
            DataPushConnection: The validated connection configuration.
        """
        conn_id = await self._resolve_connection_id(id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/connections/{conn_id}")
        return DataPushConnection.model_validate(response.json())

    async def update_connection(self, id_or_name: Union[str, uuid.UUID, int], connection: DataPushConnection) -> DataPushConnection:
        """Replaces an existing connection configuration entirely."""
        await self._pacing_delay()
        conn_id = await self._resolve_connection_id(id_or_name)
        response = await self._http.put(f"{self._resolve_path()}/connections/{conn_id}", json=connection)
        updated_conn = DataPushConnection.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        if context.connections is not None:
            new_conns = [c for c in context.connections if str(c.id) != str(updated_conn.id)]
            new_conns.append(updated_conn)
            context.connections = new_conns

        return updated_conn

    async def patch_connection(self, id_or_name: Union[str, uuid.UUID, int], updates: dict[str, Any]) -> DataPushConnection:
        """
        Applies a partial update to an existing connection.
        Useful for rotating passwords or updating host URIs autonomously.
        """
        await self._pacing_delay()
        conn_id = await self._resolve_connection_id(id_or_name)
        response = await self._http.patch(f"{self._resolve_path()}/connections/{conn_id}", json=updates)
        updated_conn = DataPushConnection.model_validate(response.json())

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        if context.connections is not None:
            new_conns = [c for c in context.connections if str(c.id) != str(updated_conn.id)]
            new_conns.append(updated_conn)
            context.connections = new_conns

        return updated_conn

    async def delete_connection(self, id_or_name: Union[str, uuid.UUID, int]) -> None:
        """Removes a specific connection."""
        await self._pacing_delay()
        conn_id = await self._resolve_connection_id(id_or_name)
        await self._http.delete(f"{self._resolve_path()}/connections/{conn_id}")

        # Update cache
        multisensors = self._client.cache.multisensors
        context = (
            multisensors[str(self.target_id)]
            if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
            else self._client.cache.singlesensor
        )
        if context.connections is not None:
            context.connections = [c for c in context.connections if str(c.id) != str(conn_id)]

    async def test_connection(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushTestResponse:
        """
        Fires a dummy payload through the connection to verify network egress.
        Crucial for pre-flight validation by autonomous deployment agents.
        """
        conn_id = await self._resolve_connection_id(id_or_name)
        response = await self._http.post(f"{self._resolve_path()}/connections/{conn_id}/test")
        return DataPushTestResponse.model_validate(response.json())

    # --- LEGACY CONFIGURATION ---
    async def get_legacy_config(self) -> "LegacyConfigGet":
        """Retrieves legacy conversion settings for backward-compatible systems."""
        response = await self._http.get(f"{self._resolve_path()}/legacy")
        return self.models.LegacyConfigGet.model_validate(response.json())

    async def update_legacy_config(self, config: "LegacyConfigPut") -> "LegacyConfigGet":
        """Updates legacy conversion settings."""
        await self._pacing_delay()
        response = await self._http.put(f"{self._resolve_path()}/legacy", json=config)
        return self.models.LegacyConfigGet.model_validate(response.json())

    async def delete_legacy_config(self) -> None:
        """Deletes legacy conversion settings."""
        await self._pacing_delay()
        await self._http.delete(f"{self._resolve_path()}/legacy")
Attributes
models property

Returns the strictly validated Pydantic models for the current firmware.

Returns:

Name Type Description
Any Any

The collection of auto-generated Pydantic V2 models synchronized with the current device firmware.

Methods:
__init__(client, target_id=None)

Initializes the DataPushManager.

Parameters:

Name Type Description Default
client DeviceClient

The parent device client instance.

required
target_id Optional[str]

The multisensor target ID, if applicable. If None, defaults to the physical singlesensor context.

None
Source code in src/xovis/api/device/resources/datapush.py
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
    """
    Initializes the DataPushManager.

    Args:
        client (DeviceClient): The parent device client instance.
        target_id (Optional[str]): The multisensor target ID, if applicable.
            If None, defaults to the physical singlesensor context.
    """
    self._client = client
    self._http = client._http_client
    self.target_id = target_id
_pacing_delay() async

Implements intra-mutation pacing to prevent sensor configuration OOM or service crashes.

Mandatory for FW 5.9.2+ where rapid REST mutations can lead to internal service desynchronization and subsequent hardware reboots.

Source code in src/xovis/api/device/resources/datapush.py
175
176
177
178
179
180
181
182
async def _pacing_delay(self) -> None:
    """
    Implements intra-mutation pacing to prevent sensor configuration OOM or service crashes.

    Mandatory for FW 5.9.2+ where rapid REST mutations can lead to internal service
    desynchronization and subsequent hardware reboots.
    """
    await asyncio.sleep(2.0)
_resolve_agent_id(id_or_name) async

Resolves an agent ID from either a UUID string, integer, or human-readable name.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The ID or name of the agent.

required

Returns:

Name Type Description
str str

The resolved agent ID string.

Raises:

Type Description
ResourceNotFoundError

If the name cannot be resolved in the cache.

MultipleResourcesFoundError

If the name is ambiguous.

Source code in src/xovis/api/device/resources/datapush.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
async def _resolve_agent_id(self, id_or_name: Union[str, uuid.UUID, int]) -> str:
    """
    Resolves an agent ID from either a UUID string, integer, or human-readable name.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The ID or name of the agent.

    Returns:
        str: The resolved agent ID string.

    Raises:
        ResourceNotFoundError: If the name cannot be resolved in the cache.
        MultipleResourcesFoundError: If the name is ambiguous.
    """
    if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
        return str(id_or_name)

    try:
        return str(uuid.UUID(str(id_or_name)))
    except ValueError:
        pass

    # Use cache for name resolution
    multisensors = self._client.cache.multisensors
    if self.target_id:
        target_str = str(self.target_id)
        if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
            # Proactive sync of multisensors if context is missing
            await self._client.multisensors.sync()
        context = multisensors[target_str]
    else:
        context = self._client.cache.singlesensor

    if context.agents is None or not any(a.name == id_or_name for a in context.agents):
        await self.get_all_agents()

    matches = [a for a in context.agents if a.name == id_or_name]

    if not matches:
        raise ResourceNotFoundError(f"No agent found with name '{id_or_name}'.")
    if len(matches) > 1:
        raise MultipleResourcesFoundError(f"Found {len(matches)} agents named '{id_or_name}'. Use exact ID.")
    return str(matches[0].id)
_resolve_connection_id(id_or_name) async

Resolves a connection ID from either a UUID string, integer, or human-readable name.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The ID or name of the connection.

required

Returns:

Name Type Description
str str

The resolved connection ID string.

Raises:

Type Description
ResourceNotFoundError

If the name cannot be resolved.

MultipleResourcesFoundError

If the name is ambiguous.

Source code in src/xovis/api/device/resources/datapush.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
async def _resolve_connection_id(self, id_or_name: Union[str, uuid.UUID, int]) -> str:
    """
    Resolves a connection ID from either a UUID string, integer, or human-readable name.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The ID or name of the connection.

    Returns:
        str: The resolved connection ID string.

    Raises:
        ResourceNotFoundError: If the name cannot be resolved.
        MultipleResourcesFoundError: If the name is ambiguous.
    """
    if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
        return str(id_or_name)

    try:
        return str(uuid.UUID(str(id_or_name)))
    except ValueError:
        pass

    # Use cache for name resolution
    multisensors = self._client.cache.multisensors
    if self.target_id:
        target_str = str(self.target_id)
        if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
            # Proactive sync of multisensors if context is missing
            await self._client.multisensors.sync()
        context = multisensors[target_str]
    else:
        context = self._client.cache.singlesensor

    # Proactive sync if cache is empty or name is missing
    if not context.connections or not any(c.name == id_or_name for c in context.connections):
        await self.get_all_connections()

    matches = [c for c in context.connections if c.name == id_or_name]

    if not matches:
        raise ResourceNotFoundError(f"No connection found with name '{id_or_name}'.")
    if len(matches) > 1:
        raise MultipleResourcesFoundError(f"Found {len(matches)} connections named '{id_or_name}'.")
    return str(matches[0].id)
_resolve_path()

Resolves the base API path based on the current isolated context.

Returns:

Name Type Description
str str

The resolved API endpoint path.

Source code in src/xovis/api/device/resources/datapush.py
75
76
77
78
79
80
81
82
83
84
def _resolve_path(self) -> str:
    """
    Resolves the base API path based on the current isolated context.

    Returns:
        str: The resolved API endpoint path.
    """
    if self.target_id:
        return f"/api/v5/multisensors/{self.target_id}/data/push"
    return "/api/v5/singlesensor/data/push"
abort_agent_trigger(id_or_name) async

Aborts a currently running trigger datapush.

Source code in src/xovis/api/device/resources/datapush.py
485
486
487
488
489
async def abort_agent_trigger(self, id_or_name: Union[str, uuid.UUID, int]) -> None:
    """Aborts a currently running trigger datapush."""
    await self._pacing_delay()
    agent_id = await self._resolve_agent_id(id_or_name)
    await self._http.delete(f"{self._resolve_path()}/agents/{agent_id}/trigger")
create_agent(agent, volatile=False, id_mode='SERVER') async

Provisions a new DataPush agent telemetry stream.

Parameters:

Name Type Description Default
agent DataPushAgent

The validated agent configuration to create.

required
volatile bool

If true, creates a volatile agent in RAM (does not survive reboot).

False
id_mode str

The ID assignment mode ('SERVER' or 'CLIENT').

'SERVER'

Returns:

Name Type Description
DataPushAgent DataPushAgent

The actively provisioned agent configuration.

Source code in src/xovis/api/device/resources/datapush.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
async def create_agent(self, agent: DataPushAgent, volatile: bool = False, id_mode: str = "SERVER") -> DataPushAgent:
    """
    Provisions a new DataPush agent telemetry stream.

    Args:
        agent (DataPushAgent): The validated agent configuration to create.
        volatile (bool): If true, creates a volatile agent in RAM (does not survive reboot).
        id_mode (str): The ID assignment mode ('SERVER' or 'CLIENT').

    Returns:
        DataPushAgent: The actively provisioned agent configuration.
    """
    await self._pacing_delay()
    params = {"volatile": "true" if volatile else "false", "id_mode": id_mode}

    # Force enabled to True in the model if it is unset to ensure activation.
    # Xovis firmware defaults new agents to deactivated if this field is missing.
    if agent.enabled is None:
        agent.enabled = True

    response = await self._http.post(f"{self._resolve_path()}/agents", params=params, json=agent)
    created_agent = DataPushAgent.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    if context.agents is not None:
        # Proactive sync if cache is empty to avoid list mismatch
        if not context.agents:
            await self.get_all_agents()

        # Replace or append
        new_agents = [a for a in context.agents if str(a.id) != str(created_agent.id)]
        new_agents.append(created_agent)
        context.agents = new_agents

    return created_agent
create_connection(connection, volatile=False, id_mode='SERVER') async

Provisions a new network target (HTTP, MQTT, TCP, etc.).

Source code in src/xovis/api/device/resources/datapush.py
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
async def create_connection(self, connection: DataPushConnection, volatile: bool = False, id_mode: str = "SERVER") -> DataPushConnection:
    """Provisions a new network target (HTTP, MQTT, TCP, etc.)."""
    await self._pacing_delay()
    params = {"volatile": "true" if volatile else "false", "id_mode": id_mode}
    response = await self._http.post(f"{self._resolve_path()}/connections", params=params, json=connection)
    created_conn = DataPushConnection.model_validate(response.json())

    # Sync cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    if context.connections is not None:
        # Proactive sync if cache is empty to avoid list mismatch
        if not context.connections:
            await self.get_all_connections()

        new_conns = [c for c in context.connections if str(c.id) != str(created_conn.id)]
        new_conns.append(created_conn)
        context.connections = new_conns

    return created_conn
delete_agent(id_or_name) async

Removes a specific DataPush agent.

Source code in src/xovis/api/device/resources/datapush.py
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
async def delete_agent(self, id_or_name: Union[str, uuid.UUID, int]) -> None:
    """Removes a specific DataPush agent."""
    await self._pacing_delay()
    agent_id = await self._resolve_agent_id(id_or_name)
    await self._http.delete(f"{self._resolve_path()}/agents/{agent_id}")

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    if context.agents is not None:
        context.agents = [a for a in context.agents if str(a.id) != str(agent_id)]
delete_all_agents(volatile=False) async

Destructively removes all DataPush agents from the context.

Parameters:

Name Type Description Default
volatile bool

If true, deletes only volatile agents stored in RAM. Defaults to False.

False
Source code in src/xovis/api/device/resources/datapush.py
253
254
255
256
257
258
259
260
261
262
263
async def delete_all_agents(self, volatile: bool = False) -> None:
    """
    Destructively removes all DataPush agents from the context.

    Args:
        volatile (bool): If true, deletes only volatile agents stored in RAM.
            Defaults to False.
    """
    await self._pacing_delay()
    params = {"volatile": "true" if volatile else "false"}
    await self._http.delete(f"{self._resolve_path()}/agents", params=params)
delete_all_connections(volatile=False) async

Destructively removes all connections from the context.

Parameters:

Name Type Description Default
volatile bool

If true, deletes only volatile connections. Defaults to False.

False
Source code in src/xovis/api/device/resources/datapush.py
534
535
536
537
538
539
540
541
542
543
544
async def delete_all_connections(self, volatile: bool = False) -> None:
    """
    Destructively removes all connections from the context.

    Args:
        volatile (bool): If true, deletes only volatile connections.
            Defaults to False.
    """
    await self._pacing_delay()
    params = {"volatile": "true" if volatile else "false"}
    await self._http.delete(f"{self._resolve_path()}/connections", params=params)
delete_connection(id_or_name) async

Removes a specific connection.

Source code in src/xovis/api/device/resources/datapush.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
async def delete_connection(self, id_or_name: Union[str, uuid.UUID, int]) -> None:
    """Removes a specific connection."""
    await self._pacing_delay()
    conn_id = await self._resolve_connection_id(id_or_name)
    await self._http.delete(f"{self._resolve_path()}/connections/{conn_id}")

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    if context.connections is not None:
        context.connections = [c for c in context.connections if str(c.id) != str(conn_id)]
delete_legacy_config() async

Deletes legacy conversion settings.

Source code in src/xovis/api/device/resources/datapush.py
643
644
645
646
async def delete_legacy_config(self) -> None:
    """Deletes legacy conversion settings."""
    await self._pacing_delay()
    await self._http.delete(f"{self._resolve_path()}/legacy")
disable_agent(id_or_name) async

Disables a specific DataPush agent on a single or multisensor context.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The target agent ID or name.

required

Returns:

Name Type Description
DataPushAgent DataPushAgent

The updated agent configuration.

Source code in src/xovis/api/device/resources/datapush.py
353
354
355
356
357
358
359
360
361
362
async def disable_agent(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushAgent:
    """Disables a specific DataPush agent on a single or multisensor context.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The target agent ID or name.

    Returns:
        DataPushAgent: The updated agent configuration.
    """
    return await self.patch_agent(id_or_name, {"enabled": False})
enable_agent(id_or_name) async

Enables a specific DataPush agent on a single or multisensor context.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The target agent ID or name.

required

Returns:

Name Type Description
DataPushAgent DataPushAgent

The updated agent configuration.

Source code in src/xovis/api/device/resources/datapush.py
342
343
344
345
346
347
348
349
350
351
async def enable_agent(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushAgent:
    """Enables a specific DataPush agent on a single or multisensor context.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The target agent ID or name.

    Returns:
        DataPushAgent: The updated agent configuration.
    """
    return await self.patch_agent(id_or_name, {"enabled": True})
get_agent(id_or_name) async

Retrieves the exact configuration of a specific DataPush agent.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The ID or logical name of the agent.

required

Returns:

Name Type Description
DataPushAgent DataPushAgent

The validated agent configuration.

Source code in src/xovis/api/device/resources/datapush.py
265
266
267
268
269
270
271
272
273
274
275
276
277
async def get_agent(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushAgent:
    """
    Retrieves the exact configuration of a specific DataPush agent.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The ID or logical name of the agent.

    Returns:
        DataPushAgent: The validated agent configuration.
    """
    agent_id = await self._resolve_agent_id(id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/agents/{agent_id}")
    return DataPushAgent.model_validate(response.json())
get_agent_status(id_or_name) async

Retrieves the runtime status for a specific DataPush agent.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The ID or logical name of the target agent.

required

Returns:

Name Type Description
DataPushStatus DataPushStatus

The runtime status of the specific agent.

Source code in src/xovis/api/device/resources/datapush.py
396
397
398
399
400
401
402
403
404
405
406
407
408
409
async def get_agent_status(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushStatus:
    """
    Retrieves the runtime status for a specific DataPush agent.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The ID or logical name
            of the target agent.

    Returns:
        DataPushStatus: The runtime status of the specific agent.
    """
    agent_id = await self._resolve_agent_id(id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/agents/{agent_id}/status")
    return DataPushStatus.model_validate(response.json())
get_agent_trigger_status(id_or_name) async

Polls the execution state of an actively running trigger datapush (BUSY or IDLE).

Source code in src/xovis/api/device/resources/datapush.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
async def get_agent_trigger_status(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushTriggerInfo:
    """
    Polls the execution state of an actively running trigger datapush (BUSY or IDLE).
    """
    agent_id = await self._resolve_agent_id(id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/agents/{agent_id}/trigger")

    # FIRMWARE BUG WORKAROUND:
    # Some firmware versions return a list containing the status object instead of the object itself.
    # Or a list of strings for errors.
    # Or the trigger config itself instead of status (seen on some Multisensor firmware).
    data = response.json()
    if isinstance(data, list) and len(data) > 0:
        if isinstance(data[0], dict):
            data = data[0]

    # If the firmware returned the config instead of status, synthesize an IDLE status
    if isinstance(data, dict) and "status" not in data and "type" in data:
        data = {"status": "IDLE", "trigger_config": data}

    return DataPushTriggerInfo.model_validate(data)
get_agents_status(volatile=False) async

Retrieves the runtime networking status for all DataPush agents.

Parameters:

Name Type Description Default
volatile bool

If true, retrieves status for volatile agents. Defaults to False.

False

Returns:

Name Type Description
DataPushStatusCollection DataPushStatusCollection

Contains transmit speeds, dropped packet counts, and HTTP/MQTT failure codes across the fleet.

Source code in src/xovis/api/device/resources/datapush.py
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
async def get_agents_status(self, volatile: bool = False) -> DataPushStatusCollection:
    """
    Retrieves the runtime networking status for all DataPush agents.

    Args:
        volatile (bool): If true, retrieves status for volatile agents.
            Defaults to False.

    Returns:
        DataPushStatusCollection: Contains transmit speeds, dropped packet counts,
            and HTTP/MQTT failure codes across the fleet.
    """
    params = {"volatile": "true" if volatile else "false"}
    response = await self._http.get(f"{self._resolve_path()}/agents/status", params=params)
    return DataPushStatusCollection.model_validate(response.json())
get_all_agents(volatile=False) async

Retrieves all configured DataPush agents from the sensor.

Parameters:

Name Type Description Default
volatile bool

If true, returns volatile agents stored in RAM. Defaults to False.

False

Returns:

Name Type Description
DataPushAgentCollection DataPushAgentCollection

A validated collection of agent configurations.

Source code in src/xovis/api/device/resources/datapush.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
async def get_all_agents(self, volatile: bool = False) -> DataPushAgentCollection:
    """
    Retrieves all configured DataPush agents from the sensor.

    Args:
        volatile (bool): If true, returns volatile agents stored in RAM.
            Defaults to False.

    Returns:
        DataPushAgentCollection: A validated collection of agent configurations.
    """
    params = {"volatile": "true" if volatile else "false"}
    response = await self._http.get(f"{self._resolve_path()}/agents", params=params)
    collection = DataPushAgentCollection.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    context.agents = collection.agents

    return collection
get_all_connections(volatile=False) async

Retrieves all defined network targets (Connections) for agents.

Source code in src/xovis/api/device/resources/datapush.py
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
async def get_all_connections(self, volatile: bool = False) -> DataPushConnectionCollection:
    """Retrieves all defined network targets (Connections) for agents."""
    params = {"volatile": "true" if volatile else "false"}
    response = await self._http.get(f"{self._resolve_path()}/connections", params=params)
    collection = DataPushConnectionCollection.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    context.connections = collection.connections

    return collection
get_connection(id_or_name) async

Retrieves a specific connection configuration.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The ID or logical name of the target connection.

required

Returns:

Name Type Description
DataPushConnection DataPushConnection

The validated connection configuration.

Source code in src/xovis/api/device/resources/datapush.py
546
547
548
549
550
551
552
553
554
555
556
557
558
559
async def get_connection(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushConnection:
    """
    Retrieves a specific connection configuration.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The ID or logical name
            of the target connection.

    Returns:
        DataPushConnection: The validated connection configuration.
    """
    conn_id = await self._resolve_connection_id(id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/connections/{conn_id}")
    return DataPushConnection.model_validate(response.json())
get_legacy_config() async

Retrieves legacy conversion settings for backward-compatible systems.

Source code in src/xovis/api/device/resources/datapush.py
632
633
634
635
async def get_legacy_config(self) -> "LegacyConfigGet":
    """Retrieves legacy conversion settings for backward-compatible systems."""
    response = await self._http.get(f"{self._resolve_path()}/legacy")
    return self.models.LegacyConfigGet.model_validate(response.json())
patch_agent(id_or_name, updates) async

Applies a partial update to an existing DataPush agent.

Crucial for autonomous agents modifying specific parameters (e.g., retry logic) without risking overwriting the entire complex payload.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The target agent.

required
updates Dict[str, Any]

A partial dictionary of modifications.

required

Returns:

Name Type Description
DataPushAgent DataPushAgent

The updated agent configuration.

Source code in src/xovis/api/device/resources/datapush.py
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
async def patch_agent(self, id_or_name: Union[str, uuid.UUID, int], updates: dict[str, Any]) -> DataPushAgent:
    """
    Applies a partial update to an existing DataPush agent.

    Crucial for autonomous agents modifying specific parameters (e.g., retry logic)
    without risking overwriting the entire complex payload.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The target agent.
        updates (Dict[str, Any]): A partial dictionary of modifications.

    Returns:
        DataPushAgent: The updated agent configuration.
    """
    await self._pacing_delay()
    agent_id = await self._resolve_agent_id(id_or_name)
    response = await self._http.patch(f"{self._resolve_path()}/agents/{agent_id}", json=updates)
    updated_agent = DataPushAgent.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    if context.agents is not None:
        new_agents = [a for a in context.agents if str(a.id) != str(updated_agent.id)]
        new_agents.append(updated_agent)
        context.agents = new_agents

    return updated_agent
patch_connection(id_or_name, updates) async

Applies a partial update to an existing connection. Useful for rotating passwords or updating host URIs autonomously.

Source code in src/xovis/api/device/resources/datapush.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
async def patch_connection(self, id_or_name: Union[str, uuid.UUID, int], updates: dict[str, Any]) -> DataPushConnection:
    """
    Applies a partial update to an existing connection.
    Useful for rotating passwords or updating host URIs autonomously.
    """
    await self._pacing_delay()
    conn_id = await self._resolve_connection_id(id_or_name)
    response = await self._http.patch(f"{self._resolve_path()}/connections/{conn_id}", json=updates)
    updated_conn = DataPushConnection.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    if context.connections is not None:
        new_conns = [c for c in context.connections if str(c.id) != str(updated_conn.id)]
        new_conns.append(updated_conn)
        context.connections = new_conns

    return updated_conn
test_connection(id_or_name) async

Fires a dummy payload through the connection to verify network egress. Crucial for pre-flight validation by autonomous deployment agents.

Source code in src/xovis/api/device/resources/datapush.py
622
623
624
625
626
627
628
629
async def test_connection(self, id_or_name: Union[str, uuid.UUID, int]) -> DataPushTestResponse:
    """
    Fires a dummy payload through the connection to verify network egress.
    Crucial for pre-flight validation by autonomous deployment agents.
    """
    conn_id = await self._resolve_connection_id(id_or_name)
    response = await self._http.post(f"{self._resolve_path()}/connections/{conn_id}/test")
    return DataPushTestResponse.model_validate(response.json())
trigger_agent_push(id_or_name, trigger_config) async

Forces the agent to immediately flush or recover data.

Essential for Autonomous Maintenance. If a sensor was offline, an agent can use a 'TIME_RANGE' trigger configuration to datapush the missing edge-cached data up to the downstream processing engine.

NOTE: 'STATUS' and 'RECORDING' agents do not support manual retriggering.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The target agent.

required
trigger_config DataPushTriggerConfig

Validated Pydantic configuration defining 'ALL', 'LAST_PACKAGE', or 'TIME_RANGE' (with XovisTime) rules.

required

Returns:

Name Type Description
DataPushTriggerInfo DataPushTriggerInfo

The active trigger execution status.

Source code in src/xovis/api/device/resources/datapush.py
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
async def trigger_agent_push(self, id_or_name: Union[str, uuid.UUID, int], trigger_config: DataPushTriggerConfig) -> DataPushTriggerInfo:
    """
    Forces the agent to immediately flush or recover data.

    Essential for Autonomous Maintenance. If a sensor was offline, an agent can
    use a 'TIME_RANGE' trigger configuration to datapush the missing edge-cached data
    up to the downstream processing engine.

    NOTE: 'STATUS' and 'RECORDING' agents do not support manual retriggering.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The target agent.
        trigger_config (DataPushTriggerConfig): Validated Pydantic configuration defining
            'ALL', 'LAST_PACKAGE', or 'TIME_RANGE' (with XovisTime) rules.

    Returns:
        DataPushTriggerInfo: The active trigger execution status.
    """
    await self._pacing_delay()

    # PROACTIVE BLOCK: Status and Recording agents do not support retriggering.
    # This prevents hardware-level rejections or instability.
    from xovis.models.device import DataPushType

    agent = await self.get_agent(id_or_name)
    if agent.type in (DataPushType.STATUS, DataPushType.RECORDING):
        import logging

        logger = logging.getLogger(__name__)
        logger.warning(f"Manual retriggering is not supported for agent type: {agent.type}")
        # We return an IDLE status instead of raising to avoid breaking autonomous loops
        return DataPushTriggerInfo(status="IDLE", trigger_config=trigger_config)

    agent_id = agent.id
    response = await self._http.post(f"{self._resolve_path()}/agents/{agent_id}/trigger", json=trigger_config)

    # FIRMWARE BUG WORKAROUND:
    # Some firmware versions return a list containing the status object instead of the object itself.
    # Or a list of strings for errors.
    # Or the trigger config itself instead of status (seen on some Multisensor firmware).
    data = response.json()
    if isinstance(data, list) and len(data) > 0:
        if isinstance(data[0], dict):
            data = data[0]

    # If the firmware returned the config instead of status, synthesize an IDLE status
    if isinstance(data, dict) and "status" not in data and "type" in data:
        data = {"status": "IDLE", "trigger_config": data}

    return DataPushTriggerInfo.model_validate(data)
update_agent(id_or_name, agent) async

Replaces an existing DataPush agent configuration entirely.

Parameters:

Name Type Description Default
id_or_name Union[str, UUID, int]

The target agent.

required
agent DataPushAgent

The full updated agent configuration payload.

required

Returns:

Name Type Description
DataPushAgent DataPushAgent

The successfully updated agent configuration.

Source code in src/xovis/api/device/resources/datapush.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
async def update_agent(self, id_or_name: Union[str, uuid.UUID, int], agent: DataPushAgent) -> DataPushAgent:
    """
    Replaces an existing DataPush agent configuration entirely.

    Args:
        id_or_name (Union[str, uuid.UUID, int]): The target agent.
        agent (DataPushAgent): The full updated agent configuration payload.

    Returns:
        DataPushAgent: The successfully updated agent configuration.
    """
    await self._pacing_delay()
    agent_id = await self._resolve_agent_id(id_or_name)
    response = await self._http.put(f"{self._resolve_path()}/agents/{agent_id}", json=agent)
    updated_agent = DataPushAgent.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    if context.agents is not None:
        new_agents = [a for a in context.agents if str(a.id) != str(updated_agent.id)]
        new_agents.append(updated_agent)
        context.agents = new_agents

    return updated_agent
update_connection(id_or_name, connection) async

Replaces an existing connection configuration entirely.

Source code in src/xovis/api/device/resources/datapush.py
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
async def update_connection(self, id_or_name: Union[str, uuid.UUID, int], connection: DataPushConnection) -> DataPushConnection:
    """Replaces an existing connection configuration entirely."""
    await self._pacing_delay()
    conn_id = await self._resolve_connection_id(id_or_name)
    response = await self._http.put(f"{self._resolve_path()}/connections/{conn_id}", json=connection)
    updated_conn = DataPushConnection.model_validate(response.json())

    # Update cache
    multisensors = self._client.cache.multisensors
    context = (
        multisensors[str(self.target_id)]
        if self.target_id and str(self.target_id) in (multisensors._items if hasattr(multisensors, "_items") else multisensors)
        else self._client.cache.singlesensor
    )
    if context.connections is not None:
        new_conns = [c for c in context.connections if str(c.id) != str(updated_conn.id)]
        new_conns.append(updated_conn)
        context.connections = new_conns

    return updated_conn
update_legacy_config(config) async

Updates legacy conversion settings.

Source code in src/xovis/api/device/resources/datapush.py
637
638
639
640
641
async def update_legacy_config(self, config: "LegacyConfigPut") -> "LegacyConfigGet":
    """Updates legacy conversion settings."""
    await self._pacing_delay()
    response = await self._http.put(f"{self._resolve_path()}/legacy", json=config)
    return self.models.LegacyConfigGet.model_validate(response.json())

Functions:

_recursive_none_filter(data)

Recursively removes None values from dictionaries and lists.

Source code in src/xovis/api/device/resources/datapush.py
33
34
35
36
37
38
39
def _recursive_none_filter(data: Any) -> Any:
    """Recursively removes None values from dictionaries and lists."""
    if isinstance(data, dict):
        return {k: _recursive_none_filter(v) for k, v in data.items() if v is not None}
    elif isinstance(data, list):
        return [_recursive_none_filter(v) for v in data if v is not None]
    return data

xovis.api.device.resources.history

Xovis SDK - History Management Resource

Operates within the Control Plane. Provides the implementation for retrieving historical counting data, start/stop spatial tracking coordinates, and diagnostic memory states from local edge sensors. Critical for Layer 3/4 spatial analytics and Geometry Optimization (Module C).

Attributes

Classes

HistoryManager

Manages historical data retrieval for the single-sensor context.

This manager abstracts the complex time-series databases of the edge sensor. It exposes aggregated logic measurements, start/stop tracking coordinates, spatial heat/height maps, and underlying database diagnostic statuses.

Source code in src/xovis/api/device/resources/history.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
class HistoryManager:
    """
    Manages historical data retrieval for the single-sensor context.

    This manager abstracts the complex time-series databases of the edge sensor.
    It exposes aggregated logic measurements, start/stop tracking coordinates,
    spatial heat/height maps, and underlying database diagnostic statuses.
    """

    def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
        """
        Initializes the HistoryManager.

        Args:
            client (DeviceClient): The parent device client instance providing
                authenticated HTTPX connection pooling.
            target_id (Optional[str]): The multisensor target ID, if applicable.
        """
        self._client = client
        self._http = client._http_client
        self.target_id = target_id

    def _resolve_path(self) -> str:
        """
        Resolves the base API path based on the current isolated context.

        Returns:
            str: The resolved API endpoint path.
        """
        # CRITICAL FIX 1: History endpoints reside under /data/history, not /analysis/history
        if self.target_id:
            return f"/api/v5/multisensors/{self.target_id}/data/history"
        return "/api/v5/singlesensor/data/history"

    async def get_status(self) -> HistoryStatus:
        """
        Retrieves the diagnostic status of the historical data storage.

        Crucial for Autonomous Maintenance to monitor database capacity,
        retention times, and ensure the sensor has not stopped persisting data.

        Returns:
            HistoryStatus: Bridge model containing capacity metrics
                and stored data limits.
        """
        response = await self._http.get(f"{self._resolve_path()}/status")
        return HistoryStatus.model_validate(response.json())

    async def get_counts(
        self,
        start_time: XovisTime,
        end_time: XovisTime = "now",
        resolution: int = 0,
        logic_id: Optional[int] = None,
        time_format: TimeFormat = TimeFormat.RFC3339,  # RFC3339 avoids Pydantic ms/s OverflowErrors
        include_empty: bool = False,
    ) -> Any:
        """
        Retrieves historical logic counts for a specified time interval.

        Args:
            start_time (XovisTime): Begin of time interval (Unix ms or relative).
            end_time (XovisTime): End of time interval (Unix ms or relative).
                Defaults to "now".
            resolution (int): Aggregation resolution in minutes. Defaults to 0 (AUTO).
            logic_id (Optional[int]): If provided, filters the payload to only include
                data for a specific logic ID, significantly reducing network overhead.
            time_format (TimeFormat): The time format for the output data. Defaults to RFC3339.
            include_empty (bool): Whether to include empty bins where data is missing.

        Returns:
            HistoryLogics: Bridge model containing the time-series bins.
        """
        # We utilize the HistoryQuery model to validate and normalize parameters.
        # This ensures that XovisTime offsets (e.g., '-1h') are converted to Unix ms.
        query = HistoryQuery(
            begin=start_time,
            end=end_time,
            resolution_min=resolution,
            time_format=time_format,
            include_empty=include_empty,
        )

        endpoint = f"{self._resolve_path()}/logics"
        if logic_id is not None:
            endpoint += f"/{logic_id}"

        response = await self._http.get(endpoint, params=query)
        return self._client.models.HistoryLogics.model_validate(response.json())

    async def get_start_stop_points(self, start_time: XovisTime, end_time: XovisTime = "now", max_points: int = 1000) -> Any:
        """
        Retrieves the 3D coordinates where tracks first appeared and terminated.

        Essential for the Geometry Optimization Agent to mathematically detect
        lines placed too deep or zones missing track intersections.

        Args:
            start_time (XovisTime): Begin of time interval (Unix ms or relative).
            end_time (XovisTime): End of time interval (Unix ms or relative).
            max_points (int): Maximum number of coordinate points to return.

        Returns:
            StartStopPoints: Bridge model containing lists of Start and Stop
                3D coordinate vectors.
        """
        query = StartStopQuery(begin=start_time, end=end_time, max=max_points)
        response = await self._http.get(f"{self._resolve_path()}/start_stop", params=query)
        return StartStopPoints.model_validate(response.json())

    async def get_heat_map(self) -> HeatHeightMap:
        """
        Retrieves the spatial heat map data array.

        Provides the percentage of time each pixel in the tracking area is
        occupied by an object. Operates on an exponential moving average (24h).

        Returns:
            HeatHeightMap: Bridge model containing the 2D
                floating-point array and mapping metadata.
        """
        response = await self._http.get(f"{self._resolve_path()}/heat_map", params={"data": "true"})
        return HeatHeightMap.model_validate(response.json())

    async def get_height_map(self) -> HeatHeightMap:
        """
        Retrieves the spatial height map data array.

        Provides the average measured height of objects across the tracking area.
        Highly valuable for diagnosing miscalibrated sensor mounting heights.

        Returns:
            HeatHeightMap: Bridge model containing the 2D
                floating-point array and mapping metadata.
        """
        response = await self._http.get(f"{self._resolve_path()}/height_map", params={"data": "true"})
        return HeatHeightMap.model_validate(response.json())

    async def clear_sensor_db(self) -> None:
        """
        Irreversibly deletes all count records and offsets in the sensor database.

        CRITICAL: This resets all outputs for logic data. It should only be executed
        by an agent if explicitly authorized under the CRITICAL safety guardrails.

        Returns:
            None
        """
        await self._http.delete(f"{self._resolve_path()}/sensor_db")
Methods:
__init__(client, target_id=None)

Initializes the HistoryManager.

Parameters:

Name Type Description Default
client DeviceClient

The parent device client instance providing authenticated HTTPX connection pooling.

required
target_id Optional[str]

The multisensor target ID, if applicable.

None
Source code in src/xovis/api/device/resources/history.py
36
37
38
39
40
41
42
43
44
45
46
47
def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
    """
    Initializes the HistoryManager.

    Args:
        client (DeviceClient): The parent device client instance providing
            authenticated HTTPX connection pooling.
        target_id (Optional[str]): The multisensor target ID, if applicable.
    """
    self._client = client
    self._http = client._http_client
    self.target_id = target_id
_resolve_path()

Resolves the base API path based on the current isolated context.

Returns:

Name Type Description
str str

The resolved API endpoint path.

Source code in src/xovis/api/device/resources/history.py
49
50
51
52
53
54
55
56
57
58
59
def _resolve_path(self) -> str:
    """
    Resolves the base API path based on the current isolated context.

    Returns:
        str: The resolved API endpoint path.
    """
    # CRITICAL FIX 1: History endpoints reside under /data/history, not /analysis/history
    if self.target_id:
        return f"/api/v5/multisensors/{self.target_id}/data/history"
    return "/api/v5/singlesensor/data/history"
clear_sensor_db() async

Irreversibly deletes all count records and offsets in the sensor database.

CRITICAL: This resets all outputs for logic data. It should only be executed by an agent if explicitly authorized under the CRITICAL safety guardrails.

Returns:

Type Description
None

None

Source code in src/xovis/api/device/resources/history.py
165
166
167
168
169
170
171
172
173
174
175
async def clear_sensor_db(self) -> None:
    """
    Irreversibly deletes all count records and offsets in the sensor database.

    CRITICAL: This resets all outputs for logic data. It should only be executed
    by an agent if explicitly authorized under the CRITICAL safety guardrails.

    Returns:
        None
    """
    await self._http.delete(f"{self._resolve_path()}/sensor_db")
get_counts(start_time, end_time='now', resolution=0, logic_id=None, time_format=TimeFormat.RFC3339, include_empty=False) async

Retrieves historical logic counts for a specified time interval.

Parameters:

Name Type Description Default
start_time XovisTime

Begin of time interval (Unix ms or relative).

required
end_time XovisTime

End of time interval (Unix ms or relative). Defaults to "now".

'now'
resolution int

Aggregation resolution in minutes. Defaults to 0 (AUTO).

0
logic_id Optional[int]

If provided, filters the payload to only include data for a specific logic ID, significantly reducing network overhead.

None
time_format TimeFormat

The time format for the output data. Defaults to RFC3339.

RFC3339
include_empty bool

Whether to include empty bins where data is missing.

False

Returns:

Name Type Description
HistoryLogics Any

Bridge model containing the time-series bins.

Source code in src/xovis/api/device/resources/history.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
async def get_counts(
    self,
    start_time: XovisTime,
    end_time: XovisTime = "now",
    resolution: int = 0,
    logic_id: Optional[int] = None,
    time_format: TimeFormat = TimeFormat.RFC3339,  # RFC3339 avoids Pydantic ms/s OverflowErrors
    include_empty: bool = False,
) -> Any:
    """
    Retrieves historical logic counts for a specified time interval.

    Args:
        start_time (XovisTime): Begin of time interval (Unix ms or relative).
        end_time (XovisTime): End of time interval (Unix ms or relative).
            Defaults to "now".
        resolution (int): Aggregation resolution in minutes. Defaults to 0 (AUTO).
        logic_id (Optional[int]): If provided, filters the payload to only include
            data for a specific logic ID, significantly reducing network overhead.
        time_format (TimeFormat): The time format for the output data. Defaults to RFC3339.
        include_empty (bool): Whether to include empty bins where data is missing.

    Returns:
        HistoryLogics: Bridge model containing the time-series bins.
    """
    # We utilize the HistoryQuery model to validate and normalize parameters.
    # This ensures that XovisTime offsets (e.g., '-1h') are converted to Unix ms.
    query = HistoryQuery(
        begin=start_time,
        end=end_time,
        resolution_min=resolution,
        time_format=time_format,
        include_empty=include_empty,
    )

    endpoint = f"{self._resolve_path()}/logics"
    if logic_id is not None:
        endpoint += f"/{logic_id}"

    response = await self._http.get(endpoint, params=query)
    return self._client.models.HistoryLogics.model_validate(response.json())
get_heat_map() async

Retrieves the spatial heat map data array.

Provides the percentage of time each pixel in the tracking area is occupied by an object. Operates on an exponential moving average (24h).

Returns:

Name Type Description
HeatHeightMap HeatHeightMap

Bridge model containing the 2D floating-point array and mapping metadata.

Source code in src/xovis/api/device/resources/history.py
137
138
139
140
141
142
143
144
145
146
147
148
149
async def get_heat_map(self) -> HeatHeightMap:
    """
    Retrieves the spatial heat map data array.

    Provides the percentage of time each pixel in the tracking area is
    occupied by an object. Operates on an exponential moving average (24h).

    Returns:
        HeatHeightMap: Bridge model containing the 2D
            floating-point array and mapping metadata.
    """
    response = await self._http.get(f"{self._resolve_path()}/heat_map", params={"data": "true"})
    return HeatHeightMap.model_validate(response.json())
get_height_map() async

Retrieves the spatial height map data array.

Provides the average measured height of objects across the tracking area. Highly valuable for diagnosing miscalibrated sensor mounting heights.

Returns:

Name Type Description
HeatHeightMap HeatHeightMap

Bridge model containing the 2D floating-point array and mapping metadata.

Source code in src/xovis/api/device/resources/history.py
151
152
153
154
155
156
157
158
159
160
161
162
163
async def get_height_map(self) -> HeatHeightMap:
    """
    Retrieves the spatial height map data array.

    Provides the average measured height of objects across the tracking area.
    Highly valuable for diagnosing miscalibrated sensor mounting heights.

    Returns:
        HeatHeightMap: Bridge model containing the 2D
            floating-point array and mapping metadata.
    """
    response = await self._http.get(f"{self._resolve_path()}/height_map", params={"data": "true"})
    return HeatHeightMap.model_validate(response.json())
get_start_stop_points(start_time, end_time='now', max_points=1000) async

Retrieves the 3D coordinates where tracks first appeared and terminated.

Essential for the Geometry Optimization Agent to mathematically detect lines placed too deep or zones missing track intersections.

Parameters:

Name Type Description Default
start_time XovisTime

Begin of time interval (Unix ms or relative).

required
end_time XovisTime

End of time interval (Unix ms or relative).

'now'
max_points int

Maximum number of coordinate points to return.

1000

Returns:

Name Type Description
StartStopPoints Any

Bridge model containing lists of Start and Stop 3D coordinate vectors.

Source code in src/xovis/api/device/resources/history.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
async def get_start_stop_points(self, start_time: XovisTime, end_time: XovisTime = "now", max_points: int = 1000) -> Any:
    """
    Retrieves the 3D coordinates where tracks first appeared and terminated.

    Essential for the Geometry Optimization Agent to mathematically detect
    lines placed too deep or zones missing track intersections.

    Args:
        start_time (XovisTime): Begin of time interval (Unix ms or relative).
        end_time (XovisTime): End of time interval (Unix ms or relative).
        max_points (int): Maximum number of coordinate points to return.

    Returns:
        StartStopPoints: Bridge model containing lists of Start and Stop
            3D coordinate vectors.
    """
    query = StartStopQuery(begin=start_time, end=end_time, max=max_points)
    response = await self._http.get(f"{self._resolve_path()}/start_stop", params=query)
    return StartStopPoints.model_validate(response.json())
get_status() async

Retrieves the diagnostic status of the historical data storage.

Crucial for Autonomous Maintenance to monitor database capacity, retention times, and ensure the sensor has not stopped persisting data.

Returns:

Name Type Description
HistoryStatus HistoryStatus

Bridge model containing capacity metrics and stored data limits.

Source code in src/xovis/api/device/resources/history.py
61
62
63
64
65
66
67
68
69
70
71
72
73
async def get_status(self) -> HistoryStatus:
    """
    Retrieves the diagnostic status of the historical data storage.

    Crucial for Autonomous Maintenance to monitor database capacity,
    retention times, and ensure the sensor has not stopped persisting data.

    Returns:
        HistoryStatus: Bridge model containing capacity metrics
            and stored data limits.
    """
    response = await self._http.get(f"{self._resolve_path()}/status")
    return HistoryStatus.model_validate(response.json())

xovis.api.device.resources.scene

Xovis SDK - Scene Management Resource

Operates within the Control Plane. Provides the implementation for managing spatial constraints on local edge sensors, including scene geometries (lines/zones), occlusion masks, layers, objects, and attention areas. Integrates capacity limit queries to prevent autonomous agents from exceeding edge hardware vertex constraints.

Classes

SceneManager

Manages the physical and optical spatial context of a Xovis device.

Orchestrates the creation and modification of Geometries, Masks, Layers, Scene Objects, and Attentions. Exposes hardware vertex and element limits to ensure autonomous generation agents (Module C) remain within safe bounds.

Source code in src/xovis/api/device/resources/scene.py
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
class SceneManager:
    """
    Manages the physical and optical spatial context of a Xovis device.

    Orchestrates the creation and modification of Geometries, Masks, Layers,
    Scene Objects, and Attentions. Exposes hardware vertex and element limits
    to ensure autonomous generation agents (Module C) remain within safe bounds.
    """

    def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
        """
        Initializes the SceneManager.

        Args:
            client (DeviceClient): The parent device client instance.
            target_id (Optional[str]): The multisensor target ID, if applicable.
        """
        self._client = client
        self._http = client._http_client
        self.target_id = target_id

    @property
    def models(self) -> Any:
        """
        Returns the strictly validated Pydantic models for the current firmware.

        Returns:
            Any: The collection of auto-generated Pydantic V2 models
                synchronized with the current device firmware.
        """
        return self._client.models if self._client else stable_models

    def _resolve_path(self) -> str:
        """
        Resolves the base API path based on the current isolated context.

        Returns:
            str: The resolved API endpoint path.
        """
        if self.target_id:
            return f"/api/v5/multisensors/{self.target_id}/scene"
        return "/api/v5/singlesensor/scene"

    async def _resolve_resource_id(self, resource_type: str, id_or_name: Union[int, str]) -> int:
        """
        Generic resolver for translating human-readable names to integer IDs
        by inspecting the persistent HostStateBucket cache.

        Args:
            resource_type (str): The collection name in the cache (e.g., 'zones', 'masks').
            id_or_name (Union[int, str]): The ID or name to resolve.

        Returns:
            int: The resolved exact integer ID.

        Raises:
            ResourceNotFoundError: If the name is missing from the cache.
            MultipleResourcesFoundError: If the name is ambiguous.
        """
        if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
            return int(id_or_name)

        multisensors = self._client.cache.multisensors
        if self.target_id:
            target_str = str(self.target_id)
            if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
                await self._client.multisensors.sync()
            context = multisensors[target_str]
        else:
            context = self._client.cache.singlesensor

        # Geometries are split into lines and zones in the cache bucket
        if resource_type == "geometries":
            items = list(getattr(context, "zones", []) or []) + list(getattr(context, "lines", []) or [])
        else:
            items = list(getattr(context, resource_type, []) or [])

        # Proactive sync if name is not found
        if not any(getattr(item, "name", None) == id_or_name for item in items):
            if resource_type == "geometries":
                await self.get_all_geometries()
            elif resource_type == "masks":
                await self.get_all_masks()
            elif resource_type == "layers":
                await self.get_all_layers()
            elif resource_type == "scene_objects":
                await self.get_all_objects()
            elif resource_type == "attentions":
                await self.get_all_attentions()

            # Re-fetch items after sync
            if resource_type == "geometries":
                items = list(getattr(context, "zones", []) or []) + list(getattr(context, "lines", []) or [])
            else:
                items = list(getattr(context, resource_type, []) or [])

        matches = [item for item in items if getattr(item, "name", None) == id_or_name]

        if not matches:
            raise ResourceNotFoundError(f"No {resource_type} found with name '{id_or_name}'.")
        if len(matches) > 1:
            raise MultipleResourcesFoundError(f"Found {len(matches)} {resource_type} named '{id_or_name}'. Use integer ID.")
        return int(matches[0].id)

    # --- GEOMETRIES ---
    async def get_geometry_limits(self) -> "SceneGeometriesLimits":
        """Retrieves the maximum allowed geometries and total vertices for the device."""
        response = await self._http.get(f"{self._resolve_path()}/geometries/limits")
        return self.models.SceneGeometriesLimits.model_validate(response.json())

    async def get_all_geometries(self, layer_id: Optional[int] = None) -> "SceneGeometries":
        """
        Retrieves all active lines and zones, optionally filtered by layer.

        Args:
            layer_id (Optional[int]): If provided, only returns geometries
                associated with this specific layer ID.

        Returns:
            SceneGeometries: A collection of all matching lines and zones.
        """
        params = {"layer_id": str(layer_id)} if layer_id is not None else {}
        response = await self._http.get(f"{self._resolve_path()}/geometries", params=params)
        return self.models.SceneGeometries.model_validate(response.json())

    async def get_geometry(self, id_or_name: Union[int, str]) -> "SceneGeometry":
        """
        Retrieves a specific scene geometry configuration.

        Args:
            id_or_name (Union[int, str]): The ID or logical name of the geometry.

        Returns:
            SceneGeometry: The validated scene geometry configuration.
        """
        geom_id = await self._resolve_resource_id("geometries", id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/geometries/{geom_id}")
        return self.models.SceneGeometry.model_validate(response.json())

    async def create_geometry(self, geometry: "SceneGeometry", id_mode: str = "SERVER") -> "SceneGeometry":
        """Provisions a new physical tracking geometry (line or polygon)."""
        params = {"id_mode": id_mode}
        response = await self._http.post(f"{self._resolve_path()}/geometries", params=params, json=geometry)
        return self.models.SceneGeometry.model_validate(response.json())

    async def update_geometry(self, id_or_name: Union[int, str], geometry: "SceneGeometry") -> "SceneGeometry":
        """
        Replaces an existing scene geometry.

        Args:
            id_or_name (Union[int, str]): The ID or name of the target geometry.
            geometry (SceneGeometry): The full updated geometry configuration.

        Returns:
            SceneGeometry: The successfully updated geometry.
        """
        geom_id = await self._resolve_resource_id("geometries", id_or_name)
        response = await self._http.put(f"{self._resolve_path()}/geometries/{geom_id}", json=geometry)
        return self.models.SceneGeometry.model_validate(response.json())

    async def delete_geometry(self, id_or_name: Union[int, str]) -> None:
        """
        Removes a specific scene geometry.

        Args:
            id_or_name (Union[int, str]): The ID or name of the geometry to delete.
        """
        geom_id = await self._resolve_resource_id("geometries", id_or_name)
        await self._http.delete(f"{self._resolve_path()}/geometries/{geom_id}")

    async def delete_all_geometries(self) -> None:
        """Destructively removes all scene geometries."""
        await self._http.delete(f"{self._resolve_path()}/geometries")

    # --- MASKS ---
    async def get_mask_limits(self) -> "SceneMasksLimits":
        """Retrieves the maximum allowed masks and vertices for the device."""
        response = await self._http.get(f"{self._resolve_path()}/masks/limits")
        return self.models.SceneMasksLimits.model_validate(response.json())

    async def get_all_masks(self) -> "AllSceneMasks":
        """
        Retrieves all exclusion and illumination masks.

        Returns:
            AllSceneMasks: A collection of all configured masks.
        """
        response = await self._http.get(f"{self._resolve_path()}/masks")
        return self.models.AllSceneMasks.model_validate(response.json())

    async def get_mask(self, id_or_name: Union[int, str]) -> "SceneMask":
        """
        Retrieves a specific exclusion mask.

        Args:
            id_or_name (Union[int, str]): The ID or name of the target mask.

        Returns:
            SceneMask: The validated mask configuration.
        """
        mask_id = await self._resolve_resource_id("masks", id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/masks/{mask_id}")
        return self.models.SceneMask.model_validate(response.json())

    async def create_mask(self, mask: "SceneMask", id_mode: str = "SERVER") -> "SceneMask":
        """Creates a new exclusion or illumination mask."""
        params = {"id_mode": id_mode}
        payload = _recursive_none_filter(mask.model_dump(mode="json", by_alias=True, exclude_unset=True))
        response = await self._http.post(f"{self._resolve_path()}/masks", params=params, json=payload)
        return self.models.SceneMask.model_validate(response.json())

    async def update_mask(self, id_or_name: Union[int, str], mask: "SceneMask") -> "SceneMask":
        """
        Updates an existing mask.

        Args:
            id_or_name (Union[int, str]): The ID or name of the target mask.
            mask (SceneMask): The updated mask configuration.

        Returns:
            SceneMask: The successfully updated mask.
        """
        mask_id = await self._resolve_resource_id("masks", id_or_name)
        payload = _recursive_none_filter(mask.model_dump(mode="json", by_alias=True, exclude_unset=True))
        response = await self._http.put(f"{self._resolve_path()}/masks/{mask_id}", json=payload)
        return self.models.SceneMask.model_validate(response.json())

    async def delete_mask(self, id_or_name: Union[int, str]) -> None:
        """
        Deletes a specific exclusion mask.

        Args:
            id_or_name (Union[int, str]): The ID or name of the mask to delete.
        """
        mask_id = await self._resolve_resource_id("masks", id_or_name)
        await self._http.delete(f"{self._resolve_path()}/masks/{mask_id}")

    async def delete_all_masks(self) -> None:
        """Destructively removes all scene masks."""
        await self._http.delete(f"{self._resolve_path()}/masks")

    # --- LAYERS ---
    async def get_layer_limits(self) -> "LayersLimits":
        """Retrieves the maximum allowed layers and constraints for the device."""
        response = await self._http.get(f"{self._resolve_path()}/layers/limits")
        return self.models.LayersLimits.model_validate(response.json())

    async def get_all_layers(self) -> "Layers":
        """
        Retrieves all scene layers (virtual sub-contexts).

        Returns:
            Layers: A collection of all defined layers.
        """
        response = await self._http.get(f"{self._resolve_path()}/layers")
        return self.models.Layers.model_validate(response.json())

    async def get_layer(self, id_or_name: Union[int, str]) -> "Layer":
        """
        Retrieves a specific scene layer.

        Args:
            id_or_name (Union[int, str]): The ID or name of the target layer.

        Returns:
            Layer: The validated layer configuration.
        """
        layer_id = await self._resolve_resource_id("layers", id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/layers/{layer_id}")
        return self.models.Layer.model_validate(response.json())

    async def create_layer(self, layer: "Layer", id_mode: str = "SERVER") -> "Layer":
        """Provisions a new logical scene layer."""
        params = {"id_mode": id_mode}
        payload = _recursive_none_filter(layer.model_dump(mode="json", by_alias=True, exclude_unset=True))
        response = await self._http.post(f"{self._resolve_path()}/layers", params=params, json=payload)
        return self.models.Layer.model_validate(response.json())

    async def update_layer(self, id_or_name: Union[int, str], layer: "Layer") -> "Layer":
        """
        Updates an existing scene layer.

        Args:
            id_or_name (Union[int, str]): The ID or name of the target layer.
            layer (Layer): The updated layer configuration.

        Returns:
            Layer: The successfully updated layer.
        """
        layer_id = await self._resolve_resource_id("layers", id_or_name)
        payload = _recursive_none_filter(layer.model_dump(mode="json", by_alias=True, exclude_unset=True))
        response = await self._http.put(f"{self._resolve_path()}/layers/{layer_id}", json=payload)
        return self.models.Layer.model_validate(response.json())

    async def delete_layer(self, id_or_name: Union[int, str]) -> None:
        """
        Deletes a specific scene layer.

        Args:
            id_or_name (Union[int, str]): The ID or name of the layer to delete.
        """
        layer_id = await self._resolve_resource_id("layers", id_or_name)
        await self._http.delete(f"{self._resolve_path()}/layers/{layer_id}")

    async def delete_all_layers(self) -> None:
        """Destructively removes all scene layers."""
        await self._http.delete(f"{self._resolve_path()}/layers")

    # --- SCENE OBJECTS ---
    async def get_object_limits(self) -> "SceneObjectsLimits":
        """Retrieves the maximum allowed scene objects and vertices for the device."""
        response = await self._http.get(f"{self._resolve_path()}/objects/limits")
        return self.models.SceneObjectsLimits.model_validate(response.json())

    async def get_all_objects(self) -> "SceneObjects":
        """
        Retrieves all physical Scene Objects (e.g., tables, barriers).

        Returns:
            SceneObjects: A collection of all configured scene objects.
        """
        response = await self._http.get(f"{self._resolve_path()}/objects")
        return self.models.SceneObjects.model_validate(response.json())

    async def get_object(self, id_or_name: Union[int, str]) -> "SceneObject":
        """
        Retrieves a specific Scene Object.

        Args:
            id_or_name (Union[int, str]): The ID or name of the target object.

        Returns:
            SceneObject: The validated scene object configuration.
        """
        obj_id = await self._resolve_resource_id("scene_objects", id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/objects/{obj_id}")
        return self.models.SceneObject.model_validate(response.json())

    async def create_object(self, scene_object: "SceneObject", id_mode: str = "SERVER") -> "SceneObject":
        """Creates a new Scene Object for physical environment tracking."""
        params = {"id_mode": id_mode}
        payload = _recursive_none_filter(scene_object.model_dump(mode="json", by_alias=True, exclude_unset=True))
        response = await self._http.post(f"{self._resolve_path()}/objects", params=params, json=payload)
        return self.models.SceneObject.model_validate(response.json())

    async def update_object(self, id_or_name: Union[int, str], scene_object: "SceneObject") -> "SceneObject":
        """
        Updates an existing Scene Object.

        Args:
            id_or_name (Union[int, str]): The ID or name of the target object.
            scene_object (SceneObject): The updated object configuration.

        Returns:
            SceneObject: The successfully updated object.
        """
        obj_id = await self._resolve_resource_id("scene_objects", id_or_name)
        payload = _recursive_none_filter(scene_object.model_dump(mode="json", by_alias=True, exclude_unset=True))
        response = await self._http.put(f"{self._resolve_path()}/objects/{obj_id}", json=payload)
        return self.models.SceneObject.model_validate(response.json())

    async def delete_object(self, id_or_name: Union[int, str]) -> None:
        """
        Deletes a specific Scene Object.

        Args:
            id_or_name (Union[int, str]): The ID or name of the object to delete.
        """
        obj_id = await self._resolve_resource_id("scene_objects", id_or_name)
        await self._http.delete(f"{self._resolve_path()}/objects/{obj_id}")

    async def delete_all_objects(self) -> None:
        """Destructively removes all Scene Objects."""
        await self._http.delete(f"{self._resolve_path()}/objects")

    # --- ATTENTIONS ---
    async def get_attention_limits(self) -> "AttentionsLimits":
        """Retrieves the maximum allowed Attention areas for the device."""
        response = await self._http.get(f"{self._resolve_path()}/attentions/limits")
        return self.models.AttentionsLimits.model_validate(response.json())

    async def get_all_attentions(self, layer_id: Optional[int] = None) -> "Attentions":
        """
        Retrieves all configured Attention areas, optionally filtered by layer.

        Args:
            layer_id (Optional[int]): Filter by a specific layer ID.

        Returns:
            Attentions: A collection of matching attention area configurations.
        """
        params = {"layer_id": str(layer_id)} if layer_id is not None else {}
        response = await self._http.get(f"{self._resolve_path()}/attentions", params=params)
        return self.models.Attentions.model_validate(response.json())

    async def get_attention(self, id_or_name: Union[int, str]) -> "Attention":
        """
        Retrieves a specific Attention area.

        Args:
            id_or_name (Union[int, str]): The ID or name of the target area.

        Returns:
            Attention: The validated attention area configuration.
        """
        attention_id = await self._resolve_resource_id("attentions", id_or_name)
        response = await self._http.get(f"{self._resolve_path()}/attentions/{attention_id}")
        return self.models.Attention.model_validate(response.json())

    async def create_attention(self, attention: "Attention", id_mode: str = "SERVER") -> "Attention":
        """Creates a new Attention area for measuring subject engagement."""
        params = {"id_mode": id_mode}
        payload = _recursive_none_filter(attention.model_dump(mode="json", by_alias=True, exclude_unset=True))
        response = await self._http.post(f"{self._resolve_path()}/attentions", params=params, json=payload)
        return self.models.Attention.model_validate(response.json())

    async def update_attention(self, id_or_name: Union[int, str], attention: "Attention") -> "Attention":
        """
        Updates an existing Attention area.

        Args:
            id_or_name (Union[int, str]): The ID or name of the target area.
            attention (Attention): The updated attention area configuration.

        Returns:
            Attention: The successfully updated attention area.
        """
        attention_id = await self._resolve_resource_id("attentions", id_or_name)
        payload = _recursive_none_filter(attention.model_dump(mode="json", by_alias=True, exclude_unset=True))
        response = await self._http.put(f"{self._resolve_path()}/attentions/{attention_id}", json=payload)
        return self.models.Attention.model_validate(response.json())

    async def delete_attention(self, id_or_name: Union[int, str]) -> None:
        """
        Deletes a specific Attention area.

        Args:
            id_or_name (Union[int, str]): The ID or name of the area to delete.
        """
        attention_id = await self._resolve_resource_id("attentions", id_or_name)
        await self._http.delete(f"{self._resolve_path()}/attentions/{attention_id}")

    async def delete_all_attentions(self) -> None:
        """Destructively removes all configured Attention areas."""
        await self._http.delete(f"{self._resolve_path()}/attentions")
Attributes
models property

Returns the strictly validated Pydantic models for the current firmware.

Returns:

Name Type Description
Any Any

The collection of auto-generated Pydantic V2 models synchronized with the current device firmware.

Methods:
__init__(client, target_id=None)

Initializes the SceneManager.

Parameters:

Name Type Description Default
client DeviceClient

The parent device client instance.

required
target_id Optional[str]

The multisensor target ID, if applicable.

None
Source code in src/xovis/api/device/resources/scene.py
55
56
57
58
59
60
61
62
63
64
65
def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
    """
    Initializes the SceneManager.

    Args:
        client (DeviceClient): The parent device client instance.
        target_id (Optional[str]): The multisensor target ID, if applicable.
    """
    self._client = client
    self._http = client._http_client
    self.target_id = target_id
_resolve_path()

Resolves the base API path based on the current isolated context.

Returns:

Name Type Description
str str

The resolved API endpoint path.

Source code in src/xovis/api/device/resources/scene.py
78
79
80
81
82
83
84
85
86
87
def _resolve_path(self) -> str:
    """
    Resolves the base API path based on the current isolated context.

    Returns:
        str: The resolved API endpoint path.
    """
    if self.target_id:
        return f"/api/v5/multisensors/{self.target_id}/scene"
    return "/api/v5/singlesensor/scene"
_resolve_resource_id(resource_type, id_or_name) async

Generic resolver for translating human-readable names to integer IDs by inspecting the persistent HostStateBucket cache.

Parameters:

Name Type Description Default
resource_type str

The collection name in the cache (e.g., 'zones', 'masks').

required
id_or_name Union[int, str]

The ID or name to resolve.

required

Returns:

Name Type Description
int int

The resolved exact integer ID.

Raises:

Type Description
ResourceNotFoundError

If the name is missing from the cache.

MultipleResourcesFoundError

If the name is ambiguous.

Source code in src/xovis/api/device/resources/scene.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
async def _resolve_resource_id(self, resource_type: str, id_or_name: Union[int, str]) -> int:
    """
    Generic resolver for translating human-readable names to integer IDs
    by inspecting the persistent HostStateBucket cache.

    Args:
        resource_type (str): The collection name in the cache (e.g., 'zones', 'masks').
        id_or_name (Union[int, str]): The ID or name to resolve.

    Returns:
        int: The resolved exact integer ID.

    Raises:
        ResourceNotFoundError: If the name is missing from the cache.
        MultipleResourcesFoundError: If the name is ambiguous.
    """
    if isinstance(id_or_name, int) or (isinstance(id_or_name, str) and id_or_name.isdigit()):
        return int(id_or_name)

    multisensors = self._client.cache.multisensors
    if self.target_id:
        target_str = str(self.target_id)
        if target_str not in (multisensors._items if hasattr(multisensors, "_items") else multisensors):
            await self._client.multisensors.sync()
        context = multisensors[target_str]
    else:
        context = self._client.cache.singlesensor

    # Geometries are split into lines and zones in the cache bucket
    if resource_type == "geometries":
        items = list(getattr(context, "zones", []) or []) + list(getattr(context, "lines", []) or [])
    else:
        items = list(getattr(context, resource_type, []) or [])

    # Proactive sync if name is not found
    if not any(getattr(item, "name", None) == id_or_name for item in items):
        if resource_type == "geometries":
            await self.get_all_geometries()
        elif resource_type == "masks":
            await self.get_all_masks()
        elif resource_type == "layers":
            await self.get_all_layers()
        elif resource_type == "scene_objects":
            await self.get_all_objects()
        elif resource_type == "attentions":
            await self.get_all_attentions()

        # Re-fetch items after sync
        if resource_type == "geometries":
            items = list(getattr(context, "zones", []) or []) + list(getattr(context, "lines", []) or [])
        else:
            items = list(getattr(context, resource_type, []) or [])

    matches = [item for item in items if getattr(item, "name", None) == id_or_name]

    if not matches:
        raise ResourceNotFoundError(f"No {resource_type} found with name '{id_or_name}'.")
    if len(matches) > 1:
        raise MultipleResourcesFoundError(f"Found {len(matches)} {resource_type} named '{id_or_name}'. Use integer ID.")
    return int(matches[0].id)
create_attention(attention, id_mode='SERVER') async

Creates a new Attention area for measuring subject engagement.

Source code in src/xovis/api/device/resources/scene.py
455
456
457
458
459
460
async def create_attention(self, attention: "Attention", id_mode: str = "SERVER") -> "Attention":
    """Creates a new Attention area for measuring subject engagement."""
    params = {"id_mode": id_mode}
    payload = _recursive_none_filter(attention.model_dump(mode="json", by_alias=True, exclude_unset=True))
    response = await self._http.post(f"{self._resolve_path()}/attentions", params=params, json=payload)
    return self.models.Attention.model_validate(response.json())
create_geometry(geometry, id_mode='SERVER') async

Provisions a new physical tracking geometry (line or polygon).

Source code in src/xovis/api/device/resources/scene.py
185
186
187
188
189
async def create_geometry(self, geometry: "SceneGeometry", id_mode: str = "SERVER") -> "SceneGeometry":
    """Provisions a new physical tracking geometry (line or polygon)."""
    params = {"id_mode": id_mode}
    response = await self._http.post(f"{self._resolve_path()}/geometries", params=params, json=geometry)
    return self.models.SceneGeometry.model_validate(response.json())
create_layer(layer, id_mode='SERVER') async

Provisions a new logical scene layer.

Source code in src/xovis/api/device/resources/scene.py
317
318
319
320
321
322
async def create_layer(self, layer: "Layer", id_mode: str = "SERVER") -> "Layer":
    """Provisions a new logical scene layer."""
    params = {"id_mode": id_mode}
    payload = _recursive_none_filter(layer.model_dump(mode="json", by_alias=True, exclude_unset=True))
    response = await self._http.post(f"{self._resolve_path()}/layers", params=params, json=payload)
    return self.models.Layer.model_validate(response.json())
create_mask(mask, id_mode='SERVER') async

Creates a new exclusion or illumination mask.

Source code in src/xovis/api/device/resources/scene.py
250
251
252
253
254
255
async def create_mask(self, mask: "SceneMask", id_mode: str = "SERVER") -> "SceneMask":
    """Creates a new exclusion or illumination mask."""
    params = {"id_mode": id_mode}
    payload = _recursive_none_filter(mask.model_dump(mode="json", by_alias=True, exclude_unset=True))
    response = await self._http.post(f"{self._resolve_path()}/masks", params=params, json=payload)
    return self.models.SceneMask.model_validate(response.json())
create_object(scene_object, id_mode='SERVER') async

Creates a new Scene Object for physical environment tracking.

Source code in src/xovis/api/device/resources/scene.py
384
385
386
387
388
389
async def create_object(self, scene_object: "SceneObject", id_mode: str = "SERVER") -> "SceneObject":
    """Creates a new Scene Object for physical environment tracking."""
    params = {"id_mode": id_mode}
    payload = _recursive_none_filter(scene_object.model_dump(mode="json", by_alias=True, exclude_unset=True))
    response = await self._http.post(f"{self._resolve_path()}/objects", params=params, json=payload)
    return self.models.SceneObject.model_validate(response.json())
delete_all_attentions() async

Destructively removes all configured Attention areas.

Source code in src/xovis/api/device/resources/scene.py
488
489
490
async def delete_all_attentions(self) -> None:
    """Destructively removes all configured Attention areas."""
    await self._http.delete(f"{self._resolve_path()}/attentions")
delete_all_geometries() async

Destructively removes all scene geometries.

Source code in src/xovis/api/device/resources/scene.py
216
217
218
async def delete_all_geometries(self) -> None:
    """Destructively removes all scene geometries."""
    await self._http.delete(f"{self._resolve_path()}/geometries")
delete_all_layers() async

Destructively removes all scene layers.

Source code in src/xovis/api/device/resources/scene.py
350
351
352
async def delete_all_layers(self) -> None:
    """Destructively removes all scene layers."""
    await self._http.delete(f"{self._resolve_path()}/layers")
delete_all_masks() async

Destructively removes all scene masks.

Source code in src/xovis/api/device/resources/scene.py
283
284
285
async def delete_all_masks(self) -> None:
    """Destructively removes all scene masks."""
    await self._http.delete(f"{self._resolve_path()}/masks")
delete_all_objects() async

Destructively removes all Scene Objects.

Source code in src/xovis/api/device/resources/scene.py
417
418
419
async def delete_all_objects(self) -> None:
    """Destructively removes all Scene Objects."""
    await self._http.delete(f"{self._resolve_path()}/objects")
delete_attention(id_or_name) async

Deletes a specific Attention area.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the area to delete.

required
Source code in src/xovis/api/device/resources/scene.py
478
479
480
481
482
483
484
485
486
async def delete_attention(self, id_or_name: Union[int, str]) -> None:
    """
    Deletes a specific Attention area.

    Args:
        id_or_name (Union[int, str]): The ID or name of the area to delete.
    """
    attention_id = await self._resolve_resource_id("attentions", id_or_name)
    await self._http.delete(f"{self._resolve_path()}/attentions/{attention_id}")
delete_geometry(id_or_name) async

Removes a specific scene geometry.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the geometry to delete.

required
Source code in src/xovis/api/device/resources/scene.py
206
207
208
209
210
211
212
213
214
async def delete_geometry(self, id_or_name: Union[int, str]) -> None:
    """
    Removes a specific scene geometry.

    Args:
        id_or_name (Union[int, str]): The ID or name of the geometry to delete.
    """
    geom_id = await self._resolve_resource_id("geometries", id_or_name)
    await self._http.delete(f"{self._resolve_path()}/geometries/{geom_id}")
delete_layer(id_or_name) async

Deletes a specific scene layer.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the layer to delete.

required
Source code in src/xovis/api/device/resources/scene.py
340
341
342
343
344
345
346
347
348
async def delete_layer(self, id_or_name: Union[int, str]) -> None:
    """
    Deletes a specific scene layer.

    Args:
        id_or_name (Union[int, str]): The ID or name of the layer to delete.
    """
    layer_id = await self._resolve_resource_id("layers", id_or_name)
    await self._http.delete(f"{self._resolve_path()}/layers/{layer_id}")
delete_mask(id_or_name) async

Deletes a specific exclusion mask.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the mask to delete.

required
Source code in src/xovis/api/device/resources/scene.py
273
274
275
276
277
278
279
280
281
async def delete_mask(self, id_or_name: Union[int, str]) -> None:
    """
    Deletes a specific exclusion mask.

    Args:
        id_or_name (Union[int, str]): The ID or name of the mask to delete.
    """
    mask_id = await self._resolve_resource_id("masks", id_or_name)
    await self._http.delete(f"{self._resolve_path()}/masks/{mask_id}")
delete_object(id_or_name) async

Deletes a specific Scene Object.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the object to delete.

required
Source code in src/xovis/api/device/resources/scene.py
407
408
409
410
411
412
413
414
415
async def delete_object(self, id_or_name: Union[int, str]) -> None:
    """
    Deletes a specific Scene Object.

    Args:
        id_or_name (Union[int, str]): The ID or name of the object to delete.
    """
    obj_id = await self._resolve_resource_id("scene_objects", id_or_name)
    await self._http.delete(f"{self._resolve_path()}/objects/{obj_id}")
get_all_attentions(layer_id=None) async

Retrieves all configured Attention areas, optionally filtered by layer.

Parameters:

Name Type Description Default
layer_id Optional[int]

Filter by a specific layer ID.

None

Returns:

Name Type Description
Attentions Attentions

A collection of matching attention area configurations.

Source code in src/xovis/api/device/resources/scene.py
427
428
429
430
431
432
433
434
435
436
437
438
439
async def get_all_attentions(self, layer_id: Optional[int] = None) -> "Attentions":
    """
    Retrieves all configured Attention areas, optionally filtered by layer.

    Args:
        layer_id (Optional[int]): Filter by a specific layer ID.

    Returns:
        Attentions: A collection of matching attention area configurations.
    """
    params = {"layer_id": str(layer_id)} if layer_id is not None else {}
    response = await self._http.get(f"{self._resolve_path()}/attentions", params=params)
    return self.models.Attentions.model_validate(response.json())
get_all_geometries(layer_id=None) async

Retrieves all active lines and zones, optionally filtered by layer.

Parameters:

Name Type Description Default
layer_id Optional[int]

If provided, only returns geometries associated with this specific layer ID.

None

Returns:

Name Type Description
SceneGeometries SceneGeometries

A collection of all matching lines and zones.

Source code in src/xovis/api/device/resources/scene.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
async def get_all_geometries(self, layer_id: Optional[int] = None) -> "SceneGeometries":
    """
    Retrieves all active lines and zones, optionally filtered by layer.

    Args:
        layer_id (Optional[int]): If provided, only returns geometries
            associated with this specific layer ID.

    Returns:
        SceneGeometries: A collection of all matching lines and zones.
    """
    params = {"layer_id": str(layer_id)} if layer_id is not None else {}
    response = await self._http.get(f"{self._resolve_path()}/geometries", params=params)
    return self.models.SceneGeometries.model_validate(response.json())
get_all_layers() async

Retrieves all scene layers (virtual sub-contexts).

Returns:

Name Type Description
Layers Layers

A collection of all defined layers.

Source code in src/xovis/api/device/resources/scene.py
293
294
295
296
297
298
299
300
301
async def get_all_layers(self) -> "Layers":
    """
    Retrieves all scene layers (virtual sub-contexts).

    Returns:
        Layers: A collection of all defined layers.
    """
    response = await self._http.get(f"{self._resolve_path()}/layers")
    return self.models.Layers.model_validate(response.json())
get_all_masks() async

Retrieves all exclusion and illumination masks.

Returns:

Name Type Description
AllSceneMasks AllSceneMasks

A collection of all configured masks.

Source code in src/xovis/api/device/resources/scene.py
226
227
228
229
230
231
232
233
234
async def get_all_masks(self) -> "AllSceneMasks":
    """
    Retrieves all exclusion and illumination masks.

    Returns:
        AllSceneMasks: A collection of all configured masks.
    """
    response = await self._http.get(f"{self._resolve_path()}/masks")
    return self.models.AllSceneMasks.model_validate(response.json())
get_all_objects() async

Retrieves all physical Scene Objects (e.g., tables, barriers).

Returns:

Name Type Description
SceneObjects SceneObjects

A collection of all configured scene objects.

Source code in src/xovis/api/device/resources/scene.py
360
361
362
363
364
365
366
367
368
async def get_all_objects(self) -> "SceneObjects":
    """
    Retrieves all physical Scene Objects (e.g., tables, barriers).

    Returns:
        SceneObjects: A collection of all configured scene objects.
    """
    response = await self._http.get(f"{self._resolve_path()}/objects")
    return self.models.SceneObjects.model_validate(response.json())
get_attention(id_or_name) async

Retrieves a specific Attention area.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the target area.

required

Returns:

Name Type Description
Attention Attention

The validated attention area configuration.

Source code in src/xovis/api/device/resources/scene.py
441
442
443
444
445
446
447
448
449
450
451
452
453
async def get_attention(self, id_or_name: Union[int, str]) -> "Attention":
    """
    Retrieves a specific Attention area.

    Args:
        id_or_name (Union[int, str]): The ID or name of the target area.

    Returns:
        Attention: The validated attention area configuration.
    """
    attention_id = await self._resolve_resource_id("attentions", id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/attentions/{attention_id}")
    return self.models.Attention.model_validate(response.json())
get_attention_limits() async

Retrieves the maximum allowed Attention areas for the device.

Source code in src/xovis/api/device/resources/scene.py
422
423
424
425
async def get_attention_limits(self) -> "AttentionsLimits":
    """Retrieves the maximum allowed Attention areas for the device."""
    response = await self._http.get(f"{self._resolve_path()}/attentions/limits")
    return self.models.AttentionsLimits.model_validate(response.json())
get_geometry(id_or_name) async

Retrieves a specific scene geometry configuration.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or logical name of the geometry.

required

Returns:

Name Type Description
SceneGeometry SceneGeometry

The validated scene geometry configuration.

Source code in src/xovis/api/device/resources/scene.py
171
172
173
174
175
176
177
178
179
180
181
182
183
async def get_geometry(self, id_or_name: Union[int, str]) -> "SceneGeometry":
    """
    Retrieves a specific scene geometry configuration.

    Args:
        id_or_name (Union[int, str]): The ID or logical name of the geometry.

    Returns:
        SceneGeometry: The validated scene geometry configuration.
    """
    geom_id = await self._resolve_resource_id("geometries", id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/geometries/{geom_id}")
    return self.models.SceneGeometry.model_validate(response.json())
get_geometry_limits() async

Retrieves the maximum allowed geometries and total vertices for the device.

Source code in src/xovis/api/device/resources/scene.py
151
152
153
154
async def get_geometry_limits(self) -> "SceneGeometriesLimits":
    """Retrieves the maximum allowed geometries and total vertices for the device."""
    response = await self._http.get(f"{self._resolve_path()}/geometries/limits")
    return self.models.SceneGeometriesLimits.model_validate(response.json())
get_layer(id_or_name) async

Retrieves a specific scene layer.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the target layer.

required

Returns:

Name Type Description
Layer Layer

The validated layer configuration.

Source code in src/xovis/api/device/resources/scene.py
303
304
305
306
307
308
309
310
311
312
313
314
315
async def get_layer(self, id_or_name: Union[int, str]) -> "Layer":
    """
    Retrieves a specific scene layer.

    Args:
        id_or_name (Union[int, str]): The ID or name of the target layer.

    Returns:
        Layer: The validated layer configuration.
    """
    layer_id = await self._resolve_resource_id("layers", id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/layers/{layer_id}")
    return self.models.Layer.model_validate(response.json())
get_layer_limits() async

Retrieves the maximum allowed layers and constraints for the device.

Source code in src/xovis/api/device/resources/scene.py
288
289
290
291
async def get_layer_limits(self) -> "LayersLimits":
    """Retrieves the maximum allowed layers and constraints for the device."""
    response = await self._http.get(f"{self._resolve_path()}/layers/limits")
    return self.models.LayersLimits.model_validate(response.json())
get_mask(id_or_name) async

Retrieves a specific exclusion mask.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the target mask.

required

Returns:

Name Type Description
SceneMask SceneMask

The validated mask configuration.

Source code in src/xovis/api/device/resources/scene.py
236
237
238
239
240
241
242
243
244
245
246
247
248
async def get_mask(self, id_or_name: Union[int, str]) -> "SceneMask":
    """
    Retrieves a specific exclusion mask.

    Args:
        id_or_name (Union[int, str]): The ID or name of the target mask.

    Returns:
        SceneMask: The validated mask configuration.
    """
    mask_id = await self._resolve_resource_id("masks", id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/masks/{mask_id}")
    return self.models.SceneMask.model_validate(response.json())
get_mask_limits() async

Retrieves the maximum allowed masks and vertices for the device.

Source code in src/xovis/api/device/resources/scene.py
221
222
223
224
async def get_mask_limits(self) -> "SceneMasksLimits":
    """Retrieves the maximum allowed masks and vertices for the device."""
    response = await self._http.get(f"{self._resolve_path()}/masks/limits")
    return self.models.SceneMasksLimits.model_validate(response.json())
get_object(id_or_name) async

Retrieves a specific Scene Object.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the target object.

required

Returns:

Name Type Description
SceneObject SceneObject

The validated scene object configuration.

Source code in src/xovis/api/device/resources/scene.py
370
371
372
373
374
375
376
377
378
379
380
381
382
async def get_object(self, id_or_name: Union[int, str]) -> "SceneObject":
    """
    Retrieves a specific Scene Object.

    Args:
        id_or_name (Union[int, str]): The ID or name of the target object.

    Returns:
        SceneObject: The validated scene object configuration.
    """
    obj_id = await self._resolve_resource_id("scene_objects", id_or_name)
    response = await self._http.get(f"{self._resolve_path()}/objects/{obj_id}")
    return self.models.SceneObject.model_validate(response.json())
get_object_limits() async

Retrieves the maximum allowed scene objects and vertices for the device.

Source code in src/xovis/api/device/resources/scene.py
355
356
357
358
async def get_object_limits(self) -> "SceneObjectsLimits":
    """Retrieves the maximum allowed scene objects and vertices for the device."""
    response = await self._http.get(f"{self._resolve_path()}/objects/limits")
    return self.models.SceneObjectsLimits.model_validate(response.json())
update_attention(id_or_name, attention) async

Updates an existing Attention area.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the target area.

required
attention Attention

The updated attention area configuration.

required

Returns:

Name Type Description
Attention Attention

The successfully updated attention area.

Source code in src/xovis/api/device/resources/scene.py
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
async def update_attention(self, id_or_name: Union[int, str], attention: "Attention") -> "Attention":
    """
    Updates an existing Attention area.

    Args:
        id_or_name (Union[int, str]): The ID or name of the target area.
        attention (Attention): The updated attention area configuration.

    Returns:
        Attention: The successfully updated attention area.
    """
    attention_id = await self._resolve_resource_id("attentions", id_or_name)
    payload = _recursive_none_filter(attention.model_dump(mode="json", by_alias=True, exclude_unset=True))
    response = await self._http.put(f"{self._resolve_path()}/attentions/{attention_id}", json=payload)
    return self.models.Attention.model_validate(response.json())
update_geometry(id_or_name, geometry) async

Replaces an existing scene geometry.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the target geometry.

required
geometry SceneGeometry

The full updated geometry configuration.

required

Returns:

Name Type Description
SceneGeometry SceneGeometry

The successfully updated geometry.

Source code in src/xovis/api/device/resources/scene.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
async def update_geometry(self, id_or_name: Union[int, str], geometry: "SceneGeometry") -> "SceneGeometry":
    """
    Replaces an existing scene geometry.

    Args:
        id_or_name (Union[int, str]): The ID or name of the target geometry.
        geometry (SceneGeometry): The full updated geometry configuration.

    Returns:
        SceneGeometry: The successfully updated geometry.
    """
    geom_id = await self._resolve_resource_id("geometries", id_or_name)
    response = await self._http.put(f"{self._resolve_path()}/geometries/{geom_id}", json=geometry)
    return self.models.SceneGeometry.model_validate(response.json())
update_layer(id_or_name, layer) async

Updates an existing scene layer.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the target layer.

required
layer Layer

The updated layer configuration.

required

Returns:

Name Type Description
Layer Layer

The successfully updated layer.

Source code in src/xovis/api/device/resources/scene.py
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
async def update_layer(self, id_or_name: Union[int, str], layer: "Layer") -> "Layer":
    """
    Updates an existing scene layer.

    Args:
        id_or_name (Union[int, str]): The ID or name of the target layer.
        layer (Layer): The updated layer configuration.

    Returns:
        Layer: The successfully updated layer.
    """
    layer_id = await self._resolve_resource_id("layers", id_or_name)
    payload = _recursive_none_filter(layer.model_dump(mode="json", by_alias=True, exclude_unset=True))
    response = await self._http.put(f"{self._resolve_path()}/layers/{layer_id}", json=payload)
    return self.models.Layer.model_validate(response.json())
update_mask(id_or_name, mask) async

Updates an existing mask.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the target mask.

required
mask SceneMask

The updated mask configuration.

required

Returns:

Name Type Description
SceneMask SceneMask

The successfully updated mask.

Source code in src/xovis/api/device/resources/scene.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
async def update_mask(self, id_or_name: Union[int, str], mask: "SceneMask") -> "SceneMask":
    """
    Updates an existing mask.

    Args:
        id_or_name (Union[int, str]): The ID or name of the target mask.
        mask (SceneMask): The updated mask configuration.

    Returns:
        SceneMask: The successfully updated mask.
    """
    mask_id = await self._resolve_resource_id("masks", id_or_name)
    payload = _recursive_none_filter(mask.model_dump(mode="json", by_alias=True, exclude_unset=True))
    response = await self._http.put(f"{self._resolve_path()}/masks/{mask_id}", json=payload)
    return self.models.SceneMask.model_validate(response.json())
update_object(id_or_name, scene_object) async

Updates an existing Scene Object.

Parameters:

Name Type Description Default
id_or_name Union[int, str]

The ID or name of the target object.

required
scene_object SceneObject

The updated object configuration.

required

Returns:

Name Type Description
SceneObject SceneObject

The successfully updated object.

Source code in src/xovis/api/device/resources/scene.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
async def update_object(self, id_or_name: Union[int, str], scene_object: "SceneObject") -> "SceneObject":
    """
    Updates an existing Scene Object.

    Args:
        id_or_name (Union[int, str]): The ID or name of the target object.
        scene_object (SceneObject): The updated object configuration.

    Returns:
        SceneObject: The successfully updated object.
    """
    obj_id = await self._resolve_resource_id("scene_objects", id_or_name)
    payload = _recursive_none_filter(scene_object.model_dump(mode="json", by_alias=True, exclude_unset=True))
    response = await self._http.put(f"{self._resolve_path()}/objects/{obj_id}", json=payload)
    return self.models.SceneObject.model_validate(response.json())

Functions:

_recursive_none_filter(data)

Recursively removes None values from dictionaries and lists.

Source code in src/xovis/api/device/resources/scene.py
37
38
39
40
41
42
43
def _recursive_none_filter(data: Any) -> Any:
    """Recursively removes None values from dictionaries and lists."""
    if isinstance(data, dict):
        return {k: _recursive_none_filter(v) for k, v in data.items() if v is not None}
    elif isinstance(data, list):
        return [_recursive_none_filter(v) for v in data if v is not None]
    return data

xovis.api.device.resources.system

Xovis SDK - System Management Resource

Operates within the Control Plane. Provides the implementation for managing device-level system operations, including hardware identification, diagnostic log retrieval, configuration backups, and destructive lifecycle commands. Critical for Autonomous Fleet Maintenance (Module D).

Classes

SystemManager

Manages core system operations, diagnostics, and lifecycle states on a Xovis device.

This manager acts as the primary interface for hardware-level orchestration, enabling agents to pull plaintext logs, manage configuration backups, update physical metadata (Name/Group), and execute critical state resets.

Source code in src/xovis/api/device/resources/system.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
class SystemManager:
    """
    Manages core system operations, diagnostics, and lifecycle states on a Xovis device.

    This manager acts as the primary interface for hardware-level orchestration,
    enabling agents to pull plaintext logs, manage configuration backups, update
    physical metadata (Name/Group), and execute critical state resets.
    """

    def __init__(
        self,
        http_client: XovisHTTPClient,
        client: Optional["DeviceClient"] = None,
        target_id: Optional[str] = None,
    ) -> None:
        """
        Initializes the SystemManager.

        Args:
            http_client (XovisHTTPClient): The resilient HTTP client.
            client (Optional[DeviceClient]): The parent DeviceClient instance.
            target_id (Optional[str]): The multisensor target ID, if applicable.
        """
        self._http = http_client
        self._client = client
        self.target_id = target_id
        self._base_path = "/api/v5/device"

    @property
    def models(self) -> Any:
        """
        Returns the strictly validated Pydantic models for the current firmware.

        Returns:
            Any: The collection of auto-generated Pydantic V2 models
                synchronized with the current device firmware.
        """
        return self._client.models if self._client else stable_models

    def _resolve_singlesensor_path(self) -> str:
        """
        Resolves the singlesensor/multisensor status path based on context.

        Returns:
            str: The resolved API path for status and identity operations.
        """
        if self.target_id:
            return f"/api/v5/multisensors/{self.target_id}"
        return "/api/v5/singlesensor"

    async def get_info(self) -> Any:
        """
        Retrieves static hardware and identity metadata from the sensor.

        Returns:
            DeviceInfo: The sensor's hardware, revisions, and firmware details.
        """
        response = await self._http.get(f"{self._base_path}/info")
        try:
            return self.models.DeviceInfo.model_validate(response.json(), strict=False)
        except ValidationError as e:
            raise SDKFirmwareDriftError(f"System info payload unparsable: {e}")

    async def get_state(self) -> Any:
        """
        Retrieves the real-time device health, temperatures, and uptime.

        Returns:
            DeviceState: The current operational state and thermal metrics.
        """
        response = await self._http.get(f"{self._base_path}/state")
        try:
            # Pydantic V2 model validation
            return self.models.DeviceState1.model_validate(response.json(), strict=False)
        except ValidationError as e:
            raise SDKFirmwareDriftError(f"System state payload unparsable: {e}")
        except AttributeError:
            # Fallback if the dynamic models are not behaving as expected
            return response.json()

    async def get_status(self) -> Any:
        """
        Retrieves the operational status of the sensor (illumination, tilt, etc.).

        Returns:
            SinglesensorStatus: The current operational status.
        """
        response = await self._http.get(f"{self._resolve_singlesensor_path()}/status")
        return self.models.SinglesensorStatus.model_validate(response.json())

    async def get_light_settings(self) -> Any:
        """
        Retrieves the current light frequency and power frequency settings.

        Returns:
            LightSettings: The current light settings configuration.
        """
        response = await self._http.get(f"{self._resolve_singlesensor_path()}/settings/light")
        return self.models.LightSettings.model_validate(response.json())

    async def update_light_settings(self, settings: Any) -> Any:
        """
        Updates the light frequency and power frequency settings.
        Essential for fixing horizontal barring when light and power frequencies mismatch.

        Args:
            settings (LightSettings): The new light settings payload.

        Returns:
            LightSettings: The updated light settings from the sensor.
        """
        payload = settings.model_dump(mode="json", by_alias=True, exclude_unset=True)
        response = await self._http.put(f"{self._resolve_singlesensor_path()}/settings/light", json=payload)
        return self.models.LightSettings.model_validate(response.json())

    async def get_license(self) -> Any:
        """
        Retrieves the active/expired states of premium features.

        Returns:
            Dict[str, Any]: A mapping of feature IDs to their current license state.
        """
        # Fixed native SDK endpoint for V5 firmwares
        response = await self._http.get("/api/v5/license/features")
        response.raise_for_status()

        # If your LicenseStatus model doesn't match this exact payload yet,
        # return the raw dict for the LLM to parse natively for now.
        return response.json()

    async def get_device_identity(self) -> Any:
        """
        Retrieves the configured logical Name and Group of the physical sensor.

        Returns:
            DeviceId: The logical identifier metadata of the device.
        """
        response = await self._http.get(f"{self._base_path}/id")
        return self.models.DeviceId.model_validate(response.json(), strict=False)

    async def update_device_identity(self, name: str, group: str) -> None:
        """
        Updates the logical Name and Group of the physical sensor.

        Args:
            name (str): The new human-readable name of the sensor.
            group (str): The logical group assignment.
        """
        payload = {"name": name, "group": group}
        await self._http.put(f"{self._resolve_singlesensor_path()}/identity", json=payload)

    async def get_logs(self) -> str:
        """
        Retrieves the raw device user logfile in plaintext.

        Crucial for Autonomous Maintenance agents to parse root-cause
        failures (e.g., token synchronization errors, connection timeouts).

        Returns:
            str: The raw plaintext log stream.
        """
        response = await self._http.get(f"{self._base_path}/log")
        response.raise_for_status()
        return response.text

    async def get_license_details(self) -> Any:
        """
        Retrieves the detailed active, expired, and test states of premium features.

        Returns:
            LicenseStatusDetailed: The comprehensive license status map.
        """
        response = await self._http.get("/api/v5/license/status/details")
        return self.models.LicenseStatusDetailed.model_validate(response.json(), strict=False)

    async def trigger_backup(self) -> None:
        """
        Triggers an asynchronous backup of the sensor configuration.

        This is a non-blocking request. The agent must subsequently poll
        `get_backup_state()` to verify completion before downloading.
        """
        await self._http.post(f"{self._base_path}/backup")

    async def get_backup_state(self) -> Any:
        """
        Retrieves the state of the current configuration backup process.

        Returns:
            DiagBundleState: The current generation state (e.g., IN_PROGRESS, AVAILABLE).
        """
        response = await self._http.get(f"{self._base_path}/backup/state")
        return self.models.DiagBundleState.model_validate(response.json(), strict=False)

    async def download_backup(self) -> bytes:
        """
        Downloads the encrypted backup archive of the sensor configuration.

        Returns:
            bytes: The binary archive payload.
        """
        response = await self._http.get(f"{self._base_path}/backup")
        response.raise_for_status()
        return response.content

    async def restore_backup(self, backup_binary: bytes, ip_handling: str = "check") -> None:
        """
        Restores the sensor configuration from a binary backup file.

        Args:
            backup_binary (bytes): The raw binary archive to restore.
            ip_handling (str): How to handle IP config mismatches. Options:
                'check' (fail if mismatch), 'keep' (retain current IP),
                'overwrite' (apply IP from backup). Defaults to 'check'.
        """
        params = {"ip": ip_handling} if ip_handling else {}
        headers = {"Content-Type": "application/octet-stream"}
        response = await self._http.put(f"{self._base_path}/restore", params=params, content=backup_binary, headers=headers)
        response.raise_for_status()

    async def get_led(self) -> Any:
        """
        Retrieves the current physical LED configuration.

        Returns:
            DeviceLedMode: The current LED mode indicator.
        """
        response = await self._http.get(f"{self._base_path}/led")
        try:
            return self.models.DeviceLedMode.model_validate(response.json(), strict=False)
        except ValidationError as e:
            raise SDKFirmwareDriftError(f"LED configuration payload unparsable: {e}")

    async def update_led(self, led_mode: Any) -> None:
        """
        Updates the physical LED configuration.

        Args:
            led_mode (DeviceLedMode): The new LED configuration payload.
        """
        await self._http.put(f"{self._base_path}/led", json=led_mode)

    async def reboot(self) -> None:
        """
        Triggers a standard hardware reboot.
        Causes a temporary gap in counting and API accessibility.
        """
        await self._http.post(f"{self._base_path}/reboot")

    async def reboot_rescue(self) -> None:
        """
        CRITICAL: Triggers a reboot into Rescue Mode.
        Halts the standard API and counting logic entirely. Requires human
        intervention or rescue-API firmware flashing to recover.
        """
        await self._http.post(f"{self._base_path}/reboot/rescue")

    async def reset(self) -> None:
        """
        CRITICAL: Triggers a Scene and Data Reset.
        Wipes all geometries, logic configurations, and historical databases.
        Network and User configurations are preserved. Device will reboot.
        """
        await self._http.post(f"{self._base_path}/reset")

    async def hard_reset(self, smk: str) -> None:
        """
        CRITICAL: Triggers a complete Factory Hard Reset.
        Deletes all data, configurations, and network settings.

        Args:
            smk (str): The Sensor Master Key required to authorize the wipe.
        """
        payload = {"smk": smk}
        await self._http.post(f"{self._base_path}/reset/hard", json=payload)

    async def format_flash(self) -> None:
        """
        CRITICAL: Formats the internal flash storage.
        Deletes data, configs, and parts of the firmware. The sensor will
        reboot into rescue mode and require a manual firmware reinstall.
        """
        await self._http.post(f"{self._base_path}/flash/format")
Attributes
models property

Returns the strictly validated Pydantic models for the current firmware.

Returns:

Name Type Description
Any Any

The collection of auto-generated Pydantic V2 models synchronized with the current device firmware.

Methods:
__init__(http_client, client=None, target_id=None)

Initializes the SystemManager.

Parameters:

Name Type Description Default
http_client XovisHTTPClient

The resilient HTTP client.

required
client Optional[DeviceClient]

The parent DeviceClient instance.

None
target_id Optional[str]

The multisensor target ID, if applicable.

None
Source code in src/xovis/api/device/resources/system.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def __init__(
    self,
    http_client: XovisHTTPClient,
    client: Optional["DeviceClient"] = None,
    target_id: Optional[str] = None,
) -> None:
    """
    Initializes the SystemManager.

    Args:
        http_client (XovisHTTPClient): The resilient HTTP client.
        client (Optional[DeviceClient]): The parent DeviceClient instance.
        target_id (Optional[str]): The multisensor target ID, if applicable.
    """
    self._http = http_client
    self._client = client
    self.target_id = target_id
    self._base_path = "/api/v5/device"
_resolve_singlesensor_path()

Resolves the singlesensor/multisensor status path based on context.

Returns:

Name Type Description
str str

The resolved API path for status and identity operations.

Source code in src/xovis/api/device/resources/system.py
62
63
64
65
66
67
68
69
70
71
def _resolve_singlesensor_path(self) -> str:
    """
    Resolves the singlesensor/multisensor status path based on context.

    Returns:
        str: The resolved API path for status and identity operations.
    """
    if self.target_id:
        return f"/api/v5/multisensors/{self.target_id}"
    return "/api/v5/singlesensor"
download_backup() async

Downloads the encrypted backup archive of the sensor configuration.

Returns:

Name Type Description
bytes bytes

The binary archive payload.

Source code in src/xovis/api/device/resources/system.py
217
218
219
220
221
222
223
224
225
226
async def download_backup(self) -> bytes:
    """
    Downloads the encrypted backup archive of the sensor configuration.

    Returns:
        bytes: The binary archive payload.
    """
    response = await self._http.get(f"{self._base_path}/backup")
    response.raise_for_status()
    return response.content
format_flash() async

CRITICAL: Formats the internal flash storage. Deletes data, configs, and parts of the firmware. The sensor will reboot into rescue mode and require a manual firmware reinstall.

Source code in src/xovis/api/device/resources/system.py
299
300
301
302
303
304
305
async def format_flash(self) -> None:
    """
    CRITICAL: Formats the internal flash storage.
    Deletes data, configs, and parts of the firmware. The sensor will
    reboot into rescue mode and require a manual firmware reinstall.
    """
    await self._http.post(f"{self._base_path}/flash/format")
get_backup_state() async

Retrieves the state of the current configuration backup process.

Returns:

Name Type Description
DiagBundleState Any

The current generation state (e.g., IN_PROGRESS, AVAILABLE).

Source code in src/xovis/api/device/resources/system.py
207
208
209
210
211
212
213
214
215
async def get_backup_state(self) -> Any:
    """
    Retrieves the state of the current configuration backup process.

    Returns:
        DiagBundleState: The current generation state (e.g., IN_PROGRESS, AVAILABLE).
    """
    response = await self._http.get(f"{self._base_path}/backup/state")
    return self.models.DiagBundleState.model_validate(response.json(), strict=False)
get_device_identity() async

Retrieves the configured logical Name and Group of the physical sensor.

Returns:

Name Type Description
DeviceId Any

The logical identifier metadata of the device.

Source code in src/xovis/api/device/resources/system.py
153
154
155
156
157
158
159
160
161
async def get_device_identity(self) -> Any:
    """
    Retrieves the configured logical Name and Group of the physical sensor.

    Returns:
        DeviceId: The logical identifier metadata of the device.
    """
    response = await self._http.get(f"{self._base_path}/id")
    return self.models.DeviceId.model_validate(response.json(), strict=False)
get_info() async

Retrieves static hardware and identity metadata from the sensor.

Returns:

Name Type Description
DeviceInfo Any

The sensor's hardware, revisions, and firmware details.

Source code in src/xovis/api/device/resources/system.py
73
74
75
76
77
78
79
80
81
82
83
84
async def get_info(self) -> Any:
    """
    Retrieves static hardware and identity metadata from the sensor.

    Returns:
        DeviceInfo: The sensor's hardware, revisions, and firmware details.
    """
    response = await self._http.get(f"{self._base_path}/info")
    try:
        return self.models.DeviceInfo.model_validate(response.json(), strict=False)
    except ValidationError as e:
        raise SDKFirmwareDriftError(f"System info payload unparsable: {e}")
get_led() async

Retrieves the current physical LED configuration.

Returns:

Name Type Description
DeviceLedMode Any

The current LED mode indicator.

Source code in src/xovis/api/device/resources/system.py
243
244
245
246
247
248
249
250
251
252
253
254
async def get_led(self) -> Any:
    """
    Retrieves the current physical LED configuration.

    Returns:
        DeviceLedMode: The current LED mode indicator.
    """
    response = await self._http.get(f"{self._base_path}/led")
    try:
        return self.models.DeviceLedMode.model_validate(response.json(), strict=False)
    except ValidationError as e:
        raise SDKFirmwareDriftError(f"LED configuration payload unparsable: {e}")
get_license() async

Retrieves the active/expired states of premium features.

Returns:

Type Description
Any

Dict[str, Any]: A mapping of feature IDs to their current license state.

Source code in src/xovis/api/device/resources/system.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
async def get_license(self) -> Any:
    """
    Retrieves the active/expired states of premium features.

    Returns:
        Dict[str, Any]: A mapping of feature IDs to their current license state.
    """
    # Fixed native SDK endpoint for V5 firmwares
    response = await self._http.get("/api/v5/license/features")
    response.raise_for_status()

    # If your LicenseStatus model doesn't match this exact payload yet,
    # return the raw dict for the LLM to parse natively for now.
    return response.json()
get_license_details() async

Retrieves the detailed active, expired, and test states of premium features.

Returns:

Name Type Description
LicenseStatusDetailed Any

The comprehensive license status map.

Source code in src/xovis/api/device/resources/system.py
188
189
190
191
192
193
194
195
196
async def get_license_details(self) -> Any:
    """
    Retrieves the detailed active, expired, and test states of premium features.

    Returns:
        LicenseStatusDetailed: The comprehensive license status map.
    """
    response = await self._http.get("/api/v5/license/status/details")
    return self.models.LicenseStatusDetailed.model_validate(response.json(), strict=False)
get_light_settings() async

Retrieves the current light frequency and power frequency settings.

Returns:

Name Type Description
LightSettings Any

The current light settings configuration.

Source code in src/xovis/api/device/resources/system.py
113
114
115
116
117
118
119
120
121
async def get_light_settings(self) -> Any:
    """
    Retrieves the current light frequency and power frequency settings.

    Returns:
        LightSettings: The current light settings configuration.
    """
    response = await self._http.get(f"{self._resolve_singlesensor_path()}/settings/light")
    return self.models.LightSettings.model_validate(response.json())
get_logs() async

Retrieves the raw device user logfile in plaintext.

Crucial for Autonomous Maintenance agents to parse root-cause failures (e.g., token synchronization errors, connection timeouts).

Returns:

Name Type Description
str str

The raw plaintext log stream.

Source code in src/xovis/api/device/resources/system.py
174
175
176
177
178
179
180
181
182
183
184
185
186
async def get_logs(self) -> str:
    """
    Retrieves the raw device user logfile in plaintext.

    Crucial for Autonomous Maintenance agents to parse root-cause
    failures (e.g., token synchronization errors, connection timeouts).

    Returns:
        str: The raw plaintext log stream.
    """
    response = await self._http.get(f"{self._base_path}/log")
    response.raise_for_status()
    return response.text
get_state() async

Retrieves the real-time device health, temperatures, and uptime.

Returns:

Name Type Description
DeviceState Any

The current operational state and thermal metrics.

Source code in src/xovis/api/device/resources/system.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
async def get_state(self) -> Any:
    """
    Retrieves the real-time device health, temperatures, and uptime.

    Returns:
        DeviceState: The current operational state and thermal metrics.
    """
    response = await self._http.get(f"{self._base_path}/state")
    try:
        # Pydantic V2 model validation
        return self.models.DeviceState1.model_validate(response.json(), strict=False)
    except ValidationError as e:
        raise SDKFirmwareDriftError(f"System state payload unparsable: {e}")
    except AttributeError:
        # Fallback if the dynamic models are not behaving as expected
        return response.json()
get_status() async

Retrieves the operational status of the sensor (illumination, tilt, etc.).

Returns:

Name Type Description
SinglesensorStatus Any

The current operational status.

Source code in src/xovis/api/device/resources/system.py
103
104
105
106
107
108
109
110
111
async def get_status(self) -> Any:
    """
    Retrieves the operational status of the sensor (illumination, tilt, etc.).

    Returns:
        SinglesensorStatus: The current operational status.
    """
    response = await self._http.get(f"{self._resolve_singlesensor_path()}/status")
    return self.models.SinglesensorStatus.model_validate(response.json())
hard_reset(smk) async

CRITICAL: Triggers a complete Factory Hard Reset. Deletes all data, configurations, and network settings.

Parameters:

Name Type Description Default
smk str

The Sensor Master Key required to authorize the wipe.

required
Source code in src/xovis/api/device/resources/system.py
288
289
290
291
292
293
294
295
296
297
async def hard_reset(self, smk: str) -> None:
    """
    CRITICAL: Triggers a complete Factory Hard Reset.
    Deletes all data, configurations, and network settings.

    Args:
        smk (str): The Sensor Master Key required to authorize the wipe.
    """
    payload = {"smk": smk}
    await self._http.post(f"{self._base_path}/reset/hard", json=payload)
reboot() async

Triggers a standard hardware reboot. Causes a temporary gap in counting and API accessibility.

Source code in src/xovis/api/device/resources/system.py
265
266
267
268
269
270
async def reboot(self) -> None:
    """
    Triggers a standard hardware reboot.
    Causes a temporary gap in counting and API accessibility.
    """
    await self._http.post(f"{self._base_path}/reboot")
reboot_rescue() async

CRITICAL: Triggers a reboot into Rescue Mode. Halts the standard API and counting logic entirely. Requires human intervention or rescue-API firmware flashing to recover.

Source code in src/xovis/api/device/resources/system.py
272
273
274
275
276
277
278
async def reboot_rescue(self) -> None:
    """
    CRITICAL: Triggers a reboot into Rescue Mode.
    Halts the standard API and counting logic entirely. Requires human
    intervention or rescue-API firmware flashing to recover.
    """
    await self._http.post(f"{self._base_path}/reboot/rescue")
reset() async

CRITICAL: Triggers a Scene and Data Reset. Wipes all geometries, logic configurations, and historical databases. Network and User configurations are preserved. Device will reboot.

Source code in src/xovis/api/device/resources/system.py
280
281
282
283
284
285
286
async def reset(self) -> None:
    """
    CRITICAL: Triggers a Scene and Data Reset.
    Wipes all geometries, logic configurations, and historical databases.
    Network and User configurations are preserved. Device will reboot.
    """
    await self._http.post(f"{self._base_path}/reset")
restore_backup(backup_binary, ip_handling='check') async

Restores the sensor configuration from a binary backup file.

Parameters:

Name Type Description Default
backup_binary bytes

The raw binary archive to restore.

required
ip_handling str

How to handle IP config mismatches. Options: 'check' (fail if mismatch), 'keep' (retain current IP), 'overwrite' (apply IP from backup). Defaults to 'check'.

'check'
Source code in src/xovis/api/device/resources/system.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
async def restore_backup(self, backup_binary: bytes, ip_handling: str = "check") -> None:
    """
    Restores the sensor configuration from a binary backup file.

    Args:
        backup_binary (bytes): The raw binary archive to restore.
        ip_handling (str): How to handle IP config mismatches. Options:
            'check' (fail if mismatch), 'keep' (retain current IP),
            'overwrite' (apply IP from backup). Defaults to 'check'.
    """
    params = {"ip": ip_handling} if ip_handling else {}
    headers = {"Content-Type": "application/octet-stream"}
    response = await self._http.put(f"{self._base_path}/restore", params=params, content=backup_binary, headers=headers)
    response.raise_for_status()
trigger_backup() async

Triggers an asynchronous backup of the sensor configuration.

This is a non-blocking request. The agent must subsequently poll get_backup_state() to verify completion before downloading.

Source code in src/xovis/api/device/resources/system.py
198
199
200
201
202
203
204
205
async def trigger_backup(self) -> None:
    """
    Triggers an asynchronous backup of the sensor configuration.

    This is a non-blocking request. The agent must subsequently poll
    `get_backup_state()` to verify completion before downloading.
    """
    await self._http.post(f"{self._base_path}/backup")
update_device_identity(name, group) async

Updates the logical Name and Group of the physical sensor.

Parameters:

Name Type Description Default
name str

The new human-readable name of the sensor.

required
group str

The logical group assignment.

required
Source code in src/xovis/api/device/resources/system.py
163
164
165
166
167
168
169
170
171
172
async def update_device_identity(self, name: str, group: str) -> None:
    """
    Updates the logical Name and Group of the physical sensor.

    Args:
        name (str): The new human-readable name of the sensor.
        group (str): The logical group assignment.
    """
    payload = {"name": name, "group": group}
    await self._http.put(f"{self._resolve_singlesensor_path()}/identity", json=payload)
update_led(led_mode) async

Updates the physical LED configuration.

Parameters:

Name Type Description Default
led_mode DeviceLedMode

The new LED configuration payload.

required
Source code in src/xovis/api/device/resources/system.py
256
257
258
259
260
261
262
263
async def update_led(self, led_mode: Any) -> None:
    """
    Updates the physical LED configuration.

    Args:
        led_mode (DeviceLedMode): The new LED configuration payload.
    """
    await self._http.put(f"{self._base_path}/led", json=led_mode)
update_light_settings(settings) async

Updates the light frequency and power frequency settings. Essential for fixing horizontal barring when light and power frequencies mismatch.

Parameters:

Name Type Description Default
settings LightSettings

The new light settings payload.

required

Returns:

Name Type Description
LightSettings Any

The updated light settings from the sensor.

Source code in src/xovis/api/device/resources/system.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
async def update_light_settings(self, settings: Any) -> Any:
    """
    Updates the light frequency and power frequency settings.
    Essential for fixing horizontal barring when light and power frequencies mismatch.

    Args:
        settings (LightSettings): The new light settings payload.

    Returns:
        LightSettings: The updated light settings from the sensor.
    """
    payload = settings.model_dump(mode="json", by_alias=True, exclude_unset=True)
    response = await self._http.put(f"{self._resolve_singlesensor_path()}/settings/light", json=payload)
    return self.models.LightSettings.model_validate(response.json())

xovis.api.device.resources.network

Xovis SDK - Network Management Resource

Operates within the Control Plane. Provides comprehensive implementation for managing edge sensor networking, including IPv4/IPv6, Remote Tunnels (Cloud Hub), 802.1X (EAPoL) Enterprise Security, X.509 Truststores, and advanced Wireless scanning.

Classes

NetworkManager

Manages physical and logical network configurations on a Xovis device.

Provides deep orchestration capabilities for Autonomous Maintenance (Module D), allowing agents to rebuild dropped Cloud Hub tunnels, rotate expiring X.509 certificates, probe local Wi-Fi environments, and manage 802.1X authentication.

Source code in src/xovis/api/device/resources/network.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
class NetworkManager:
    """
    Manages physical and logical network configurations on a Xovis device.

    Provides deep orchestration capabilities for Autonomous Maintenance (Module D),
    allowing agents to rebuild dropped Cloud Hub tunnels, rotate expiring X.509
    certificates, probe local Wi-Fi environments, and manage 802.1X authentication.
    """

    def __init__(self, http_client: XovisHTTPClient, client: Optional["DeviceClient"] = None) -> None:
        """
        Initializes the NetworkManager.

        Args:
            http_client (XovisHTTPClient): The resilient HTTP client.
            client (Optional[DeviceClient]): The parent DeviceClient instance.
        """
        self._http = http_client
        self._client = client
        self._base_path = "/api/v5/network"

    @property
    def models(self):
        """Returns the strictly validated Pydantic models for the current firmware."""
        return self._client.models if self._client else stable_models

    # --- GENERAL NETWORK STATE & HOSTNAME ---
    async def get_state(self) -> Any:
        """Retrieves the comprehensive operational state of all network interfaces."""
        response = await self._http.get(f"{self._base_path}/state")
        return self.models.NetworkState.model_validate(response.json())

    async def get_hostname(self) -> Any:
        """Retrieves the current hostname configuration."""
        response = await self._http.get(f"{self._base_path}/hostname")
        return self.models.Hostname.model_validate(response.json())

    async def update_hostname(self, hostname: Any) -> None:
        """Updates the sensor's logical hostname."""
        await self._http.put(f"{self._base_path}/hostname", json=hostname)

    async def reset_hostname(self) -> None:
        """Resets the hostname to the factory default (XS-SENSOR-[MAC])."""
        await self._http.delete(f"{self._base_path}/hostname")

    async def get_gigabit(self) -> Any:
        """Retrieves the Gigabit Ethernet enablement capability."""
        response = await self._http.get(f"{self._base_path}/gigabit")
        return self.models.GigabitEnabled.model_validate(response.json())

    async def update_gigabit(self, gigabit: Any) -> None:
        """Enables or disables Gigabit Ethernet capability."""
        await self._http.put(f"{self._base_path}/gigabit", json=gigabit)

    # --- IPV4 & IPV6 CONFIGURATION ---
    async def get_ipv4(self) -> Any:
        """Retrieves the current IPv4 network configuration (Static/DHCP)."""
        response = await self._http.get(f"{self._base_path}/ipv4")
        return self.models.NetworkIpv4Settings.model_validate(response.json())

    async def update_ipv4(self, settings: Any) -> None:
        """CRITICAL: Updates IPv4 configuration. May sever active connections."""
        await self._http.put(f"{self._base_path}/ipv4", json=settings)

    async def reset_ipv4(self) -> None:
        """CRITICAL: Resets IPv4 to factory defaults (DHCP enabled)."""
        await self._http.delete(f"{self._base_path}/ipv4")

    async def get_ipv6(self) -> Any:
        """Retrieves the current IPv6 network configuration."""
        response = await self._http.get(f"{self._base_path}/ipv6")
        return self.models.NetworkIpv6Settings.model_validate(response.json())

    async def update_ipv6(self, settings: Any) -> None:
        """CRITICAL: Updates IPv6 configuration."""
        await self._http.put(f"{self._base_path}/ipv6", json=settings)

    async def reset_ipv6(self) -> None:
        """CRITICAL: Resets IPv6 to factory defaults (Disabled)."""
        await self._http.delete(f"{self._base_path}/ipv6")

    # --- REMOTE CONNECTIONS (HUB TUNNELS) ---
    async def get_all_remotes(self) -> Any:
        """Retrieves configurations for all defined remote connections (Cloud Tunnels)."""
        response = await self._http.get(f"{self._base_path}/remotes")
        # We use a custom parser to handle relative URLs in the 'uri' field,
        # which Pydantic's AnyUrl strictly rejects even in non-strict mode.
        data = response.json()
        if "remotes" in data:
            for remote in data["remotes"]:
                if "uri" in remote and isinstance(remote["uri"], str) and remote["uri"].startswith("/"):
                    remote["uri"] = f"http://localhost{remote['uri']}"
        return self.models.RemoteConnections.model_validate(data)

    async def create_remote(self, remote: Any) -> Any:
        """
        Creates a new remote connection (e.g., establishing a new Hub Tunnel).
        Automatically starts the connection upon creation.
        """
        response = await self._http.post(f"{self._base_path}/remotes", json=remote)
        data = response.json()
        if "uri" in data and isinstance(data["uri"], str) and data["uri"].startswith("/"):
            data["uri"] = f"http://localhost{data['uri']}"
        return self.models.IndexedRemoteConnection.model_validate(data)

    async def create_remote_from_config(self, config: Any) -> Any:
        """Creates a new remote connection using a base64-encoded configuration payload."""
        response = await self._http.post(f"{self._base_path}/remotes/configuration", json=config)
        data = response.json()
        if "uri" in data and isinstance(data["uri"], str) and data["uri"].startswith("/"):
            data["uri"] = f"http://localhost{data['uri']}"
        return self.models.IndexedRemoteConnection.model_validate(data)

    async def get_remote(self, remote_id: int) -> Any:
        """Retrieves a specific remote connection configuration."""
        response = await self._http.get(f"{self._base_path}/remotes/{remote_id}")
        data = response.json()
        if "uri" in data and isinstance(data["uri"], str) and data["uri"].startswith("/"):
            data["uri"] = f"http://localhost{data['uri']}"
        return self.models.IndexedRemoteConnection.model_validate(data)

    async def update_remote(self, remote_id: int, remote: Any) -> Any:
        """Updates an existing remote connection."""
        payload = remote.model_dump(by_alias=True, exclude_unset=True, mode="json")
        response = await self._http.put(f"{self._base_path}/remotes/{remote_id}", json=payload)
        data = response.json()
        if "uri" in data and isinstance(data["uri"], str) and data["uri"].startswith("/"):
            data["uri"] = f"http://localhost{data['uri']}"
        return self.models.IndexedRemoteConnection.model_validate(data)

    async def delete_remote(self, remote_id: int) -> None:
        """BLOCKED: Deletes a remote connection, severing the management tunnel."""
        await self._http.delete(f"{self._base_path}/remotes/{remote_id}")

    async def get_all_remotes_state(self) -> Any:
        """Retrieves the connectivity status for all remote tunnels."""
        response = await self._http.get(f"{self._base_path}/remotes/state")
        return self.models.RemoteConnectionStates.model_validate(response.json())

    async def get_remote_state(self, remote_id: int) -> Any:
        """Retrieves the connectivity status of a specific remote tunnel."""
        response = await self._http.get(f"{self._base_path}/remotes/{remote_id}/state")
        return self.models.RemoteConnectionState.model_validate(response.json())

    # --- XOVIS SUPPORT & REMOTE SERVICES ---
    async def get_xovis_support_state(self) -> Any:
        """Retrieves the state of the Xovis Tier 3 Support Remote Connection."""
        response = await self._http.get(f"{self._base_path}/remotes/xovissupport")
        return self.models.XovisRemoteSupportState.model_validate(response.json())

    async def update_xovis_support(self, ctrl: Any) -> None:
        """Controls the enablement of the Xovis Support Remote Connection."""
        payload = ctrl.model_dump(by_alias=True, exclude_unset=True, mode="json")
        await self._http.put(f"{self._base_path}/remotes/xovissupport", json=payload)

    async def get_remote_services_config(self) -> Any:
        """Retrieves the overarching configuration for Xovis Remote Services (IoT)."""
        response = await self._http.get(f"{self._base_path}/remotes/services/config")
        return self.models.RemoteServicesSettings.model_validate(response.json())

    async def update_remote_services_config(self, settings: Any) -> None:
        """Updates the Xovis Remote Services configuration."""
        payload = settings.model_dump(by_alias=True, exclude_unset=True, mode="json")
        await self._http.put(f"{self._base_path}/remotes/services/config", json=payload)

    async def reset_remote_services_config(self) -> None:
        """Resets the Xovis Remote Services configuration."""
        await self._http.delete(f"{self._base_path}/remotes/services/config")

    async def get_remote_services_state(self) -> Any:
        """Retrieves the connectivity status of Xovis Remote Services."""
        response = await self._http.get(f"{self._base_path}/remotes/services/state")
        return self.models.RemoteServicesState.model_validate(response.json())

    # --- 802.1X EAPoL & IDENTITY ---
    async def get_eapol_config(self) -> Any:
        """Retrieves the 802.1X EAPoL Supplicant configuration."""
        response = await self._http.get(f"{self._base_path}/eapol/config")
        return self.models.EapolConfig.model_validate(response.json())

    async def update_eapol_config(self, config: Any) -> Any:
        """CRITICAL: Updates 802.1X configuration. May cause network drops."""
        response = await self._http.put(f"{self._base_path}/eapol/config", json=config)
        return self.models.EapolConfig.model_validate(response.json())

    async def reset_eapol_config(self) -> Any:
        """CRITICAL: Resets 802.1X configuration to factory defaults."""
        response = await self._http.delete(f"{self._base_path}/eapol/config")
        return self.models.EapolConfig.model_validate(response.json())

    async def get_eapol_state(self) -> Any:
        """Retrieves the authentication state of the 802.1X Supplicant."""
        response = await self._http.get(f"{self._base_path}/eapol/state")
        return self.models.EapolState.model_validate(response.json())

    async def get_eapol_keystore(self) -> Any:
        """Retrieves the configured X.509 client identity for 802.1X."""
        response = await self._http.get(f"{self._base_path}/eapol/x509/keystore")
        if response.status_code == 204:
            return None
        return self.models.X509Certificate.model_validate(response.json())

    async def update_eapol_keystore(self, pem_binary: bytes) -> Any:
        """
        Uploads a new X.509 client identity (Certificate + RSA Key).
        Must be formatted as PEM. Uses strict binary streaming.
        """
        headers = {"Content-Type": "application/octet-stream"}
        response = await self._http.put(f"{self._base_path}/eapol/x509/keystore", content=pem_binary, headers=headers)
        return self.models.X509Certificate.model_validate(response.json())

    async def delete_eapol_keystore(self) -> None:
        """Removes the X.509 client identity from the EAPoL Supplicant."""
        await self._http.delete(f"{self._base_path}/eapol/x509/keystore")

    # --- X.509 TRUSTSTORE ---
    async def get_truststore_certs(self) -> Any:
        """Retrieves all installed X.509 certificates in the truststore."""
        response = await self._http.get(f"{self._base_path}/x509/truststore")
        return self.models.X509Certificates.model_validate(response.json())

    async def install_truststore_cert(self, cert_binary: bytes) -> Any:
        """Installs a new X.509 certificate to the truststore."""
        headers = {"Content-Type": "application/octet-stream"}
        response = await self._http.post(f"{self._base_path}/x509/truststore", content=cert_binary, headers=headers)
        return self.models.X509Certificate.model_validate(response.json())

    async def reset_truststore(self) -> None:
        """Resets the truststore to factory default certificates."""
        await self._http.delete(f"{self._base_path}/x509/truststore")

    async def get_truststore_config(self) -> Any:
        """Retrieves the truststore configuration (e.g., use defaults)."""
        response = await self._http.get(f"{self._base_path}/x509/truststore/config")
        return self.models.X509TruststoreConfig.model_validate(response.json())

    async def update_truststore_config(self, config: Any) -> Any:
        """Updates the truststore configuration."""
        response = await self._http.put(f"{self._base_path}/x509/truststore/config", json=config)
        return self.models.X509TruststoreConfig.model_validate(response.json())

    async def get_truststore_cert(self, fingerprint: str) -> Any:
        """Retrieves details of a specific X.509 certificate by its SHA-1 fingerprint."""
        response = await self._http.get(f"{self._base_path}/x509/truststore/{fingerprint}")
        return self.models.X509Certificate.model_validate(response.json())

    async def delete_truststore_cert(self, fingerprint: str) -> None:
        """Removes a specific X.509 certificate from the truststore."""
        await self._http.delete(f"{self._base_path}/x509/truststore/{fingerprint}")

    # --- WIRELESS (WLAN) SCANNING & CONFIGURATION ---
    async def get_wireless(self) -> Any:
        """Retrieves the global wireless networking configuration."""
        response = await self._http.get(f"{self._base_path}/wireless")
        return self.models.WlanSettings.model_validate(response.json())

    async def update_wireless(self, settings: Any) -> Any:
        """Updates the global wireless networking configuration."""
        response = await self._http.put(f"{self._base_path}/wireless", json=settings)
        return self.models.WlanSettings.model_validate(response.json())

    async def reset_wireless(self) -> Any:
        """Resets global wireless networking configuration to factory defaults."""
        response = await self._http.delete(f"{self._base_path}/wireless")
        return self.models.WlanSettings.model_validate(response.json())

    async def get_wireless_state(self) -> Any:
        """Retrieves the current state and connection metrics of wireless networking."""
        response = await self._http.get(f"{self._base_path}/wireless/state")
        return self.models.StateResult.model_validate(response.json())

    async def get_wireless_networks(self) -> Any:
        """Retrieves all configured wireless network profiles."""
        response = await self._http.get(f"{self._base_path}/wireless/networks")
        return self.models.WlanNetworks.model_validate(response.json())

    async def create_wireless_network(self, network: Any) -> Any:
        """Adds a new wireless network profile."""
        response = await self._http.post(f"{self._base_path}/wireless/networks", json=network)
        return self.models.WlanNetwork.model_validate(response.json())

    async def delete_all_wireless_networks(self) -> Any:
        """Removes all configured wireless network profiles."""
        response = await self._http.delete(f"{self._base_path}/wireless/networks")
        return self.models.WlanNetworks.model_validate(response.json())

    async def get_wireless_network(self, network_id: int) -> Any:
        """Retrieves a specific wireless network profile."""
        response = await self._http.get(f"{self._base_path}/wireless/networks/{network_id}")
        return self.models.WlanNetwork.model_validate(response.json())

    async def update_wireless_network(self, network_id: int, network: Any) -> Any:
        """Updates a specific wireless network profile."""
        response = await self._http.put(f"{self._base_path}/wireless/networks/{network_id}", json=network)
        return self.models.WlanNetwork.model_validate(response.json())

    async def delete_wireless_network(self, network_id: int) -> Any:
        """Removes a specific wireless network profile."""
        response = await self._http.delete(f"{self._base_path}/wireless/networks/{network_id}")
        return self.models.WlanNetwork.model_validate(response.json())

    async def test_wireless_network(self, network_id: int) -> None:
        """Triggers a connection test for a specific wireless profile."""
        await self._http.post(f"{self._base_path}/wireless/networks/{network_id}/test")

    async def get_wireless_test_result(self, network_id: int) -> Any:
        """Retrieves the result of a wireless connection test."""
        response = await self._http.get(f"{self._base_path}/wireless/networks/{network_id}/test")
        return self.models.ScanResult.model_validate(response.json())

    async def trigger_wireless_scan(self) -> None:
        """Triggers an active scan for nearby IEEE 802.11 Access Points."""
        await self._http.post(f"{self._base_path}/wireless/scan")

    async def get_wireless_scan_results(self) -> Any:
        """Retrieves the results of the latest wireless network scan."""
        response = await self._http.get(f"{self._base_path}/wireless/scan")
        return self.models.ScanResults.model_validate(response.json())

    # --- PROXY & MDNS ---
    async def get_proxy(self) -> Any:
        """Retrieves the current network proxy configuration."""
        response = await self._http.get(f"{self._base_path}/proxy")
        return self.models.NetworkProxy.model_validate(response.json())

    async def update_proxy(self, proxy: Any) -> None:
        """Updates the network proxy configuration."""
        await self._http.put(f"{self._base_path}/proxy", json=proxy)

    async def reset_proxy(self) -> None:
        """Deletes the current proxy configuration."""
        await self._http.delete(f"{self._base_path}/proxy")

    async def get_mdns_config(self) -> Any:
        """Retrieves the mDNS (Multicast DNS) configuration."""
        response = await self._http.get(f"{self._base_path}/mdns/config")
        return self.models.MdnsConfig.model_validate(response.json())

    async def update_mdns_config(self, config: Any) -> None:
        """Updates the mDNS configuration."""
        await self._http.put(f"{self._base_path}/mdns/config", json=config)

    async def get_mdns_state(self) -> Any:
        """Retrieves the currently discovered mDNS services in the local network."""
        response = await self._http.get(f"{self._base_path}/mdns/state")
        return self.models.MdnsState.model_validate(response.json())

    # --- PIP (Product Improvement Program) ---
    async def get_pip_config(self) -> Any:
        """Retrieves the Xovis PIP (Product Improvement Program) configuration."""
        response = await self._http.get(f"{self._base_path}/pip")
        return self.models.PipSettings.model_validate(response.json())

    async def update_pip_config(self, settings: Any) -> None:
        """Updates the Xovis PIP configuration."""
        await self._http.put(f"{self._base_path}/pip", json=settings)

    async def get_pip_state(self) -> Any:
        """Retrieves the state and quota usage of the Xovis PIP service."""
        response = await self._http.get(f"{self._base_path}/pip/status")
        return self.models.PipState.model_validate(response.json())

    async def reset_pip_quota(self) -> None:
        """Resets the monthly used PIP quota."""
        await self._http.post(f"{self._base_path}/pip_quota_reset")
Attributes
models property

Returns the strictly validated Pydantic models for the current firmware.

Methods:
__init__(http_client, client=None)

Initializes the NetworkManager.

Parameters:

Name Type Description Default
http_client XovisHTTPClient

The resilient HTTP client.

required
client Optional[DeviceClient]

The parent DeviceClient instance.

None
Source code in src/xovis/api/device/resources/network.py
28
29
30
31
32
33
34
35
36
37
38
def __init__(self, http_client: XovisHTTPClient, client: Optional["DeviceClient"] = None) -> None:
    """
    Initializes the NetworkManager.

    Args:
        http_client (XovisHTTPClient): The resilient HTTP client.
        client (Optional[DeviceClient]): The parent DeviceClient instance.
    """
    self._http = http_client
    self._client = client
    self._base_path = "/api/v5/network"
create_remote(remote) async

Creates a new remote connection (e.g., establishing a new Hub Tunnel). Automatically starts the connection upon creation.

Source code in src/xovis/api/device/resources/network.py
113
114
115
116
117
118
119
120
121
122
async def create_remote(self, remote: Any) -> Any:
    """
    Creates a new remote connection (e.g., establishing a new Hub Tunnel).
    Automatically starts the connection upon creation.
    """
    response = await self._http.post(f"{self._base_path}/remotes", json=remote)
    data = response.json()
    if "uri" in data and isinstance(data["uri"], str) and data["uri"].startswith("/"):
        data["uri"] = f"http://localhost{data['uri']}"
    return self.models.IndexedRemoteConnection.model_validate(data)
create_remote_from_config(config) async

Creates a new remote connection using a base64-encoded configuration payload.

Source code in src/xovis/api/device/resources/network.py
124
125
126
127
128
129
130
async def create_remote_from_config(self, config: Any) -> Any:
    """Creates a new remote connection using a base64-encoded configuration payload."""
    response = await self._http.post(f"{self._base_path}/remotes/configuration", json=config)
    data = response.json()
    if "uri" in data and isinstance(data["uri"], str) and data["uri"].startswith("/"):
        data["uri"] = f"http://localhost{data['uri']}"
    return self.models.IndexedRemoteConnection.model_validate(data)
create_wireless_network(network) async

Adds a new wireless network profile.

Source code in src/xovis/api/device/resources/network.py
295
296
297
298
async def create_wireless_network(self, network: Any) -> Any:
    """Adds a new wireless network profile."""
    response = await self._http.post(f"{self._base_path}/wireless/networks", json=network)
    return self.models.WlanNetwork.model_validate(response.json())
delete_all_wireless_networks() async

Removes all configured wireless network profiles.

Source code in src/xovis/api/device/resources/network.py
300
301
302
303
async def delete_all_wireless_networks(self) -> Any:
    """Removes all configured wireless network profiles."""
    response = await self._http.delete(f"{self._base_path}/wireless/networks")
    return self.models.WlanNetworks.model_validate(response.json())
delete_eapol_keystore() async

Removes the X.509 client identity from the EAPoL Supplicant.

Source code in src/xovis/api/device/resources/network.py
230
231
232
async def delete_eapol_keystore(self) -> None:
    """Removes the X.509 client identity from the EAPoL Supplicant."""
    await self._http.delete(f"{self._base_path}/eapol/x509/keystore")
delete_remote(remote_id) async

BLOCKED: Deletes a remote connection, severing the management tunnel.

Source code in src/xovis/api/device/resources/network.py
149
150
151
async def delete_remote(self, remote_id: int) -> None:
    """BLOCKED: Deletes a remote connection, severing the management tunnel."""
    await self._http.delete(f"{self._base_path}/remotes/{remote_id}")
delete_truststore_cert(fingerprint) async

Removes a specific X.509 certificate from the truststore.

Source code in src/xovis/api/device/resources/network.py
265
266
267
async def delete_truststore_cert(self, fingerprint: str) -> None:
    """Removes a specific X.509 certificate from the truststore."""
    await self._http.delete(f"{self._base_path}/x509/truststore/{fingerprint}")
delete_wireless_network(network_id) async

Removes a specific wireless network profile.

Source code in src/xovis/api/device/resources/network.py
315
316
317
318
async def delete_wireless_network(self, network_id: int) -> Any:
    """Removes a specific wireless network profile."""
    response = await self._http.delete(f"{self._base_path}/wireless/networks/{network_id}")
    return self.models.WlanNetwork.model_validate(response.json())
get_all_remotes() async

Retrieves configurations for all defined remote connections (Cloud Tunnels).

Source code in src/xovis/api/device/resources/network.py
101
102
103
104
105
106
107
108
109
110
111
async def get_all_remotes(self) -> Any:
    """Retrieves configurations for all defined remote connections (Cloud Tunnels)."""
    response = await self._http.get(f"{self._base_path}/remotes")
    # We use a custom parser to handle relative URLs in the 'uri' field,
    # which Pydantic's AnyUrl strictly rejects even in non-strict mode.
    data = response.json()
    if "remotes" in data:
        for remote in data["remotes"]:
            if "uri" in remote and isinstance(remote["uri"], str) and remote["uri"].startswith("/"):
                remote["uri"] = f"http://localhost{remote['uri']}"
    return self.models.RemoteConnections.model_validate(data)
get_all_remotes_state() async

Retrieves the connectivity status for all remote tunnels.

Source code in src/xovis/api/device/resources/network.py
153
154
155
156
async def get_all_remotes_state(self) -> Any:
    """Retrieves the connectivity status for all remote tunnels."""
    response = await self._http.get(f"{self._base_path}/remotes/state")
    return self.models.RemoteConnectionStates.model_validate(response.json())
get_eapol_config() async

Retrieves the 802.1X EAPoL Supplicant configuration.

Source code in src/xovis/api/device/resources/network.py
194
195
196
197
async def get_eapol_config(self) -> Any:
    """Retrieves the 802.1X EAPoL Supplicant configuration."""
    response = await self._http.get(f"{self._base_path}/eapol/config")
    return self.models.EapolConfig.model_validate(response.json())
get_eapol_keystore() async

Retrieves the configured X.509 client identity for 802.1X.

Source code in src/xovis/api/device/resources/network.py
214
215
216
217
218
219
async def get_eapol_keystore(self) -> Any:
    """Retrieves the configured X.509 client identity for 802.1X."""
    response = await self._http.get(f"{self._base_path}/eapol/x509/keystore")
    if response.status_code == 204:
        return None
    return self.models.X509Certificate.model_validate(response.json())
get_eapol_state() async

Retrieves the authentication state of the 802.1X Supplicant.

Source code in src/xovis/api/device/resources/network.py
209
210
211
212
async def get_eapol_state(self) -> Any:
    """Retrieves the authentication state of the 802.1X Supplicant."""
    response = await self._http.get(f"{self._base_path}/eapol/state")
    return self.models.EapolState.model_validate(response.json())
get_gigabit() async

Retrieves the Gigabit Ethernet enablement capability.

Source code in src/xovis/api/device/resources/network.py
64
65
66
67
async def get_gigabit(self) -> Any:
    """Retrieves the Gigabit Ethernet enablement capability."""
    response = await self._http.get(f"{self._base_path}/gigabit")
    return self.models.GigabitEnabled.model_validate(response.json())
get_hostname() async

Retrieves the current hostname configuration.

Source code in src/xovis/api/device/resources/network.py
51
52
53
54
async def get_hostname(self) -> Any:
    """Retrieves the current hostname configuration."""
    response = await self._http.get(f"{self._base_path}/hostname")
    return self.models.Hostname.model_validate(response.json())
get_ipv4() async

Retrieves the current IPv4 network configuration (Static/DHCP).

Source code in src/xovis/api/device/resources/network.py
74
75
76
77
async def get_ipv4(self) -> Any:
    """Retrieves the current IPv4 network configuration (Static/DHCP)."""
    response = await self._http.get(f"{self._base_path}/ipv4")
    return self.models.NetworkIpv4Settings.model_validate(response.json())
get_ipv6() async

Retrieves the current IPv6 network configuration.

Source code in src/xovis/api/device/resources/network.py
87
88
89
90
async def get_ipv6(self) -> Any:
    """Retrieves the current IPv6 network configuration."""
    response = await self._http.get(f"{self._base_path}/ipv6")
    return self.models.NetworkIpv6Settings.model_validate(response.json())
get_mdns_config() async

Retrieves the mDNS (Multicast DNS) configuration.

Source code in src/xovis/api/device/resources/network.py
352
353
354
355
async def get_mdns_config(self) -> Any:
    """Retrieves the mDNS (Multicast DNS) configuration."""
    response = await self._http.get(f"{self._base_path}/mdns/config")
    return self.models.MdnsConfig.model_validate(response.json())
get_mdns_state() async

Retrieves the currently discovered mDNS services in the local network.

Source code in src/xovis/api/device/resources/network.py
361
362
363
364
async def get_mdns_state(self) -> Any:
    """Retrieves the currently discovered mDNS services in the local network."""
    response = await self._http.get(f"{self._base_path}/mdns/state")
    return self.models.MdnsState.model_validate(response.json())
get_pip_config() async

Retrieves the Xovis PIP (Product Improvement Program) configuration.

Source code in src/xovis/api/device/resources/network.py
367
368
369
370
async def get_pip_config(self) -> Any:
    """Retrieves the Xovis PIP (Product Improvement Program) configuration."""
    response = await self._http.get(f"{self._base_path}/pip")
    return self.models.PipSettings.model_validate(response.json())
get_pip_state() async

Retrieves the state and quota usage of the Xovis PIP service.

Source code in src/xovis/api/device/resources/network.py
376
377
378
379
async def get_pip_state(self) -> Any:
    """Retrieves the state and quota usage of the Xovis PIP service."""
    response = await self._http.get(f"{self._base_path}/pip/status")
    return self.models.PipState.model_validate(response.json())
get_proxy() async

Retrieves the current network proxy configuration.

Source code in src/xovis/api/device/resources/network.py
339
340
341
342
async def get_proxy(self) -> Any:
    """Retrieves the current network proxy configuration."""
    response = await self._http.get(f"{self._base_path}/proxy")
    return self.models.NetworkProxy.model_validate(response.json())
get_remote(remote_id) async

Retrieves a specific remote connection configuration.

Source code in src/xovis/api/device/resources/network.py
132
133
134
135
136
137
138
async def get_remote(self, remote_id: int) -> Any:
    """Retrieves a specific remote connection configuration."""
    response = await self._http.get(f"{self._base_path}/remotes/{remote_id}")
    data = response.json()
    if "uri" in data and isinstance(data["uri"], str) and data["uri"].startswith("/"):
        data["uri"] = f"http://localhost{data['uri']}"
    return self.models.IndexedRemoteConnection.model_validate(data)
get_remote_services_config() async

Retrieves the overarching configuration for Xovis Remote Services (IoT).

Source code in src/xovis/api/device/resources/network.py
174
175
176
177
async def get_remote_services_config(self) -> Any:
    """Retrieves the overarching configuration for Xovis Remote Services (IoT)."""
    response = await self._http.get(f"{self._base_path}/remotes/services/config")
    return self.models.RemoteServicesSettings.model_validate(response.json())
get_remote_services_state() async

Retrieves the connectivity status of Xovis Remote Services.

Source code in src/xovis/api/device/resources/network.py
188
189
190
191
async def get_remote_services_state(self) -> Any:
    """Retrieves the connectivity status of Xovis Remote Services."""
    response = await self._http.get(f"{self._base_path}/remotes/services/state")
    return self.models.RemoteServicesState.model_validate(response.json())
get_remote_state(remote_id) async

Retrieves the connectivity status of a specific remote tunnel.

Source code in src/xovis/api/device/resources/network.py
158
159
160
161
async def get_remote_state(self, remote_id: int) -> Any:
    """Retrieves the connectivity status of a specific remote tunnel."""
    response = await self._http.get(f"{self._base_path}/remotes/{remote_id}/state")
    return self.models.RemoteConnectionState.model_validate(response.json())
get_state() async

Retrieves the comprehensive operational state of all network interfaces.

Source code in src/xovis/api/device/resources/network.py
46
47
48
49
async def get_state(self) -> Any:
    """Retrieves the comprehensive operational state of all network interfaces."""
    response = await self._http.get(f"{self._base_path}/state")
    return self.models.NetworkState.model_validate(response.json())
get_truststore_cert(fingerprint) async

Retrieves details of a specific X.509 certificate by its SHA-1 fingerprint.

Source code in src/xovis/api/device/resources/network.py
260
261
262
263
async def get_truststore_cert(self, fingerprint: str) -> Any:
    """Retrieves details of a specific X.509 certificate by its SHA-1 fingerprint."""
    response = await self._http.get(f"{self._base_path}/x509/truststore/{fingerprint}")
    return self.models.X509Certificate.model_validate(response.json())
get_truststore_certs() async

Retrieves all installed X.509 certificates in the truststore.

Source code in src/xovis/api/device/resources/network.py
235
236
237
238
async def get_truststore_certs(self) -> Any:
    """Retrieves all installed X.509 certificates in the truststore."""
    response = await self._http.get(f"{self._base_path}/x509/truststore")
    return self.models.X509Certificates.model_validate(response.json())
get_truststore_config() async

Retrieves the truststore configuration (e.g., use defaults).

Source code in src/xovis/api/device/resources/network.py
250
251
252
253
async def get_truststore_config(self) -> Any:
    """Retrieves the truststore configuration (e.g., use defaults)."""
    response = await self._http.get(f"{self._base_path}/x509/truststore/config")
    return self.models.X509TruststoreConfig.model_validate(response.json())
get_wireless() async

Retrieves the global wireless networking configuration.

Source code in src/xovis/api/device/resources/network.py
270
271
272
273
async def get_wireless(self) -> Any:
    """Retrieves the global wireless networking configuration."""
    response = await self._http.get(f"{self._base_path}/wireless")
    return self.models.WlanSettings.model_validate(response.json())
get_wireless_network(network_id) async

Retrieves a specific wireless network profile.

Source code in src/xovis/api/device/resources/network.py
305
306
307
308
async def get_wireless_network(self, network_id: int) -> Any:
    """Retrieves a specific wireless network profile."""
    response = await self._http.get(f"{self._base_path}/wireless/networks/{network_id}")
    return self.models.WlanNetwork.model_validate(response.json())
get_wireless_networks() async

Retrieves all configured wireless network profiles.

Source code in src/xovis/api/device/resources/network.py
290
291
292
293
async def get_wireless_networks(self) -> Any:
    """Retrieves all configured wireless network profiles."""
    response = await self._http.get(f"{self._base_path}/wireless/networks")
    return self.models.WlanNetworks.model_validate(response.json())
get_wireless_scan_results() async

Retrieves the results of the latest wireless network scan.

Source code in src/xovis/api/device/resources/network.py
333
334
335
336
async def get_wireless_scan_results(self) -> Any:
    """Retrieves the results of the latest wireless network scan."""
    response = await self._http.get(f"{self._base_path}/wireless/scan")
    return self.models.ScanResults.model_validate(response.json())
get_wireless_state() async

Retrieves the current state and connection metrics of wireless networking.

Source code in src/xovis/api/device/resources/network.py
285
286
287
288
async def get_wireless_state(self) -> Any:
    """Retrieves the current state and connection metrics of wireless networking."""
    response = await self._http.get(f"{self._base_path}/wireless/state")
    return self.models.StateResult.model_validate(response.json())
get_wireless_test_result(network_id) async

Retrieves the result of a wireless connection test.

Source code in src/xovis/api/device/resources/network.py
324
325
326
327
async def get_wireless_test_result(self, network_id: int) -> Any:
    """Retrieves the result of a wireless connection test."""
    response = await self._http.get(f"{self._base_path}/wireless/networks/{network_id}/test")
    return self.models.ScanResult.model_validate(response.json())
get_xovis_support_state() async

Retrieves the state of the Xovis Tier 3 Support Remote Connection.

Source code in src/xovis/api/device/resources/network.py
164
165
166
167
async def get_xovis_support_state(self) -> Any:
    """Retrieves the state of the Xovis Tier 3 Support Remote Connection."""
    response = await self._http.get(f"{self._base_path}/remotes/xovissupport")
    return self.models.XovisRemoteSupportState.model_validate(response.json())
install_truststore_cert(cert_binary) async

Installs a new X.509 certificate to the truststore.

Source code in src/xovis/api/device/resources/network.py
240
241
242
243
244
async def install_truststore_cert(self, cert_binary: bytes) -> Any:
    """Installs a new X.509 certificate to the truststore."""
    headers = {"Content-Type": "application/octet-stream"}
    response = await self._http.post(f"{self._base_path}/x509/truststore", content=cert_binary, headers=headers)
    return self.models.X509Certificate.model_validate(response.json())
reset_eapol_config() async

CRITICAL: Resets 802.1X configuration to factory defaults.

Source code in src/xovis/api/device/resources/network.py
204
205
206
207
async def reset_eapol_config(self) -> Any:
    """CRITICAL: Resets 802.1X configuration to factory defaults."""
    response = await self._http.delete(f"{self._base_path}/eapol/config")
    return self.models.EapolConfig.model_validate(response.json())
reset_hostname() async

Resets the hostname to the factory default (XS-SENSOR-[MAC]).

Source code in src/xovis/api/device/resources/network.py
60
61
62
async def reset_hostname(self) -> None:
    """Resets the hostname to the factory default (XS-SENSOR-[MAC])."""
    await self._http.delete(f"{self._base_path}/hostname")
reset_ipv4() async

CRITICAL: Resets IPv4 to factory defaults (DHCP enabled).

Source code in src/xovis/api/device/resources/network.py
83
84
85
async def reset_ipv4(self) -> None:
    """CRITICAL: Resets IPv4 to factory defaults (DHCP enabled)."""
    await self._http.delete(f"{self._base_path}/ipv4")
reset_ipv6() async

CRITICAL: Resets IPv6 to factory defaults (Disabled).

Source code in src/xovis/api/device/resources/network.py
96
97
98
async def reset_ipv6(self) -> None:
    """CRITICAL: Resets IPv6 to factory defaults (Disabled)."""
    await self._http.delete(f"{self._base_path}/ipv6")
reset_pip_quota() async

Resets the monthly used PIP quota.

Source code in src/xovis/api/device/resources/network.py
381
382
383
async def reset_pip_quota(self) -> None:
    """Resets the monthly used PIP quota."""
    await self._http.post(f"{self._base_path}/pip_quota_reset")
reset_proxy() async

Deletes the current proxy configuration.

Source code in src/xovis/api/device/resources/network.py
348
349
350
async def reset_proxy(self) -> None:
    """Deletes the current proxy configuration."""
    await self._http.delete(f"{self._base_path}/proxy")
reset_remote_services_config() async

Resets the Xovis Remote Services configuration.

Source code in src/xovis/api/device/resources/network.py
184
185
186
async def reset_remote_services_config(self) -> None:
    """Resets the Xovis Remote Services configuration."""
    await self._http.delete(f"{self._base_path}/remotes/services/config")
reset_truststore() async

Resets the truststore to factory default certificates.

Source code in src/xovis/api/device/resources/network.py
246
247
248
async def reset_truststore(self) -> None:
    """Resets the truststore to factory default certificates."""
    await self._http.delete(f"{self._base_path}/x509/truststore")
reset_wireless() async

Resets global wireless networking configuration to factory defaults.

Source code in src/xovis/api/device/resources/network.py
280
281
282
283
async def reset_wireless(self) -> Any:
    """Resets global wireless networking configuration to factory defaults."""
    response = await self._http.delete(f"{self._base_path}/wireless")
    return self.models.WlanSettings.model_validate(response.json())
test_wireless_network(network_id) async

Triggers a connection test for a specific wireless profile.

Source code in src/xovis/api/device/resources/network.py
320
321
322
async def test_wireless_network(self, network_id: int) -> None:
    """Triggers a connection test for a specific wireless profile."""
    await self._http.post(f"{self._base_path}/wireless/networks/{network_id}/test")
trigger_wireless_scan() async

Triggers an active scan for nearby IEEE 802.11 Access Points.

Source code in src/xovis/api/device/resources/network.py
329
330
331
async def trigger_wireless_scan(self) -> None:
    """Triggers an active scan for nearby IEEE 802.11 Access Points."""
    await self._http.post(f"{self._base_path}/wireless/scan")
update_eapol_config(config) async

CRITICAL: Updates 802.1X configuration. May cause network drops.

Source code in src/xovis/api/device/resources/network.py
199
200
201
202
async def update_eapol_config(self, config: Any) -> Any:
    """CRITICAL: Updates 802.1X configuration. May cause network drops."""
    response = await self._http.put(f"{self._base_path}/eapol/config", json=config)
    return self.models.EapolConfig.model_validate(response.json())
update_eapol_keystore(pem_binary) async

Uploads a new X.509 client identity (Certificate + RSA Key). Must be formatted as PEM. Uses strict binary streaming.

Source code in src/xovis/api/device/resources/network.py
221
222
223
224
225
226
227
228
async def update_eapol_keystore(self, pem_binary: bytes) -> Any:
    """
    Uploads a new X.509 client identity (Certificate + RSA Key).
    Must be formatted as PEM. Uses strict binary streaming.
    """
    headers = {"Content-Type": "application/octet-stream"}
    response = await self._http.put(f"{self._base_path}/eapol/x509/keystore", content=pem_binary, headers=headers)
    return self.models.X509Certificate.model_validate(response.json())
update_gigabit(gigabit) async

Enables or disables Gigabit Ethernet capability.

Source code in src/xovis/api/device/resources/network.py
69
70
71
async def update_gigabit(self, gigabit: Any) -> None:
    """Enables or disables Gigabit Ethernet capability."""
    await self._http.put(f"{self._base_path}/gigabit", json=gigabit)
update_hostname(hostname) async

Updates the sensor's logical hostname.

Source code in src/xovis/api/device/resources/network.py
56
57
58
async def update_hostname(self, hostname: Any) -> None:
    """Updates the sensor's logical hostname."""
    await self._http.put(f"{self._base_path}/hostname", json=hostname)
update_ipv4(settings) async

CRITICAL: Updates IPv4 configuration. May sever active connections.

Source code in src/xovis/api/device/resources/network.py
79
80
81
async def update_ipv4(self, settings: Any) -> None:
    """CRITICAL: Updates IPv4 configuration. May sever active connections."""
    await self._http.put(f"{self._base_path}/ipv4", json=settings)
update_ipv6(settings) async

CRITICAL: Updates IPv6 configuration.

Source code in src/xovis/api/device/resources/network.py
92
93
94
async def update_ipv6(self, settings: Any) -> None:
    """CRITICAL: Updates IPv6 configuration."""
    await self._http.put(f"{self._base_path}/ipv6", json=settings)
update_mdns_config(config) async

Updates the mDNS configuration.

Source code in src/xovis/api/device/resources/network.py
357
358
359
async def update_mdns_config(self, config: Any) -> None:
    """Updates the mDNS configuration."""
    await self._http.put(f"{self._base_path}/mdns/config", json=config)
update_pip_config(settings) async

Updates the Xovis PIP configuration.

Source code in src/xovis/api/device/resources/network.py
372
373
374
async def update_pip_config(self, settings: Any) -> None:
    """Updates the Xovis PIP configuration."""
    await self._http.put(f"{self._base_path}/pip", json=settings)
update_proxy(proxy) async

Updates the network proxy configuration.

Source code in src/xovis/api/device/resources/network.py
344
345
346
async def update_proxy(self, proxy: Any) -> None:
    """Updates the network proxy configuration."""
    await self._http.put(f"{self._base_path}/proxy", json=proxy)
update_remote(remote_id, remote) async

Updates an existing remote connection.

Source code in src/xovis/api/device/resources/network.py
140
141
142
143
144
145
146
147
async def update_remote(self, remote_id: int, remote: Any) -> Any:
    """Updates an existing remote connection."""
    payload = remote.model_dump(by_alias=True, exclude_unset=True, mode="json")
    response = await self._http.put(f"{self._base_path}/remotes/{remote_id}", json=payload)
    data = response.json()
    if "uri" in data and isinstance(data["uri"], str) and data["uri"].startswith("/"):
        data["uri"] = f"http://localhost{data['uri']}"
    return self.models.IndexedRemoteConnection.model_validate(data)
update_remote_services_config(settings) async

Updates the Xovis Remote Services configuration.

Source code in src/xovis/api/device/resources/network.py
179
180
181
182
async def update_remote_services_config(self, settings: Any) -> None:
    """Updates the Xovis Remote Services configuration."""
    payload = settings.model_dump(by_alias=True, exclude_unset=True, mode="json")
    await self._http.put(f"{self._base_path}/remotes/services/config", json=payload)
update_truststore_config(config) async

Updates the truststore configuration.

Source code in src/xovis/api/device/resources/network.py
255
256
257
258
async def update_truststore_config(self, config: Any) -> Any:
    """Updates the truststore configuration."""
    response = await self._http.put(f"{self._base_path}/x509/truststore/config", json=config)
    return self.models.X509TruststoreConfig.model_validate(response.json())
update_wireless(settings) async

Updates the global wireless networking configuration.

Source code in src/xovis/api/device/resources/network.py
275
276
277
278
async def update_wireless(self, settings: Any) -> Any:
    """Updates the global wireless networking configuration."""
    response = await self._http.put(f"{self._base_path}/wireless", json=settings)
    return self.models.WlanSettings.model_validate(response.json())
update_wireless_network(network_id, network) async

Updates a specific wireless network profile.

Source code in src/xovis/api/device/resources/network.py
310
311
312
313
async def update_wireless_network(self, network_id: int, network: Any) -> Any:
    """Updates a specific wireless network profile."""
    response = await self._http.put(f"{self._base_path}/wireless/networks/{network_id}", json=network)
    return self.models.WlanNetwork.model_validate(response.json())
update_xovis_support(ctrl) async

Controls the enablement of the Xovis Support Remote Connection.

Source code in src/xovis/api/device/resources/network.py
169
170
171
172
async def update_xovis_support(self, ctrl: Any) -> None:
    """Controls the enablement of the Xovis Support Remote Connection."""
    payload = ctrl.model_dump(by_alias=True, exclude_unset=True, mode="json")
    await self._http.put(f"{self._base_path}/remotes/xovissupport", json=payload)

xovis.api.device.resources.privacy

Xovis SDK - Privacy Management Resource

Provides the implementation for managing logical privacy modes and RF-based (Wi-Fi/Bluetooth) monitoring configurations on local edge sensors. Operates within the Control Plane.

Classes

PrivacyManager

Manages privacy modes and RF monitoring settings on a Xovis device.

This manager provides control over the sensor's privacy levels (e.g., blurring, counting only) and the anonymization of detected Wi-Fi/Bluetooth devices.

Source code in src/xovis/api/device/resources/privacy.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
class PrivacyManager:
    """
    Manages privacy modes and RF monitoring settings on a Xovis device.

    This manager provides control over the sensor's privacy levels (e.g., blurring,
    counting only) and the anonymization of detected Wi-Fi/Bluetooth devices.
    """

    def __init__(
        self,
        http_client: XovisHTTPClient,
        client: "DeviceClient" = None,
        target_id: Optional[str] = None,
    ) -> None:
        """
        Initializes the PrivacyManager.

        Args:
            http_client (XovisHTTPClient): The resilient HTTP client.
            client (DeviceClient): The parent DeviceClient instance.
            target_id (Optional[str]): The multisensor target ID, if applicable.
        """
        self._http = http_client
        self._client = client
        self.target_id = target_id
        self._base_path = "/api/v5/privacy"

    @property
    def models(self):
        """Returns the appropriate Pydantic models for the current device firmware."""
        return self._client.models if self._client else stable_models

    def _resolve_mode_path(self) -> str:
        """Resolves the base API path for privacy modes."""
        if self.target_id:
            return f"/api/v5/multisensors/{self.target_id}/settings/privacy"
        return "/api/v5/privacy"

    def _resolve_rf_path(self) -> str:
        """Resolves the base API path for RF privacy."""
        if self.target_id:
            return f"/api/v5/multisensors/{self.target_id}/settings/rf/privacy"
        return "/api/v5/rf/privacy"

    # --- Privacy Mode ---
    async def get_privacy_mode(self) -> Any:
        """
        Retrieves the current logical privacy mode.

        Returns:
            PrivacyMode: The current privacy mode settings.
        """
        path = f"{self._resolve_mode_path()}/mode" if not self.target_id else self._resolve_mode_path()
        response = await self._http.get(path)
        return self.models.PrivacyMode.model_validate(response.json())

    async def update_privacy_mode(self, mode: Any) -> Any:
        """
        Updates the sensor's privacy mode.

        Args:
            mode (PrivacyMode): The new privacy mode configuration.

        Returns:
            PrivacyMode: The updated privacy mode settings.
        """
        response = await self._http.put(f"{self._resolve_mode_path()}/mode", json=mode)
        return self.models.PrivacyMode.model_validate(response.json())

    async def reset_privacy_mode(self) -> None:
        """Resets the privacy mode to factory defaults."""
        await self._http.delete(f"{self._base_path}/mode")

    # --- RF Privacy Settings ---
    async def get_rf_privacy(self) -> Any:
        """
        Retrieves the current RF privacy (hashing/anonymization) configuration.

        Returns:
            PrivacySettings: The current RF privacy settings.
        """
        response = await self._http.get("/api/v5/rf/privacy")
        return self.models.PrivacySettings.model_validate(response.json())

    async def update_rf_privacy(self, settings: Any) -> Any:
        """
        Updates the RF privacy configuration.

        Args:
            settings (PrivacySettings): The new RF privacy settings.

        Returns:
            PrivacySettings: The updated RF privacy settings.
        """
        response = await self._http.put("/api/v5/rf/privacy", json=settings)
        return self.models.PrivacySettings.model_validate(response.json())

    async def reset_rf_privacy(self) -> Any:
        """
        Resets the RF privacy configuration to factory defaults.

        Returns:
            PrivacySettings: The default RF privacy settings.
        """
        response = await self._http.delete("/api/v5/rf/privacy")
        return self.models.PrivacySettings.model_validate(response.json())

    async def get_rf_salt(self) -> Any:
        """
        Retrieves the current hashing salt used for RF anonymization.

        Returns:
            PrivacySaltSettings: The current hashing salt.
        """
        response = await self._http.get("/api/v5/rf/privacy/salt")
        return self.models.PrivacySaltSettings.model_validate(response.json())

    async def update_rf_salt(self, settings: Any) -> Any:
        """
        Updates the hashing salt for RF anonymization.

        Args:
            settings (PrivacySaltSettings): The new hashing salt.

        Returns:
            PrivacySaltSettings: The updated hashing salt.
        """
        response = await self._http.put("/api/v5/rf/privacy/salt", json=settings)
        return self.models.PrivacySaltSettings.model_validate(response.json())

    async def reset_rf_salt(self) -> Any:
        """
        Resets the RF salt, triggering the generation of a new random salt.

        Returns:
            PrivacySaltSettings: The newly generated salt settings.
        """
        response = await self._http.delete("/api/v5/rf/privacy/salt")
        return self.models.PrivacySaltSettings.model_validate(response.json())

    # --- Bluetooth Settings ---
    async def get_bluetooth(self) -> Any:
        """
        Retrieves the current Bluetooth monitoring configuration.

        Returns:
            BluetoothSettings: The current Bluetooth settings.
        """
        response = await self._http.get("/api/v5/rf/bluetooth")
        return self.models.BluetoothSettings.model_validate(response.json())

    async def update_bluetooth(self, settings: Any) -> Any:
        """
        Updates the Bluetooth monitoring configuration.

        Args:
            settings (BluetoothSettings): The new Bluetooth configuration.

        Returns:
            BluetoothSettings: The updated Bluetooth settings.
        """
        response = await self._http.put("/api/v5/rf/bluetooth", json=settings)
        return self.models.BluetoothSettings.model_validate(response.json())

    async def reset_bluetooth(self) -> Any:
        """
        Resets the Bluetooth monitoring configuration to factory defaults.

        Returns:
            BluetoothSettings: The default Bluetooth settings.
        """
        response = await self._http.delete("/api/v5/rf/bluetooth")
        return self.models.BluetoothSettings.model_validate(response.json())

    # --- WiFi Settings ---
    async def get_wifi(self) -> Any:
        """
        Retrieves the current Wi-Fi monitoring configuration.

        Returns:
            WifiSettings: The current Wi-Fi settings.
        """
        response = await self._http.get("/api/v5/rf/wifi")
        return self.models.WifiSettings.model_validate(response.json())

    async def update_wifi(self, settings: Any) -> Any:
        """
        Updates the Wi-Fi monitoring configuration.

        Args:
            settings (WifiSettings): The new Wi-Fi configuration.

        Returns:
            WifiSettings: The updated Wi-Fi settings.
        """
        response = await self._http.put("/api/v5/rf/wifi", json=settings)
        return self.models.WifiSettings.model_validate(response.json())

    async def reset_wifi(self) -> Any:
        """
        Resets the Wi-Fi monitoring configuration to factory defaults.

        Returns:
            WifiSettings: The default Wi-Fi settings.
        """
        response = await self._http.delete("/api/v5/rf/wifi")
        return self.models.WifiSettings.model_validate(response.json())

    # --- Detected Devices ---
    async def get_devices(self) -> Any:
        """
        Retrieves the list of devices detected in the last 5 seconds.

        Returns:
            DeviceIdList: A list of detected device identifiers.
        """
        response = await self._http.get("/api/v5/rf/devices")
        return self.models.DeviceIdList.model_validate(response.json())

    async def get_devices_summary(self) -> str:
        """
        Retrieves a human-readable summary of devices detected in the last 5 seconds.

        Returns:
            str: A formatted list of detected devices.
        """
        response = await self._http.get("/api/v5/rf/devices/summary")
        return response.text
Attributes
models property

Returns the appropriate Pydantic models for the current device firmware.

Methods:
__init__(http_client, client=None, target_id=None)

Initializes the PrivacyManager.

Parameters:

Name Type Description Default
http_client XovisHTTPClient

The resilient HTTP client.

required
client DeviceClient

The parent DeviceClient instance.

None
target_id Optional[str]

The multisensor target ID, if applicable.

None
Source code in src/xovis/api/device/resources/privacy.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __init__(
    self,
    http_client: XovisHTTPClient,
    client: "DeviceClient" = None,
    target_id: Optional[str] = None,
) -> None:
    """
    Initializes the PrivacyManager.

    Args:
        http_client (XovisHTTPClient): The resilient HTTP client.
        client (DeviceClient): The parent DeviceClient instance.
        target_id (Optional[str]): The multisensor target ID, if applicable.
    """
    self._http = http_client
    self._client = client
    self.target_id = target_id
    self._base_path = "/api/v5/privacy"
_resolve_mode_path()

Resolves the base API path for privacy modes.

Source code in src/xovis/api/device/resources/privacy.py
50
51
52
53
54
def _resolve_mode_path(self) -> str:
    """Resolves the base API path for privacy modes."""
    if self.target_id:
        return f"/api/v5/multisensors/{self.target_id}/settings/privacy"
    return "/api/v5/privacy"
_resolve_rf_path()

Resolves the base API path for RF privacy.

Source code in src/xovis/api/device/resources/privacy.py
56
57
58
59
60
def _resolve_rf_path(self) -> str:
    """Resolves the base API path for RF privacy."""
    if self.target_id:
        return f"/api/v5/multisensors/{self.target_id}/settings/rf/privacy"
    return "/api/v5/rf/privacy"
get_bluetooth() async

Retrieves the current Bluetooth monitoring configuration.

Returns:

Name Type Description
BluetoothSettings Any

The current Bluetooth settings.

Source code in src/xovis/api/device/resources/privacy.py
159
160
161
162
163
164
165
166
167
async def get_bluetooth(self) -> Any:
    """
    Retrieves the current Bluetooth monitoring configuration.

    Returns:
        BluetoothSettings: The current Bluetooth settings.
    """
    response = await self._http.get("/api/v5/rf/bluetooth")
    return self.models.BluetoothSettings.model_validate(response.json())
get_devices() async

Retrieves the list of devices detected in the last 5 seconds.

Returns:

Name Type Description
DeviceIdList Any

A list of detected device identifiers.

Source code in src/xovis/api/device/resources/privacy.py
227
228
229
230
231
232
233
234
235
async def get_devices(self) -> Any:
    """
    Retrieves the list of devices detected in the last 5 seconds.

    Returns:
        DeviceIdList: A list of detected device identifiers.
    """
    response = await self._http.get("/api/v5/rf/devices")
    return self.models.DeviceIdList.model_validate(response.json())
get_devices_summary() async

Retrieves a human-readable summary of devices detected in the last 5 seconds.

Returns:

Name Type Description
str str

A formatted list of detected devices.

Source code in src/xovis/api/device/resources/privacy.py
237
238
239
240
241
242
243
244
245
async def get_devices_summary(self) -> str:
    """
    Retrieves a human-readable summary of devices detected in the last 5 seconds.

    Returns:
        str: A formatted list of detected devices.
    """
    response = await self._http.get("/api/v5/rf/devices/summary")
    return response.text
get_privacy_mode() async

Retrieves the current logical privacy mode.

Returns:

Name Type Description
PrivacyMode Any

The current privacy mode settings.

Source code in src/xovis/api/device/resources/privacy.py
63
64
65
66
67
68
69
70
71
72
async def get_privacy_mode(self) -> Any:
    """
    Retrieves the current logical privacy mode.

    Returns:
        PrivacyMode: The current privacy mode settings.
    """
    path = f"{self._resolve_mode_path()}/mode" if not self.target_id else self._resolve_mode_path()
    response = await self._http.get(path)
    return self.models.PrivacyMode.model_validate(response.json())
get_rf_privacy() async

Retrieves the current RF privacy (hashing/anonymization) configuration.

Returns:

Name Type Description
PrivacySettings Any

The current RF privacy settings.

Source code in src/xovis/api/device/resources/privacy.py
 92
 93
 94
 95
 96
 97
 98
 99
100
async def get_rf_privacy(self) -> Any:
    """
    Retrieves the current RF privacy (hashing/anonymization) configuration.

    Returns:
        PrivacySettings: The current RF privacy settings.
    """
    response = await self._http.get("/api/v5/rf/privacy")
    return self.models.PrivacySettings.model_validate(response.json())
get_rf_salt() async

Retrieves the current hashing salt used for RF anonymization.

Returns:

Name Type Description
PrivacySaltSettings Any

The current hashing salt.

Source code in src/xovis/api/device/resources/privacy.py
125
126
127
128
129
130
131
132
133
async def get_rf_salt(self) -> Any:
    """
    Retrieves the current hashing salt used for RF anonymization.

    Returns:
        PrivacySaltSettings: The current hashing salt.
    """
    response = await self._http.get("/api/v5/rf/privacy/salt")
    return self.models.PrivacySaltSettings.model_validate(response.json())
get_wifi() async

Retrieves the current Wi-Fi monitoring configuration.

Returns:

Name Type Description
WifiSettings Any

The current Wi-Fi settings.

Source code in src/xovis/api/device/resources/privacy.py
193
194
195
196
197
198
199
200
201
async def get_wifi(self) -> Any:
    """
    Retrieves the current Wi-Fi monitoring configuration.

    Returns:
        WifiSettings: The current Wi-Fi settings.
    """
    response = await self._http.get("/api/v5/rf/wifi")
    return self.models.WifiSettings.model_validate(response.json())
reset_bluetooth() async

Resets the Bluetooth monitoring configuration to factory defaults.

Returns:

Name Type Description
BluetoothSettings Any

The default Bluetooth settings.

Source code in src/xovis/api/device/resources/privacy.py
182
183
184
185
186
187
188
189
190
async def reset_bluetooth(self) -> Any:
    """
    Resets the Bluetooth monitoring configuration to factory defaults.

    Returns:
        BluetoothSettings: The default Bluetooth settings.
    """
    response = await self._http.delete("/api/v5/rf/bluetooth")
    return self.models.BluetoothSettings.model_validate(response.json())
reset_privacy_mode() async

Resets the privacy mode to factory defaults.

Source code in src/xovis/api/device/resources/privacy.py
87
88
89
async def reset_privacy_mode(self) -> None:
    """Resets the privacy mode to factory defaults."""
    await self._http.delete(f"{self._base_path}/mode")
reset_rf_privacy() async

Resets the RF privacy configuration to factory defaults.

Returns:

Name Type Description
PrivacySettings Any

The default RF privacy settings.

Source code in src/xovis/api/device/resources/privacy.py
115
116
117
118
119
120
121
122
123
async def reset_rf_privacy(self) -> Any:
    """
    Resets the RF privacy configuration to factory defaults.

    Returns:
        PrivacySettings: The default RF privacy settings.
    """
    response = await self._http.delete("/api/v5/rf/privacy")
    return self.models.PrivacySettings.model_validate(response.json())
reset_rf_salt() async

Resets the RF salt, triggering the generation of a new random salt.

Returns:

Name Type Description
PrivacySaltSettings Any

The newly generated salt settings.

Source code in src/xovis/api/device/resources/privacy.py
148
149
150
151
152
153
154
155
156
async def reset_rf_salt(self) -> Any:
    """
    Resets the RF salt, triggering the generation of a new random salt.

    Returns:
        PrivacySaltSettings: The newly generated salt settings.
    """
    response = await self._http.delete("/api/v5/rf/privacy/salt")
    return self.models.PrivacySaltSettings.model_validate(response.json())
reset_wifi() async

Resets the Wi-Fi monitoring configuration to factory defaults.

Returns:

Name Type Description
WifiSettings Any

The default Wi-Fi settings.

Source code in src/xovis/api/device/resources/privacy.py
216
217
218
219
220
221
222
223
224
async def reset_wifi(self) -> Any:
    """
    Resets the Wi-Fi monitoring configuration to factory defaults.

    Returns:
        WifiSettings: The default Wi-Fi settings.
    """
    response = await self._http.delete("/api/v5/rf/wifi")
    return self.models.WifiSettings.model_validate(response.json())
update_bluetooth(settings) async

Updates the Bluetooth monitoring configuration.

Parameters:

Name Type Description Default
settings BluetoothSettings

The new Bluetooth configuration.

required

Returns:

Name Type Description
BluetoothSettings Any

The updated Bluetooth settings.

Source code in src/xovis/api/device/resources/privacy.py
169
170
171
172
173
174
175
176
177
178
179
180
async def update_bluetooth(self, settings: Any) -> Any:
    """
    Updates the Bluetooth monitoring configuration.

    Args:
        settings (BluetoothSettings): The new Bluetooth configuration.

    Returns:
        BluetoothSettings: The updated Bluetooth settings.
    """
    response = await self._http.put("/api/v5/rf/bluetooth", json=settings)
    return self.models.BluetoothSettings.model_validate(response.json())
update_privacy_mode(mode) async

Updates the sensor's privacy mode.

Parameters:

Name Type Description Default
mode PrivacyMode

The new privacy mode configuration.

required

Returns:

Name Type Description
PrivacyMode Any

The updated privacy mode settings.

Source code in src/xovis/api/device/resources/privacy.py
74
75
76
77
78
79
80
81
82
83
84
85
async def update_privacy_mode(self, mode: Any) -> Any:
    """
    Updates the sensor's privacy mode.

    Args:
        mode (PrivacyMode): The new privacy mode configuration.

    Returns:
        PrivacyMode: The updated privacy mode settings.
    """
    response = await self._http.put(f"{self._resolve_mode_path()}/mode", json=mode)
    return self.models.PrivacyMode.model_validate(response.json())
update_rf_privacy(settings) async

Updates the RF privacy configuration.

Parameters:

Name Type Description Default
settings PrivacySettings

The new RF privacy settings.

required

Returns:

Name Type Description
PrivacySettings Any

The updated RF privacy settings.

Source code in src/xovis/api/device/resources/privacy.py
102
103
104
105
106
107
108
109
110
111
112
113
async def update_rf_privacy(self, settings: Any) -> Any:
    """
    Updates the RF privacy configuration.

    Args:
        settings (PrivacySettings): The new RF privacy settings.

    Returns:
        PrivacySettings: The updated RF privacy settings.
    """
    response = await self._http.put("/api/v5/rf/privacy", json=settings)
    return self.models.PrivacySettings.model_validate(response.json())
update_rf_salt(settings) async

Updates the hashing salt for RF anonymization.

Parameters:

Name Type Description Default
settings PrivacySaltSettings

The new hashing salt.

required

Returns:

Name Type Description
PrivacySaltSettings Any

The updated hashing salt.

Source code in src/xovis/api/device/resources/privacy.py
135
136
137
138
139
140
141
142
143
144
145
146
async def update_rf_salt(self, settings: Any) -> Any:
    """
    Updates the hashing salt for RF anonymization.

    Args:
        settings (PrivacySaltSettings): The new hashing salt.

    Returns:
        PrivacySaltSettings: The updated hashing salt.
    """
    response = await self._http.put("/api/v5/rf/privacy/salt", json=settings)
    return self.models.PrivacySaltSettings.model_validate(response.json())
update_wifi(settings) async

Updates the Wi-Fi monitoring configuration.

Parameters:

Name Type Description Default
settings WifiSettings

The new Wi-Fi configuration.

required

Returns:

Name Type Description
WifiSettings Any

The updated Wi-Fi settings.

Source code in src/xovis/api/device/resources/privacy.py
203
204
205
206
207
208
209
210
211
212
213
214
async def update_wifi(self, settings: Any) -> Any:
    """
    Updates the Wi-Fi monitoring configuration.

    Args:
        settings (WifiSettings): The new Wi-Fi configuration.

    Returns:
        WifiSettings: The updated Wi-Fi settings.
    """
    response = await self._http.put("/api/v5/rf/wifi", json=settings)
    return self.models.WifiSettings.model_validate(response.json())

xovis.api.device.resources.itxpt

Xovis SDK - ITxPT Management Resource

Operates within the Control Plane. Provides comprehensive implementation for managing Information Technology for Public Transport (ITxPT) configurations. Orchestrates Automatic Passenger Counting (APC) doors, Time Discovery (SNTP), mDNS TXT broadcasts, and extracts raw inventory/APC telemetry directly from the edge sensor.

Classes

ITxPTManager

Manages ITxPT configurations, networking discovery, and APC doors.

This manager orchestrates public transport services on the sensor. It allows autonomous agents to diagnose vehicle time synchronization (SNTP), configure mDNS broadcasts, and fetch real-time passenger counts directly from the ITxPT service endpoints.

Source code in src/xovis/api/device/resources/itxpt.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
class ITxPTManager:
    """
    Manages ITxPT configurations, networking discovery, and APC doors.

    This manager orchestrates public transport services on the sensor. It allows
    autonomous agents to diagnose vehicle time synchronization (SNTP), configure
    mDNS broadcasts, and fetch real-time passenger counts directly from the
    ITxPT service endpoints.
    """

    def __init__(self, http_client: XovisHTTPClient, client: "DeviceClient" = None) -> None:
        """
        Initializes the ITxPTManager.

        Args:
            http_client (XovisHTTPClient): The resilient HTTP client.
            client (DeviceClient): The parent DeviceClient instance.
        """
        self._http = http_client
        self._client = client
        self._base_path = "/api/v5/itxpt"

    @property
    def models(self):
        """Returns the strictly validated Pydantic models for the current firmware."""
        return self._client.models if self._client else stable_models

    # --- GLOBAL CONFIGURATION & STATE ---
    async def get_config(self) -> Any:
        """
        Retrieves the global ITxPT configuration (e.g., protocol version, vehicle ID source).
        """
        response = await self._http.get(f"{self._base_path}/config")
        return self.models.ItxptConfig.model_validate(response.json())

    async def update_config(self, config: Any) -> Any:
        """
        Updates the global ITxPT configuration.
        """
        response = await self._http.put(f"{self._base_path}/config", json=config)
        return self.models.ItxptConfig.model_validate(response.json())

    async def get_state(self) -> Any:
        """
        Retrieves the current ITxPT operational state and resolved Vehicle ID.
        """
        response = await self._http.get(f"{self._base_path}/state")
        return self.models.ItxptState.model_validate(response.json())

    async def get_services_state(self) -> Any:
        """
        Retrieves the runtime networking state of all ITxPT sub-services,
        including current consumers subscribed to the APC feed.
        """
        response = await self._http.get(f"{self._base_path}/services/state")
        return self.models.ItxptServicesState.model_validate(response.json())

    # --- TIME DISCOVERY (SNTP) ---
    async def get_time_config(self) -> Any:
        """
        Retrieves the ITxPT time discovery configuration (SNTP enable toggle).
        """
        response = await self._http.get(f"{self._base_path}/config/time")
        return self.models.ItxptTimeConfig.model_validate(response.json())

    async def update_time_config(self, config: Any) -> Any:
        """
        Updates the ITxPT time discovery configuration.
        """
        response = await self._http.put(f"{self._base_path}/config/time", json=config)
        return self.models.ItxptTimeConfig.model_validate(response.json())

    async def get_time_status(self) -> Any:
        """
        Retrieves the runtime status of the vehicle time discovery.
        Crucial for agents to verify if the sensor has found an SNTP server.
        """
        response = await self._http.get(f"{self._base_path}/time/sntp_server")
        return self.models.ItxptTime.model_validate(response.json())

    # --- mDNS TXT RECORDS ---
    async def get_txt_records(self) -> Any:
        """
        Retrieves the extra TXT records published in the ITxPT mDNS broadcast.
        """
        response = await self._http.get(f"{self._base_path}/config/txt")
        return self.models.ItxptConfigTxt.model_validate(response.json())

    async def update_txt_records(self, txt_records: Any) -> Any:
        """
        Updates the extra TXT records for the mDNS broadcast.
        """
        response = await self._http.put(f"{self._base_path}/config/txt", json=txt_records)
        return self.models.ItxptConfigTxt.model_validate(response.json())

    # --- CUSTOM CONFIGURATIONS (BLOB) ---
    async def get_custom_configurations(self) -> Any:
        """
        Retrieves custom free text configurations specific to ITxPT deployments.
        """
        response = await self._http.get("/api/v5/blob/store/itxpt_custom_configurations.json")
        return self.models.ItxptCustomConfigurations.model_validate(response.json())

    async def update_custom_configurations(self, custom_configs: Any) -> Any:
        """
        Updates the custom free text configurations blob.
        """
        await self._http.put("/api/v5/blob/store/itxpt_custom_configurations.json", json=custom_configs)

    # --- APC DOOR MANAGEMENT ---
    async def get_all_doors(self) -> Any:
        """Retrieves all defined ITxPT APC door configurations."""
        response = await self._http.get(f"{self._base_path}/config/doors")
        return self.models.ItxptConfigDoorCollection.model_validate(response.json())

    async def create_door(self, door: Any) -> Any:
        """Creates a new ITxPT APC door configuration."""
        response = await self._http.post(f"{self._base_path}/config/doors", json=door)
        return self.models.ItxptConfigDoorApcPathRequired.model_validate(response.json())

    async def delete_all_doors(self) -> None:
        """Deletes all existing ITxPT APC door configurations."""
        await self._http.delete(f"{self._base_path}/config/doors")

    async def get_door(self, door_id: int) -> Any:
        """Retrieves a specific APC door configuration."""
        response = await self._http.get(f"{self._base_path}/config/doors/{door_id}")
        return self.models.ItxptConfigDoorApcPathRequired.model_validate(response.json())

    async def update_door(self, door_id: int, door: Any) -> Any:
        """Updates an existing APC door configuration."""
        response = await self._http.put(f"{self._base_path}/config/doors/{door_id}", json=door)
        return self.models.ItxptConfigDoorApcPathRequired.model_validate(response.json())

    async def delete_door(self, door_id: int) -> None:
        """Deletes a specific APC door configuration."""
        await self._http.delete(f"{self._base_path}/config/doors/{door_id}")

    # --- RAW DATA ENDPOINTS (TELEMETRY) ---
    async def get_apc_data(self) -> Any:
        """
        Retrieves the raw PassengerDoorCountDelivery telemetry directly
        from the ITxPT APC service endpoint.

        Note: The return format is XML/dictated by ITxPT standards. Pydantic
        validation relies on the translated JSON schema.
        """
        # The ITxPT standard specifies this endpoint for APC polling
        response = await self._http.get(f"{self._base_path}/services/apc/passengerdoorcount")

        # Depending on content negotiation, this may return XML. Assuming the client
        # handles standard JSON decoding if application/json is requested.
        try:
            return self.models.PassengerDoorCountDelivery.model_validate(response.json())
        except Exception:
            return response.text

    async def get_inventory_info(self) -> Any:
        """
        Retrieves the raw ModuleDelivery inventory telemetry (Hardware/Services info)
        directly from the ITxPT inventory service endpoint.
        """
        response = await self._http.get(f"{self._base_path}/services/inventory/moduleinfo.xml")

        try:
            return self.models.ModulesDelivery.model_validate(response.json())
        except Exception:
            return response.text
Attributes
models property

Returns the strictly validated Pydantic models for the current firmware.

Methods:
__init__(http_client, client=None)

Initializes the ITxPTManager.

Parameters:

Name Type Description Default
http_client XovisHTTPClient

The resilient HTTP client.

required
client DeviceClient

The parent DeviceClient instance.

None
Source code in src/xovis/api/device/resources/itxpt.py
30
31
32
33
34
35
36
37
38
39
40
def __init__(self, http_client: XovisHTTPClient, client: "DeviceClient" = None) -> None:
    """
    Initializes the ITxPTManager.

    Args:
        http_client (XovisHTTPClient): The resilient HTTP client.
        client (DeviceClient): The parent DeviceClient instance.
    """
    self._http = http_client
    self._client = client
    self._base_path = "/api/v5/itxpt"
create_door(door) async

Creates a new ITxPT APC door configuration.

Source code in src/xovis/api/device/resources/itxpt.py
135
136
137
138
async def create_door(self, door: Any) -> Any:
    """Creates a new ITxPT APC door configuration."""
    response = await self._http.post(f"{self._base_path}/config/doors", json=door)
    return self.models.ItxptConfigDoorApcPathRequired.model_validate(response.json())
delete_all_doors() async

Deletes all existing ITxPT APC door configurations.

Source code in src/xovis/api/device/resources/itxpt.py
140
141
142
async def delete_all_doors(self) -> None:
    """Deletes all existing ITxPT APC door configurations."""
    await self._http.delete(f"{self._base_path}/config/doors")
delete_door(door_id) async

Deletes a specific APC door configuration.

Source code in src/xovis/api/device/resources/itxpt.py
154
155
156
async def delete_door(self, door_id: int) -> None:
    """Deletes a specific APC door configuration."""
    await self._http.delete(f"{self._base_path}/config/doors/{door_id}")
get_all_doors() async

Retrieves all defined ITxPT APC door configurations.

Source code in src/xovis/api/device/resources/itxpt.py
130
131
132
133
async def get_all_doors(self) -> Any:
    """Retrieves all defined ITxPT APC door configurations."""
    response = await self._http.get(f"{self._base_path}/config/doors")
    return self.models.ItxptConfigDoorCollection.model_validate(response.json())
get_apc_data() async

Retrieves the raw PassengerDoorCountDelivery telemetry directly from the ITxPT APC service endpoint.

Note: The return format is XML/dictated by ITxPT standards. Pydantic validation relies on the translated JSON schema.

Source code in src/xovis/api/device/resources/itxpt.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
async def get_apc_data(self) -> Any:
    """
    Retrieves the raw PassengerDoorCountDelivery telemetry directly
    from the ITxPT APC service endpoint.

    Note: The return format is XML/dictated by ITxPT standards. Pydantic
    validation relies on the translated JSON schema.
    """
    # The ITxPT standard specifies this endpoint for APC polling
    response = await self._http.get(f"{self._base_path}/services/apc/passengerdoorcount")

    # Depending on content negotiation, this may return XML. Assuming the client
    # handles standard JSON decoding if application/json is requested.
    try:
        return self.models.PassengerDoorCountDelivery.model_validate(response.json())
    except Exception:
        return response.text
get_config() async

Retrieves the global ITxPT configuration (e.g., protocol version, vehicle ID source).

Source code in src/xovis/api/device/resources/itxpt.py
48
49
50
51
52
53
async def get_config(self) -> Any:
    """
    Retrieves the global ITxPT configuration (e.g., protocol version, vehicle ID source).
    """
    response = await self._http.get(f"{self._base_path}/config")
    return self.models.ItxptConfig.model_validate(response.json())
get_custom_configurations() async

Retrieves custom free text configurations specific to ITxPT deployments.

Source code in src/xovis/api/device/resources/itxpt.py
116
117
118
119
120
121
async def get_custom_configurations(self) -> Any:
    """
    Retrieves custom free text configurations specific to ITxPT deployments.
    """
    response = await self._http.get("/api/v5/blob/store/itxpt_custom_configurations.json")
    return self.models.ItxptCustomConfigurations.model_validate(response.json())
get_door(door_id) async

Retrieves a specific APC door configuration.

Source code in src/xovis/api/device/resources/itxpt.py
144
145
146
147
async def get_door(self, door_id: int) -> Any:
    """Retrieves a specific APC door configuration."""
    response = await self._http.get(f"{self._base_path}/config/doors/{door_id}")
    return self.models.ItxptConfigDoorApcPathRequired.model_validate(response.json())
get_inventory_info() async

Retrieves the raw ModuleDelivery inventory telemetry (Hardware/Services info) directly from the ITxPT inventory service endpoint.

Source code in src/xovis/api/device/resources/itxpt.py
177
178
179
180
181
182
183
184
185
186
187
async def get_inventory_info(self) -> Any:
    """
    Retrieves the raw ModuleDelivery inventory telemetry (Hardware/Services info)
    directly from the ITxPT inventory service endpoint.
    """
    response = await self._http.get(f"{self._base_path}/services/inventory/moduleinfo.xml")

    try:
        return self.models.ModulesDelivery.model_validate(response.json())
    except Exception:
        return response.text
get_services_state() async

Retrieves the runtime networking state of all ITxPT sub-services, including current consumers subscribed to the APC feed.

Source code in src/xovis/api/device/resources/itxpt.py
69
70
71
72
73
74
75
async def get_services_state(self) -> Any:
    """
    Retrieves the runtime networking state of all ITxPT sub-services,
    including current consumers subscribed to the APC feed.
    """
    response = await self._http.get(f"{self._base_path}/services/state")
    return self.models.ItxptServicesState.model_validate(response.json())
get_state() async

Retrieves the current ITxPT operational state and resolved Vehicle ID.

Source code in src/xovis/api/device/resources/itxpt.py
62
63
64
65
66
67
async def get_state(self) -> Any:
    """
    Retrieves the current ITxPT operational state and resolved Vehicle ID.
    """
    response = await self._http.get(f"{self._base_path}/state")
    return self.models.ItxptState.model_validate(response.json())
get_time_config() async

Retrieves the ITxPT time discovery configuration (SNTP enable toggle).

Source code in src/xovis/api/device/resources/itxpt.py
78
79
80
81
82
83
async def get_time_config(self) -> Any:
    """
    Retrieves the ITxPT time discovery configuration (SNTP enable toggle).
    """
    response = await self._http.get(f"{self._base_path}/config/time")
    return self.models.ItxptTimeConfig.model_validate(response.json())
get_time_status() async

Retrieves the runtime status of the vehicle time discovery. Crucial for agents to verify if the sensor has found an SNTP server.

Source code in src/xovis/api/device/resources/itxpt.py
92
93
94
95
96
97
98
async def get_time_status(self) -> Any:
    """
    Retrieves the runtime status of the vehicle time discovery.
    Crucial for agents to verify if the sensor has found an SNTP server.
    """
    response = await self._http.get(f"{self._base_path}/time/sntp_server")
    return self.models.ItxptTime.model_validate(response.json())
get_txt_records() async

Retrieves the extra TXT records published in the ITxPT mDNS broadcast.

Source code in src/xovis/api/device/resources/itxpt.py
101
102
103
104
105
106
async def get_txt_records(self) -> Any:
    """
    Retrieves the extra TXT records published in the ITxPT mDNS broadcast.
    """
    response = await self._http.get(f"{self._base_path}/config/txt")
    return self.models.ItxptConfigTxt.model_validate(response.json())
update_config(config) async

Updates the global ITxPT configuration.

Source code in src/xovis/api/device/resources/itxpt.py
55
56
57
58
59
60
async def update_config(self, config: Any) -> Any:
    """
    Updates the global ITxPT configuration.
    """
    response = await self._http.put(f"{self._base_path}/config", json=config)
    return self.models.ItxptConfig.model_validate(response.json())
update_custom_configurations(custom_configs) async

Updates the custom free text configurations blob.

Source code in src/xovis/api/device/resources/itxpt.py
123
124
125
126
127
async def update_custom_configurations(self, custom_configs: Any) -> Any:
    """
    Updates the custom free text configurations blob.
    """
    await self._http.put("/api/v5/blob/store/itxpt_custom_configurations.json", json=custom_configs)
update_door(door_id, door) async

Updates an existing APC door configuration.

Source code in src/xovis/api/device/resources/itxpt.py
149
150
151
152
async def update_door(self, door_id: int, door: Any) -> Any:
    """Updates an existing APC door configuration."""
    response = await self._http.put(f"{self._base_path}/config/doors/{door_id}", json=door)
    return self.models.ItxptConfigDoorApcPathRequired.model_validate(response.json())
update_time_config(config) async

Updates the ITxPT time discovery configuration.

Source code in src/xovis/api/device/resources/itxpt.py
85
86
87
88
89
90
async def update_time_config(self, config: Any) -> Any:
    """
    Updates the ITxPT time discovery configuration.
    """
    response = await self._http.put(f"{self._base_path}/config/time", json=config)
    return self.models.ItxptTimeConfig.model_validate(response.json())
update_txt_records(txt_records) async

Updates the extra TXT records for the mDNS broadcast.

Source code in src/xovis/api/device/resources/itxpt.py
108
109
110
111
112
113
async def update_txt_records(self, txt_records: Any) -> Any:
    """
    Updates the extra TXT records for the mDNS broadcast.
    """
    response = await self._http.put(f"{self._base_path}/config/txt", json=txt_records)
    return self.models.ItxptConfigTxt.model_validate(response.json())

xovis.api.device.resources.update

Xovis SDK - Firmware Update Resource

Operates within the Control Plane. Provides comprehensive implementation for managing the firmware update lifecycle on local edge sensors. This includes OTA (Over-The-Air) cloud downloads, local binary flashing, installation scheduling, and autonomous update diagnostics.

Classes

UpdateManager

Manages firmware update operations, schedulers, and diagnostics on a Xovis device.

This manager orchestrates the entire firmware maintenance lifecycle. It allows agents to query cloud availability, stream raw firmware binaries, schedule off-hours installations, and parse plaintext installation logs for autonomous failure remediation.

Source code in src/xovis/api/device/resources/update.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
class UpdateManager:
    """
    Manages firmware update operations, schedulers, and diagnostics on a Xovis device.

    This manager orchestrates the entire firmware maintenance lifecycle. It allows
    agents to query cloud availability, stream raw firmware binaries, schedule
    off-hours installations, and parse plaintext installation logs for autonomous
    failure remediation.
    """

    def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
        """
        Initializes the UpdateManager.

        Args:
            client (DeviceClient): The parent device client instance providing
                authenticated HTTPX connection pooling.
            target_id (Optional[str]): The multisensor target ID, if applicable.
        """
        self._client = client
        self._http = client._http_client
        self.target_id = target_id
        self._base_path = "/api/v5/updates"

    @property
    def models(self):
        """Returns the strictly validated Pydantic models for the current firmware."""
        return self._client.models if self._client else stable_models

    def _resolve_path(self) -> str:
        """Resolves the base API path for updates."""
        if self.target_id:
            # Note: For multisensors, update status is under stitcher sensors
            return f"/api/v5/multisensors/{self.target_id}/stitcher/sensors/update"
        return "/api/v5/updates"

    async def get_info(self) -> Any:
        """
        Retrieves static minimal update information.

        Returns:
            UpdateInfo: The minimal software version and current version data.
        """
        response = await self._http.get(f"{self._resolve_path()}/info")
        return self.models.UpdateInfo.model_validate(response.json())

    async def get_state(self) -> Any:
        """
        Retrieves the real-time installation and execution state of the update engine.

        Returns:
            UpdateState: Current update state (e.g., OK, INSTALLING, REBOOTING).
        """
        response = await self._http.get(f"{self._resolve_path()}/state")
        return self.models.UpdateState.model_validate(response.json())

    async def get_history(self, include_fails: bool = True) -> Any:
        """
        Retrieves the chronological history of firmware installations.

        Args:
            include_fails (bool): Whether to include failed installation attempts
                in the diagnostic ledger. Defaults to True.

        Returns:
            UpdateHistory: Validated Pydantic model containing the installation ledger.
        """
        params = {"include_fails": "true" if include_fails else "false"}
        response = await self._http.get(f"{self._resolve_path()}/history", params=params)
        return self.models.UpdateHistory.model_validate(response.json())

    async def get_log(self, offset: int = 0) -> str:
        """
        Retrieves the raw plaintext installation log.

        Crucial for autonomous agents to parse why an installation failed
        (e.g., hardware incompatibility or checksum errors).

        Args:
            offset (int): Number of bytes to skip, allowing for polled streaming.

        Returns:
            str: The raw plaintext log output.
        """
        params = {"offset": offset}
        response = await self._http.get(f"{self._resolve_path()}/log", params=params)
        response.raise_for_status()
        return response.text

    async def get_packages(self) -> Any:
        """
        Retrieves the list of firmware packages currently stored on the sensor's flash.

        Returns:
            UpdatePackages: A list of version identifiers currently cached.
        """
        response = await self._http.get(self._resolve_path())
        return self.models.UpdatePackages.model_validate(response.json())

    async def delete_all_packages(self, force: bool = False) -> None:
        """
        Deletes all firmware packages cached on the sensor's internal storage.

        Args:
            force (bool): If True, also deletes any pending scheduled updates.
        """
        params = {"force": "true" if force else "false"}
        await self._http.delete(self._resolve_path(), params=params)

    async def delete_package(self, version: str, force: bool = False) -> None:
        """
        Deletes a specific firmware package from the sensor's storage.

        Args:
            version (str): The exact version identifier of the package.
            force (bool): If True, also deletes if it is currently scheduled.
        """
        params = {"force": "true" if force else "false"}
        await self._http.delete(f"{self._resolve_path()}/{version}", params=params)

    async def upload_firmware(self, file_path: str) -> Any:
        """
        Uploads a firmware update package (.xup) to the sensor's flash memory.

        Bypasses multipart/form-data to stream the raw binary directly,
        satisfying the strict application/octet-stream OpenAPI requirement.

        Args:
            file_path (str): The local filesystem path to the .xup binary.

        Returns:
            UpdateVersion: The parsed version identifier of the uploaded package.
        """
        headers = {"Content-Type": "application/octet-stream"}
        with open(file_path, "rb") as f:
            response = await self._http.post(self._resolve_path(), content=f, headers=headers)
        return self.models.UpdateVersion.model_validate(response.json())

    async def install_package(self, version: str, force: bool = False) -> None:
        """
        Triggers the installation of a firmware package already cached on the sensor.

        Args:
            version (str): The exact version identifier to install.
            force (bool): CRITICAL: If True, forces the installation even if it
                results in a configuration-breaking downgrade.
        """
        params = {"force": "true" if force else "false"}
        await self._http.post(f"{self._resolve_path()}/{version}/install", params=params)

    async def upload_and_install(self, file_path: str, force: bool = False) -> Any:
        """
        Simultaneously datapush and executes a firmware binary installation.

        Args:
            file_path (str): The local filesystem path to the .xup binary.
            force (bool): CRITICAL: If True, forces the installation.

        Returns:
            UpdateVersion: The parsed version identifier of the installed package.
        """
        headers = {"Content-Type": "application/octet-stream"}
        params = {"force": "true" if force else "false"}
        with open(file_path, "rb") as f:
            response = await self._http.post(f"{self._resolve_path()}/install", content=f, headers=headers, params=params)
        return self.models.UpdateVersion.model_validate(response.json())

    async def get_schedule(self) -> Any:
        """
        Retrieves the current automated firmware installation schedule.

        Returns:
            UpdateSchedule: The configured execution time and target version.
        """
        response = await self._http.get(f"{self._resolve_path()}/schedule")
        return self.models.UpdateSchedule.model_validate(response.json())

    async def set_schedule(self, schedule: Any) -> Any:
        """
        Configures an automated future firmware installation.

        Args:
            schedule (UpdateSchedule): The validated Pydantic schedule payload
                containing the target version and XovisTime.

        Returns:
            UpdateSchedule: The confirmed active schedule.
        """
        response = await self._http.put(f"{self._resolve_path()}/schedule", json=schedule)
        return self.models.UpdateSchedule.model_validate(response.json())

    async def delete_schedule(self) -> None:
        """Cancels any pending automated firmware installation."""
        await self._http.delete(f"{self._resolve_path()}/schedule")

    async def get_config(self) -> Any:
        """
        Retrieves the automated cloud download configuration.

        Returns:
            DownloadConfig: The current automation toggles for minor updates.
        """
        response = await self._http.get(f"{self._resolve_path()}/config")
        return self.models.DownloadConfig.model_validate(response.json())

    async def set_config(self, config: Any) -> Any:
        """
        Modifies the automated cloud download configuration.

        Args:
            config (DownloadConfig): The updated Pydantic configuration model.

        Returns:
            DownloadConfig: The successfully applied configuration.
        """
        response = await self._http.put(f"{self._resolve_path()}/config", json=config)
        return self.models.DownloadConfig.model_validate(response.json())

    async def delete_config(self) -> None:
        """Resets the automated cloud download configuration to factory defaults."""
        await self._http.delete(f"{self._resolve_path()}/config")

    async def get_available_updates(self) -> Any:
        """
        Queries the Xovis Cloud for applicable firmware upgrades.

        Returns:
            UpdatesAvailable: List of compatible firmware metadata.
        """
        response = await self._http.get(f"{self._resolve_path()}/available")
        return self.models.UpdatesAvailable.model_validate(response.json())

    async def refresh_available_updates(self) -> None:
        """Forces the sensor to immediately re-poll the Xovis Cloud for upgrades."""
        await self._http.post(f"{self._resolve_path()}/available/refresh")

    async def get_download_state(self) -> Any:
        """
        Retrieves the real-time progression of an active cloud firmware download.

        Returns:
            DownloadState: The current percentage and error states of the download.
        """
        response = await self._http.get(f"{self._resolve_path()}/download/state")
        return self.models.DownloadState.model_validate(response.json())

    async def start_cloud_download(self, version: str) -> None:
        """
        Commands the sensor to begin downloading a specific firmware version
        from the Xovis Cloud.

        Args:
            version (str): The exact version string to fetch.
        """
        await self._http.post(f"{self._resolve_path()}/download/{version}")

    async def cancel_cloud_download(self) -> None:
        """Aborts any currently active firmware download from the Xovis Cloud."""
        await self._http.delete(f"{self._resolve_path()}/download")
Attributes
models property

Returns the strictly validated Pydantic models for the current firmware.

Methods:
__init__(client, target_id=None)

Initializes the UpdateManager.

Parameters:

Name Type Description Default
client DeviceClient

The parent device client instance providing authenticated HTTPX connection pooling.

required
target_id Optional[str]

The multisensor target ID, if applicable.

None
Source code in src/xovis/api/device/resources/update.py
29
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
    """
    Initializes the UpdateManager.

    Args:
        client (DeviceClient): The parent device client instance providing
            authenticated HTTPX connection pooling.
        target_id (Optional[str]): The multisensor target ID, if applicable.
    """
    self._client = client
    self._http = client._http_client
    self.target_id = target_id
    self._base_path = "/api/v5/updates"
_resolve_path()

Resolves the base API path for updates.

Source code in src/xovis/api/device/resources/update.py
48
49
50
51
52
53
def _resolve_path(self) -> str:
    """Resolves the base API path for updates."""
    if self.target_id:
        # Note: For multisensors, update status is under stitcher sensors
        return f"/api/v5/multisensors/{self.target_id}/stitcher/sensors/update"
    return "/api/v5/updates"
cancel_cloud_download() async

Aborts any currently active firmware download from the Xovis Cloud.

Source code in src/xovis/api/device/resources/update.py
275
276
277
async def cancel_cloud_download(self) -> None:
    """Aborts any currently active firmware download from the Xovis Cloud."""
    await self._http.delete(f"{self._resolve_path()}/download")
delete_all_packages(force=False) async

Deletes all firmware packages cached on the sensor's internal storage.

Parameters:

Name Type Description Default
force bool

If True, also deletes any pending scheduled updates.

False
Source code in src/xovis/api/device/resources/update.py
118
119
120
121
122
123
124
125
126
async def delete_all_packages(self, force: bool = False) -> None:
    """
    Deletes all firmware packages cached on the sensor's internal storage.

    Args:
        force (bool): If True, also deletes any pending scheduled updates.
    """
    params = {"force": "true" if force else "false"}
    await self._http.delete(self._resolve_path(), params=params)
delete_config() async

Resets the automated cloud download configuration to factory defaults.

Source code in src/xovis/api/device/resources/update.py
237
238
239
async def delete_config(self) -> None:
    """Resets the automated cloud download configuration to factory defaults."""
    await self._http.delete(f"{self._resolve_path()}/config")
delete_package(version, force=False) async

Deletes a specific firmware package from the sensor's storage.

Parameters:

Name Type Description Default
version str

The exact version identifier of the package.

required
force bool

If True, also deletes if it is currently scheduled.

False
Source code in src/xovis/api/device/resources/update.py
128
129
130
131
132
133
134
135
136
137
async def delete_package(self, version: str, force: bool = False) -> None:
    """
    Deletes a specific firmware package from the sensor's storage.

    Args:
        version (str): The exact version identifier of the package.
        force (bool): If True, also deletes if it is currently scheduled.
    """
    params = {"force": "true" if force else "false"}
    await self._http.delete(f"{self._resolve_path()}/{version}", params=params)
delete_schedule() async

Cancels any pending automated firmware installation.

Source code in src/xovis/api/device/resources/update.py
210
211
212
async def delete_schedule(self) -> None:
    """Cancels any pending automated firmware installation."""
    await self._http.delete(f"{self._resolve_path()}/schedule")
get_available_updates() async

Queries the Xovis Cloud for applicable firmware upgrades.

Returns:

Name Type Description
UpdatesAvailable Any

List of compatible firmware metadata.

Source code in src/xovis/api/device/resources/update.py
241
242
243
244
245
246
247
248
249
async def get_available_updates(self) -> Any:
    """
    Queries the Xovis Cloud for applicable firmware upgrades.

    Returns:
        UpdatesAvailable: List of compatible firmware metadata.
    """
    response = await self._http.get(f"{self._resolve_path()}/available")
    return self.models.UpdatesAvailable.model_validate(response.json())
get_config() async

Retrieves the automated cloud download configuration.

Returns:

Name Type Description
DownloadConfig Any

The current automation toggles for minor updates.

Source code in src/xovis/api/device/resources/update.py
214
215
216
217
218
219
220
221
222
async def get_config(self) -> Any:
    """
    Retrieves the automated cloud download configuration.

    Returns:
        DownloadConfig: The current automation toggles for minor updates.
    """
    response = await self._http.get(f"{self._resolve_path()}/config")
    return self.models.DownloadConfig.model_validate(response.json())
get_download_state() async

Retrieves the real-time progression of an active cloud firmware download.

Returns:

Name Type Description
DownloadState Any

The current percentage and error states of the download.

Source code in src/xovis/api/device/resources/update.py
255
256
257
258
259
260
261
262
263
async def get_download_state(self) -> Any:
    """
    Retrieves the real-time progression of an active cloud firmware download.

    Returns:
        DownloadState: The current percentage and error states of the download.
    """
    response = await self._http.get(f"{self._resolve_path()}/download/state")
    return self.models.DownloadState.model_validate(response.json())
get_history(include_fails=True) async

Retrieves the chronological history of firmware installations.

Parameters:

Name Type Description Default
include_fails bool

Whether to include failed installation attempts in the diagnostic ledger. Defaults to True.

True

Returns:

Name Type Description
UpdateHistory Any

Validated Pydantic model containing the installation ledger.

Source code in src/xovis/api/device/resources/update.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
async def get_history(self, include_fails: bool = True) -> Any:
    """
    Retrieves the chronological history of firmware installations.

    Args:
        include_fails (bool): Whether to include failed installation attempts
            in the diagnostic ledger. Defaults to True.

    Returns:
        UpdateHistory: Validated Pydantic model containing the installation ledger.
    """
    params = {"include_fails": "true" if include_fails else "false"}
    response = await self._http.get(f"{self._resolve_path()}/history", params=params)
    return self.models.UpdateHistory.model_validate(response.json())
get_info() async

Retrieves static minimal update information.

Returns:

Name Type Description
UpdateInfo Any

The minimal software version and current version data.

Source code in src/xovis/api/device/resources/update.py
55
56
57
58
59
60
61
62
63
async def get_info(self) -> Any:
    """
    Retrieves static minimal update information.

    Returns:
        UpdateInfo: The minimal software version and current version data.
    """
    response = await self._http.get(f"{self._resolve_path()}/info")
    return self.models.UpdateInfo.model_validate(response.json())
get_log(offset=0) async

Retrieves the raw plaintext installation log.

Crucial for autonomous agents to parse why an installation failed (e.g., hardware incompatibility or checksum errors).

Parameters:

Name Type Description Default
offset int

Number of bytes to skip, allowing for polled streaming.

0

Returns:

Name Type Description
str str

The raw plaintext log output.

Source code in src/xovis/api/device/resources/update.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
async def get_log(self, offset: int = 0) -> str:
    """
    Retrieves the raw plaintext installation log.

    Crucial for autonomous agents to parse why an installation failed
    (e.g., hardware incompatibility or checksum errors).

    Args:
        offset (int): Number of bytes to skip, allowing for polled streaming.

    Returns:
        str: The raw plaintext log output.
    """
    params = {"offset": offset}
    response = await self._http.get(f"{self._resolve_path()}/log", params=params)
    response.raise_for_status()
    return response.text
get_packages() async

Retrieves the list of firmware packages currently stored on the sensor's flash.

Returns:

Name Type Description
UpdatePackages Any

A list of version identifiers currently cached.

Source code in src/xovis/api/device/resources/update.py
108
109
110
111
112
113
114
115
116
async def get_packages(self) -> Any:
    """
    Retrieves the list of firmware packages currently stored on the sensor's flash.

    Returns:
        UpdatePackages: A list of version identifiers currently cached.
    """
    response = await self._http.get(self._resolve_path())
    return self.models.UpdatePackages.model_validate(response.json())
get_schedule() async

Retrieves the current automated firmware installation schedule.

Returns:

Name Type Description
UpdateSchedule Any

The configured execution time and target version.

Source code in src/xovis/api/device/resources/update.py
186
187
188
189
190
191
192
193
194
async def get_schedule(self) -> Any:
    """
    Retrieves the current automated firmware installation schedule.

    Returns:
        UpdateSchedule: The configured execution time and target version.
    """
    response = await self._http.get(f"{self._resolve_path()}/schedule")
    return self.models.UpdateSchedule.model_validate(response.json())
get_state() async

Retrieves the real-time installation and execution state of the update engine.

Returns:

Name Type Description
UpdateState Any

Current update state (e.g., OK, INSTALLING, REBOOTING).

Source code in src/xovis/api/device/resources/update.py
65
66
67
68
69
70
71
72
73
async def get_state(self) -> Any:
    """
    Retrieves the real-time installation and execution state of the update engine.

    Returns:
        UpdateState: Current update state (e.g., OK, INSTALLING, REBOOTING).
    """
    response = await self._http.get(f"{self._resolve_path()}/state")
    return self.models.UpdateState.model_validate(response.json())
install_package(version, force=False) async

Triggers the installation of a firmware package already cached on the sensor.

Parameters:

Name Type Description Default
version str

The exact version identifier to install.

required
force bool

CRITICAL: If True, forces the installation even if it results in a configuration-breaking downgrade.

False
Source code in src/xovis/api/device/resources/update.py
157
158
159
160
161
162
163
164
165
166
167
async def install_package(self, version: str, force: bool = False) -> None:
    """
    Triggers the installation of a firmware package already cached on the sensor.

    Args:
        version (str): The exact version identifier to install.
        force (bool): CRITICAL: If True, forces the installation even if it
            results in a configuration-breaking downgrade.
    """
    params = {"force": "true" if force else "false"}
    await self._http.post(f"{self._resolve_path()}/{version}/install", params=params)
refresh_available_updates() async

Forces the sensor to immediately re-poll the Xovis Cloud for upgrades.

Source code in src/xovis/api/device/resources/update.py
251
252
253
async def refresh_available_updates(self) -> None:
    """Forces the sensor to immediately re-poll the Xovis Cloud for upgrades."""
    await self._http.post(f"{self._resolve_path()}/available/refresh")
set_config(config) async

Modifies the automated cloud download configuration.

Parameters:

Name Type Description Default
config DownloadConfig

The updated Pydantic configuration model.

required

Returns:

Name Type Description
DownloadConfig Any

The successfully applied configuration.

Source code in src/xovis/api/device/resources/update.py
224
225
226
227
228
229
230
231
232
233
234
235
async def set_config(self, config: Any) -> Any:
    """
    Modifies the automated cloud download configuration.

    Args:
        config (DownloadConfig): The updated Pydantic configuration model.

    Returns:
        DownloadConfig: The successfully applied configuration.
    """
    response = await self._http.put(f"{self._resolve_path()}/config", json=config)
    return self.models.DownloadConfig.model_validate(response.json())
set_schedule(schedule) async

Configures an automated future firmware installation.

Parameters:

Name Type Description Default
schedule UpdateSchedule

The validated Pydantic schedule payload containing the target version and XovisTime.

required

Returns:

Name Type Description
UpdateSchedule Any

The confirmed active schedule.

Source code in src/xovis/api/device/resources/update.py
196
197
198
199
200
201
202
203
204
205
206
207
208
async def set_schedule(self, schedule: Any) -> Any:
    """
    Configures an automated future firmware installation.

    Args:
        schedule (UpdateSchedule): The validated Pydantic schedule payload
            containing the target version and XovisTime.

    Returns:
        UpdateSchedule: The confirmed active schedule.
    """
    response = await self._http.put(f"{self._resolve_path()}/schedule", json=schedule)
    return self.models.UpdateSchedule.model_validate(response.json())
start_cloud_download(version) async

Commands the sensor to begin downloading a specific firmware version from the Xovis Cloud.

Parameters:

Name Type Description Default
version str

The exact version string to fetch.

required
Source code in src/xovis/api/device/resources/update.py
265
266
267
268
269
270
271
272
273
async def start_cloud_download(self, version: str) -> None:
    """
    Commands the sensor to begin downloading a specific firmware version
    from the Xovis Cloud.

    Args:
        version (str): The exact version string to fetch.
    """
    await self._http.post(f"{self._resolve_path()}/download/{version}")
upload_and_install(file_path, force=False) async

Simultaneously datapush and executes a firmware binary installation.

Parameters:

Name Type Description Default
file_path str

The local filesystem path to the .xup binary.

required
force bool

CRITICAL: If True, forces the installation.

False

Returns:

Name Type Description
UpdateVersion Any

The parsed version identifier of the installed package.

Source code in src/xovis/api/device/resources/update.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
async def upload_and_install(self, file_path: str, force: bool = False) -> Any:
    """
    Simultaneously datapush and executes a firmware binary installation.

    Args:
        file_path (str): The local filesystem path to the .xup binary.
        force (bool): CRITICAL: If True, forces the installation.

    Returns:
        UpdateVersion: The parsed version identifier of the installed package.
    """
    headers = {"Content-Type": "application/octet-stream"}
    params = {"force": "true" if force else "false"}
    with open(file_path, "rb") as f:
        response = await self._http.post(f"{self._resolve_path()}/install", content=f, headers=headers, params=params)
    return self.models.UpdateVersion.model_validate(response.json())
upload_firmware(file_path) async

Uploads a firmware update package (.xup) to the sensor's flash memory.

Bypasses multipart/form-data to stream the raw binary directly, satisfying the strict application/octet-stream OpenAPI requirement.

Parameters:

Name Type Description Default
file_path str

The local filesystem path to the .xup binary.

required

Returns:

Name Type Description
UpdateVersion Any

The parsed version identifier of the uploaded package.

Source code in src/xovis/api/device/resources/update.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
async def upload_firmware(self, file_path: str) -> Any:
    """
    Uploads a firmware update package (.xup) to the sensor's flash memory.

    Bypasses multipart/form-data to stream the raw binary directly,
    satisfying the strict application/octet-stream OpenAPI requirement.

    Args:
        file_path (str): The local filesystem path to the .xup binary.

    Returns:
        UpdateVersion: The parsed version identifier of the uploaded package.
    """
    headers = {"Content-Type": "application/octet-stream"}
    with open(file_path, "rb") as f:
        response = await self._http.post(self._resolve_path(), content=f, headers=headers)
    return self.models.UpdateVersion.model_validate(response.json())

xovis.api.device.resources.time_config

Xovis SDK - Time Configuration Resource

Operates within the Control Plane. Provides comprehensive implementation for managing time synchronization, NTP clock drift diagnostics, timezones, and manual overrides.

Classes

TimeManager

Manages time configuration and synchronization state on a Xovis device.

This manager orchestrates NTP synchronization, timezone management, and exposes raw chronological diagnostics to ensure telemetry timestamps remain perfectly aligned with downstream enterprise systems.

Source code in src/xovis/api/device/resources/time_config.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class TimeManager:
    """
    Manages time configuration and synchronization state on a Xovis device.

    This manager orchestrates NTP synchronization, timezone management,
    and exposes raw chronological diagnostics to ensure telemetry timestamps
    remain perfectly aligned with downstream enterprise systems.
    """

    def __init__(self, http_client: XovisHTTPClient, client: Optional["DeviceClient"] = None) -> None:
        """
        Initializes the TimeManager.

        Args:
            http_client (XovisHTTPClient): The resilient HTTP client.
            client (Optional[DeviceClient]): The parent DeviceClient instance.
        """
        self._http = http_client
        self._client = client
        self._base_path = "/api/v5/time"

    @property
    def models(self):
        """Returns the strictly validated Pydantic models for the current firmware."""
        return self._client.models if self._client else stable_models

    async def get_zones(self) -> Any:
        """
        Retrieves the list of available timezones supported by the device.

        Returns:
            Timezones: A collection of supported timezone identifiers
                (e.g., 'UTC', 'America/New_York').
        """
        response = await self._http.get(f"{self._base_path}/zones")
        return self.models.Timezones.model_validate(response.json())

    async def get_settings(self) -> Any:
        """
        Retrieves the current global time configuration.

        Provides insight into whether the device is acting as an NTP client,
        an NTP server, or both, alongside its configured upstream peers.

        Returns:
            TimeSettings: The validated time and NTP configuration model.
        """
        response = await self._http.get(self._base_path)
        return self.models.TimeSettings.model_validate(response.json())

    async def update_settings(self, settings: Any) -> None:
        """
        Updates the global time configuration.

        Allows agents to dynamically inject new NTP peers or change the timezone
        if the device is relocated.

        Args:
            settings (TimeSettings): The updated time configuration model.
        """
        await self._http.put(self._base_path, json=settings)

    async def reset_settings(self) -> None:
        """
        Resets the time configuration to factory defaults.

        Default state falls back to UTC with 'pool.ntp.org' as the primary
        upstream peer and NTP server capabilities disabled.
        """
        await self._http.delete(self._base_path)

    async def set_manual_time(self, time_settings: Any) -> None:
        """
        Manually forces the current device time.

        CRITICAL: This endpoint will fail with a 412 Precondition Failed error
        if the device currently has NTP enabled. Agents must first disable NTP
        via `update_settings` before invoking this method.

        Args:
            time_settings (TimeManualSettings): The manual time details containing
                either `time_utc` or `time_local`.
        """
        await self._http.put(f"{self._base_path}/manual", json=time_settings)

    async def get_state(self) -> Any:
        """
        Retrieves the highly detailed operational state of the time service.

        Crucial for Autonomous Maintenance (Module D). Agents can poll this
        endpoint to monitor `ntp_rms_offset`, `ntp_root_delay`, and identify
        peers marked as `FALSETICKER` or `UNREACHABLE`.

        Returns:
            TimeState: The runtime time synchronization status and source health.
        """
        response = await self._http.get(f"{self._base_path}/state")
        return self.models.TimeState.model_validate(response.json())

    async def get_stamp(self) -> str:
        """
        Retrieves a raw UNIX timestamp directly from the OS-level clock.

        This is an undocumented diagnostic endpoint useful for latency-sensitive
        ping tests between the Hub and the Edge to calculate transit delay.

        Returns:
            str: The raw plaintext timestamp string.
        """
        response = await self._http.get(f"{self._base_path}/stamp")
        return response.text

    async def get_verbose_diagnostics(self) -> str:
        """
        Retrieves raw, verbose NTP daemon logs directly from the Edge OS.

        This is an undocumented diagnostic endpoint providing deep Chrony/NTPd
        output, allowing LLM agents to perform advanced root-cause analysis on
        time synchronization failures.

        Returns:
            str: The raw plaintext verbose diagnostic output.
        """
        response = await self._http.get(f"{self._base_path}/verbose")
        return response.text
Attributes
models property

Returns the strictly validated Pydantic models for the current firmware.

Methods:
__init__(http_client, client=None)

Initializes the TimeManager.

Parameters:

Name Type Description Default
http_client XovisHTTPClient

The resilient HTTP client.

required
client Optional[DeviceClient]

The parent DeviceClient instance.

None
Source code in src/xovis/api/device/resources/time_config.py
27
28
29
30
31
32
33
34
35
36
37
def __init__(self, http_client: XovisHTTPClient, client: Optional["DeviceClient"] = None) -> None:
    """
    Initializes the TimeManager.

    Args:
        http_client (XovisHTTPClient): The resilient HTTP client.
        client (Optional[DeviceClient]): The parent DeviceClient instance.
    """
    self._http = http_client
    self._client = client
    self._base_path = "/api/v5/time"
get_settings() async

Retrieves the current global time configuration.

Provides insight into whether the device is acting as an NTP client, an NTP server, or both, alongside its configured upstream peers.

Returns:

Name Type Description
TimeSettings Any

The validated time and NTP configuration model.

Source code in src/xovis/api/device/resources/time_config.py
55
56
57
58
59
60
61
62
63
64
65
66
async def get_settings(self) -> Any:
    """
    Retrieves the current global time configuration.

    Provides insight into whether the device is acting as an NTP client,
    an NTP server, or both, alongside its configured upstream peers.

    Returns:
        TimeSettings: The validated time and NTP configuration model.
    """
    response = await self._http.get(self._base_path)
    return self.models.TimeSettings.model_validate(response.json())
get_stamp() async

Retrieves a raw UNIX timestamp directly from the OS-level clock.

This is an undocumented diagnostic endpoint useful for latency-sensitive ping tests between the Hub and the Edge to calculate transit delay.

Returns:

Name Type Description
str str

The raw plaintext timestamp string.

Source code in src/xovis/api/device/resources/time_config.py
117
118
119
120
121
122
123
124
125
126
127
128
async def get_stamp(self) -> str:
    """
    Retrieves a raw UNIX timestamp directly from the OS-level clock.

    This is an undocumented diagnostic endpoint useful for latency-sensitive
    ping tests between the Hub and the Edge to calculate transit delay.

    Returns:
        str: The raw plaintext timestamp string.
    """
    response = await self._http.get(f"{self._base_path}/stamp")
    return response.text
get_state() async

Retrieves the highly detailed operational state of the time service.

Crucial for Autonomous Maintenance (Module D). Agents can poll this endpoint to monitor ntp_rms_offset, ntp_root_delay, and identify peers marked as FALSETICKER or UNREACHABLE.

Returns:

Name Type Description
TimeState Any

The runtime time synchronization status and source health.

Source code in src/xovis/api/device/resources/time_config.py
103
104
105
106
107
108
109
110
111
112
113
114
115
async def get_state(self) -> Any:
    """
    Retrieves the highly detailed operational state of the time service.

    Crucial for Autonomous Maintenance (Module D). Agents can poll this
    endpoint to monitor `ntp_rms_offset`, `ntp_root_delay`, and identify
    peers marked as `FALSETICKER` or `UNREACHABLE`.

    Returns:
        TimeState: The runtime time synchronization status and source health.
    """
    response = await self._http.get(f"{self._base_path}/state")
    return self.models.TimeState.model_validate(response.json())
get_verbose_diagnostics() async

Retrieves raw, verbose NTP daemon logs directly from the Edge OS.

This is an undocumented diagnostic endpoint providing deep Chrony/NTPd output, allowing LLM agents to perform advanced root-cause analysis on time synchronization failures.

Returns:

Name Type Description
str str

The raw plaintext verbose diagnostic output.

Source code in src/xovis/api/device/resources/time_config.py
130
131
132
133
134
135
136
137
138
139
140
141
142
async def get_verbose_diagnostics(self) -> str:
    """
    Retrieves raw, verbose NTP daemon logs directly from the Edge OS.

    This is an undocumented diagnostic endpoint providing deep Chrony/NTPd
    output, allowing LLM agents to perform advanced root-cause analysis on
    time synchronization failures.

    Returns:
        str: The raw plaintext verbose diagnostic output.
    """
    response = await self._http.get(f"{self._base_path}/verbose")
    return response.text
get_zones() async

Retrieves the list of available timezones supported by the device.

Returns:

Name Type Description
Timezones Any

A collection of supported timezone identifiers (e.g., 'UTC', 'America/New_York').

Source code in src/xovis/api/device/resources/time_config.py
44
45
46
47
48
49
50
51
52
53
async def get_zones(self) -> Any:
    """
    Retrieves the list of available timezones supported by the device.

    Returns:
        Timezones: A collection of supported timezone identifiers
            (e.g., 'UTC', 'America/New_York').
    """
    response = await self._http.get(f"{self._base_path}/zones")
    return self.models.Timezones.model_validate(response.json())
reset_settings() async

Resets the time configuration to factory defaults.

Default state falls back to UTC with 'pool.ntp.org' as the primary upstream peer and NTP server capabilities disabled.

Source code in src/xovis/api/device/resources/time_config.py
80
81
82
83
84
85
86
87
async def reset_settings(self) -> None:
    """
    Resets the time configuration to factory defaults.

    Default state falls back to UTC with 'pool.ntp.org' as the primary
    upstream peer and NTP server capabilities disabled.
    """
    await self._http.delete(self._base_path)
set_manual_time(time_settings) async

Manually forces the current device time.

CRITICAL: This endpoint will fail with a 412 Precondition Failed error if the device currently has NTP enabled. Agents must first disable NTP via update_settings before invoking this method.

Parameters:

Name Type Description Default
time_settings TimeManualSettings

The manual time details containing either time_utc or time_local.

required
Source code in src/xovis/api/device/resources/time_config.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
async def set_manual_time(self, time_settings: Any) -> None:
    """
    Manually forces the current device time.

    CRITICAL: This endpoint will fail with a 412 Precondition Failed error
    if the device currently has NTP enabled. Agents must first disable NTP
    via `update_settings` before invoking this method.

    Args:
        time_settings (TimeManualSettings): The manual time details containing
            either `time_utc` or `time_local`.
    """
    await self._http.put(f"{self._base_path}/manual", json=time_settings)
update_settings(settings) async

Updates the global time configuration.

Allows agents to dynamically inject new NTP peers or change the timezone if the device is relocated.

Parameters:

Name Type Description Default
settings TimeSettings

The updated time configuration model.

required
Source code in src/xovis/api/device/resources/time_config.py
68
69
70
71
72
73
74
75
76
77
78
async def update_settings(self, settings: Any) -> None:
    """
    Updates the global time configuration.

    Allows agents to dynamically inject new NTP peers or change the timezone
    if the device is relocated.

    Args:
        settings (TimeSettings): The updated time configuration model.
    """
    await self._http.put(self._base_path, json=settings)

xovis.api.device.resources.users

Xovis SDK - User Management Resource

Provides the implementation for managing user accounts, passwords, sessions, and activation states on local edge sensors. Operates within the Control Plane.

Classes

UsersManager

Manages user accounts and security settings on a Xovis device.

This manager handles the full user lifecycle, including session management, password updates, account activation, and factory reset authorization (SMK).

Source code in src/xovis/api/device/resources/users.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class UsersManager:
    """
    Manages user accounts and security settings on a Xovis device.

    This manager handles the full user lifecycle, including session management,
    password updates, account activation, and factory reset authorization (SMK).
    """

    def __init__(self, http_client: XovisHTTPClient, client: "DeviceClient" = None) -> None:
        """
        Initializes the UsersManager.

        Args:
            http_client (XovisHTTPClient): The resilient HTTP client.
            client (DeviceClient): The parent DeviceClient instance.
        """
        self._http = http_client
        self._client = client
        self._base_path = "/api/v5/users"

    @property
    def models(self):
        """Returns the appropriate Pydantic models for the current device firmware."""
        return self._client.models if self._client else stable_models

    async def get_all(self) -> Any:
        """
        Retrieves the list of all configured user accounts.

        Returns:
            UserDetails: A collection of user account details.
        """
        response = await self._http.get(self._base_path)
        return self.models.UserDetails.model_validate(response.json())

    async def apply_factory_defaults(self) -> None:
        """Applies factory default user configurations."""
        await self._http.delete(self._base_path)

    async def get_current(self) -> Any:
        """
        Retrieves details for the currently authenticated user.

        Returns:
            UserDetail: The active user's account details.
        """
        response = await self._http.get(f"{self._base_path}/current")
        return self.models.UserDetail.model_validate(response.json())

    async def update_current_password(self, credentials: Any) -> None:
        """
        Updates the password for the current user.

        Args:
            credentials (UserCredentials): The new password details.
        """
        await self._http.put(f"{self._base_path}/current/password", json=credentials)

    async def login(self) -> Any:
        """
        Authenticates the user and creates a new session.

        Returns:
            UserSession: The details of the newly created session.
        """
        response = await self._http.post(f"{self._base_path}/login")
        return self.models.UserSession.model_validate(response.json())

    async def logout(self) -> None:
        """Terminates the current user session."""
        await self._http.post(f"{self._base_path}/logout")

    async def reset(self, smk: Any) -> None:
        """
        Applies factory defaults using a Secure Management Key (SMK).

        Args:
            smk (UserSmk): The SMK authorization payload.
        """
        await self._http.post(f"{self._base_path}/reset", json=smk)

    async def get_user(self, user_id: str) -> Any:
        """
        Retrieves details for a specific user account.

        Args:
            user_id (str): The identifier of the user.

        Returns:
            UserDetail: The requested user's account details.
        """
        response = await self._http.get(f"{self._base_path}/{user_id}")
        return self.models.UserDetail.model_validate(response.json())

    async def update_activation(self, user_id: str, activation: Any) -> Any:
        """
        Updates the activation state of a specific user account.

        Args:
            user_id (str): The identifier of the user.
            activation (UserActivation): The new activation state.

        Returns:
            UserActivation: The updated activation state.
        """
        response = await self._http.put(f"{self._base_path}/{user_id}/activation", json=activation)
        return self.models.UserActivation.model_validate(response.json())

    async def reset_password(self, user_id: str) -> None:
        """
        Resets a user's password to the factory default.

        Args:
            user_id (str): The identifier of the user.
        """
        await self._http.delete(f"{self._base_path}/{user_id}/password")
Attributes
models property

Returns the appropriate Pydantic models for the current device firmware.

Methods:
__init__(http_client, client=None)

Initializes the UsersManager.

Parameters:

Name Type Description Default
http_client XovisHTTPClient

The resilient HTTP client.

required
client DeviceClient

The parent DeviceClient instance.

None
Source code in src/xovis/api/device/resources/users.py
25
26
27
28
29
30
31
32
33
34
35
def __init__(self, http_client: XovisHTTPClient, client: "DeviceClient" = None) -> None:
    """
    Initializes the UsersManager.

    Args:
        http_client (XovisHTTPClient): The resilient HTTP client.
        client (DeviceClient): The parent DeviceClient instance.
    """
    self._http = http_client
    self._client = client
    self._base_path = "/api/v5/users"
apply_factory_defaults() async

Applies factory default user configurations.

Source code in src/xovis/api/device/resources/users.py
52
53
54
async def apply_factory_defaults(self) -> None:
    """Applies factory default user configurations."""
    await self._http.delete(self._base_path)
get_all() async

Retrieves the list of all configured user accounts.

Returns:

Name Type Description
UserDetails Any

A collection of user account details.

Source code in src/xovis/api/device/resources/users.py
42
43
44
45
46
47
48
49
50
async def get_all(self) -> Any:
    """
    Retrieves the list of all configured user accounts.

    Returns:
        UserDetails: A collection of user account details.
    """
    response = await self._http.get(self._base_path)
    return self.models.UserDetails.model_validate(response.json())
get_current() async

Retrieves details for the currently authenticated user.

Returns:

Name Type Description
UserDetail Any

The active user's account details.

Source code in src/xovis/api/device/resources/users.py
56
57
58
59
60
61
62
63
64
async def get_current(self) -> Any:
    """
    Retrieves details for the currently authenticated user.

    Returns:
        UserDetail: The active user's account details.
    """
    response = await self._http.get(f"{self._base_path}/current")
    return self.models.UserDetail.model_validate(response.json())
get_user(user_id) async

Retrieves details for a specific user account.

Parameters:

Name Type Description Default
user_id str

The identifier of the user.

required

Returns:

Name Type Description
UserDetail Any

The requested user's account details.

Source code in src/xovis/api/device/resources/users.py
 98
 99
100
101
102
103
104
105
106
107
108
109
async def get_user(self, user_id: str) -> Any:
    """
    Retrieves details for a specific user account.

    Args:
        user_id (str): The identifier of the user.

    Returns:
        UserDetail: The requested user's account details.
    """
    response = await self._http.get(f"{self._base_path}/{user_id}")
    return self.models.UserDetail.model_validate(response.json())
login() async

Authenticates the user and creates a new session.

Returns:

Name Type Description
UserSession Any

The details of the newly created session.

Source code in src/xovis/api/device/resources/users.py
75
76
77
78
79
80
81
82
83
async def login(self) -> Any:
    """
    Authenticates the user and creates a new session.

    Returns:
        UserSession: The details of the newly created session.
    """
    response = await self._http.post(f"{self._base_path}/login")
    return self.models.UserSession.model_validate(response.json())
logout() async

Terminates the current user session.

Source code in src/xovis/api/device/resources/users.py
85
86
87
async def logout(self) -> None:
    """Terminates the current user session."""
    await self._http.post(f"{self._base_path}/logout")
reset(smk) async

Applies factory defaults using a Secure Management Key (SMK).

Parameters:

Name Type Description Default
smk UserSmk

The SMK authorization payload.

required
Source code in src/xovis/api/device/resources/users.py
89
90
91
92
93
94
95
96
async def reset(self, smk: Any) -> None:
    """
    Applies factory defaults using a Secure Management Key (SMK).

    Args:
        smk (UserSmk): The SMK authorization payload.
    """
    await self._http.post(f"{self._base_path}/reset", json=smk)
reset_password(user_id) async

Resets a user's password to the factory default.

Parameters:

Name Type Description Default
user_id str

The identifier of the user.

required
Source code in src/xovis/api/device/resources/users.py
125
126
127
128
129
130
131
132
async def reset_password(self, user_id: str) -> None:
    """
    Resets a user's password to the factory default.

    Args:
        user_id (str): The identifier of the user.
    """
    await self._http.delete(f"{self._base_path}/{user_id}/password")
update_activation(user_id, activation) async

Updates the activation state of a specific user account.

Parameters:

Name Type Description Default
user_id str

The identifier of the user.

required
activation UserActivation

The new activation state.

required

Returns:

Name Type Description
UserActivation Any

The updated activation state.

Source code in src/xovis/api/device/resources/users.py
111
112
113
114
115
116
117
118
119
120
121
122
123
async def update_activation(self, user_id: str, activation: Any) -> Any:
    """
    Updates the activation state of a specific user account.

    Args:
        user_id (str): The identifier of the user.
        activation (UserActivation): The new activation state.

    Returns:
        UserActivation: The updated activation state.
    """
    response = await self._http.put(f"{self._base_path}/{user_id}/activation", json=activation)
    return self.models.UserActivation.model_validate(response.json())
update_current_password(credentials) async

Updates the password for the current user.

Parameters:

Name Type Description Default
credentials UserCredentials

The new password details.

required
Source code in src/xovis/api/device/resources/users.py
66
67
68
69
70
71
72
73
async def update_current_password(self, credentials: Any) -> None:
    """
    Updates the password for the current user.

    Args:
        credentials (UserCredentials): The new password details.
    """
    await self._http.put(f"{self._base_path}/current/password", json=credentials)

xovis.api.device.resources.images

Xovis SDK - Image Management Resource

Provides the implementation for retrieving background, raw lens, stereo, and depth images from local edge sensors.

Classes

ImagesManager

Manages image retrieval for the single-sensor context.

Source code in src/xovis/api/device/resources/images.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class ImagesManager:
    """
    Manages image retrieval for the single-sensor context.
    """

    def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
        """
        Initializes the ImagesManager.

        Args:
            client (DeviceClient): The parent device client instance.
            target_id (Optional[str]): The multisensor target ID, if applicable.
        """
        self._client = client
        self._http = client._http_client
        self.target_id = target_id

    def _resolve_path(self, perspective: str = "scene") -> str:
        """Resolves the base path for images."""
        if self.target_id:
            return f"/api/v5/multisensors/{self.target_id}/{perspective}/images"
        return f"/api/v5/singlesensor/{perspective}/images"

    async def get_raw_left(self) -> bytes:
        """
        Fetches the raw left lens image.

        Returns:
            bytes: The raw JPEG image binary data.
        """
        if self.target_id:
            # Spiders don't have raw lenses in multisensor context usually,
            # but we'll follow the pattern if possible.
            # Actually, raw_left is usually only at root.
            pass
        response = await self._http.get("/api/v5/singlesensor/images/raw_left.jpg")
        return response.content

    async def get_raw_right(self) -> bytes:
        """
        Fetches the raw right lens image.

        Returns:
            bytes: The raw JPEG image binary data.
        """
        response = await self._http.get("/api/v5/singlesensor/images/raw_right.jpg")
        return response.content

    async def get_stereo(self) -> bytes:
        """
        Fetches the stereo image.

        Returns:
            bytes: The PNG image binary data.
        """
        response = await self._http.get("/api/v5/singlesensor/images/stereo.png")
        return response.content

    async def get_depth_map(self, colored: bool = False) -> bytes:
        """
        Fetches the depth map image.

        Args:
            colored (bool): If True, requests the colored depth map.
                Defaults to False.

        Returns:
            bytes: The depth map PNG binary data.
        """
        endpoint = (
            "/api/v5/singlesensor/experimental/view/images/depth_color.png" if colored else "/api/v5/singlesensor/experimental/view/images/depth.png"
        )
        response = await self._http.get(endpoint)
        return response.content

    async def get_background(self, perspective: str = "scene") -> tuple[bytes, dict[str, Any]]:
        """
        Fetches the static background image.

        Args:
            perspective (str): The coordinate projection to use ("scene" or "view").
                Defaults to "scene".

        Returns:
            Tuple[bytes, Dict[str, Any]]: A tuple containing the JPEG binary data
                and a dictionary of the parsed image metadata.
        """
        response = await self._http.get(f"{self._resolve_path(perspective)}/background.jpg")
        metadata = json.loads(response.headers.get("x-image-metadata", "{}"))
        return response.content, metadata

    async def get_background_tarball(
        self,
        perspective: str = "scene",
        json_int64_workaround: bool = False,
        tracked_objects: bool = True,
        events: bool = True,
    ) -> bytes:
        """
        Fetches the static background image and metadata as a raw tarball.

        Args:
            perspective (str): The coordinate projection to use ("scene" or "view"). Defaults to "scene".
            json_int64_workaround (bool): Include workaround for 64-bit JSON ints. Defaults to False.
            tracked_objects (bool): Include tracked objects overlay data. Defaults to True.
            events (bool): Include events overlay data. Defaults to True.

        Returns:
            bytes: The raw tarball binary data containing 'image.jpg' and 'X-Image-Metadata.json'.
        """
        params = {
            "json_int64_workaround": str(json_int64_workaround).lower(),
            "tracked_objects": str(tracked_objects).lower(),
            "events": str(events).lower(),
        }
        headers = {"accept": "application/x-tar"}
        response = await self._http.get(f"{self._resolve_path(perspective)}/background.tar", params=params, headers=headers)
        return response.content

    async def get_live(self, perspective: str = "scene") -> tuple[bytes, dict[str, Any]]:
        """
        Fetches the live image.

        Args:
            perspective (str): The coordinate projection to use ("scene" or "view").
                Defaults to "scene".

        Returns:
            Tuple[bytes, Dict[str, Any]]: A tuple containing the JPEG binary data
                and a dictionary of the parsed image metadata.
        """
        response = await self._http.get(f"{self._resolve_path(perspective)}/live.jpg")
        metadata = json.loads(response.headers.get("x-image-metadata", "{}"))
        return response.content, metadata

    async def reset_background(self, perspective: str = "scene") -> None:
        """
        Resets the background image.

        Args:
            perspective (str): The projection context to reset. Defaults to "scene".
        """
        await self._http.post(f"{self._resolve_path(perspective)}/background/reset")

    async def update_settings_image(self) -> None:
        """
        Updates the settings image used for configuration.
        """
        await self._http.post("/api/v5/singlesensor/settings/image/update")
Methods:
__init__(client, target_id=None)

Initializes the ImagesManager.

Parameters:

Name Type Description Default
client DeviceClient

The parent device client instance.

required
target_id Optional[str]

The multisensor target ID, if applicable.

None
Source code in src/xovis/api/device/resources/images.py
20
21
22
23
24
25
26
27
28
29
30
def __init__(self, client: "DeviceClient", target_id: Optional[str] = None) -> None:
    """
    Initializes the ImagesManager.

    Args:
        client (DeviceClient): The parent device client instance.
        target_id (Optional[str]): The multisensor target ID, if applicable.
    """
    self._client = client
    self._http = client._http_client
    self.target_id = target_id
_resolve_path(perspective='scene')

Resolves the base path for images.

Source code in src/xovis/api/device/resources/images.py
32
33
34
35
36
def _resolve_path(self, perspective: str = "scene") -> str:
    """Resolves the base path for images."""
    if self.target_id:
        return f"/api/v5/multisensors/{self.target_id}/{perspective}/images"
    return f"/api/v5/singlesensor/{perspective}/images"
get_background(perspective='scene') async

Fetches the static background image.

Parameters:

Name Type Description Default
perspective str

The coordinate projection to use ("scene" or "view"). Defaults to "scene".

'scene'

Returns:

Type Description
tuple[bytes, dict[str, Any]]

Tuple[bytes, Dict[str, Any]]: A tuple containing the JPEG binary data and a dictionary of the parsed image metadata.

Source code in src/xovis/api/device/resources/images.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
async def get_background(self, perspective: str = "scene") -> tuple[bytes, dict[str, Any]]:
    """
    Fetches the static background image.

    Args:
        perspective (str): The coordinate projection to use ("scene" or "view").
            Defaults to "scene".

    Returns:
        Tuple[bytes, Dict[str, Any]]: A tuple containing the JPEG binary data
            and a dictionary of the parsed image metadata.
    """
    response = await self._http.get(f"{self._resolve_path(perspective)}/background.jpg")
    metadata = json.loads(response.headers.get("x-image-metadata", "{}"))
    return response.content, metadata
get_background_tarball(perspective='scene', json_int64_workaround=False, tracked_objects=True, events=True) async

Fetches the static background image and metadata as a raw tarball.

Parameters:

Name Type Description Default
perspective str

The coordinate projection to use ("scene" or "view"). Defaults to "scene".

'scene'
json_int64_workaround bool

Include workaround for 64-bit JSON ints. Defaults to False.

False
tracked_objects bool

Include tracked objects overlay data. Defaults to True.

True
events bool

Include events overlay data. Defaults to True.

True

Returns:

Name Type Description
bytes bytes

The raw tarball binary data containing 'image.jpg' and 'X-Image-Metadata.json'.

Source code in src/xovis/api/device/resources/images.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
async def get_background_tarball(
    self,
    perspective: str = "scene",
    json_int64_workaround: bool = False,
    tracked_objects: bool = True,
    events: bool = True,
) -> bytes:
    """
    Fetches the static background image and metadata as a raw tarball.

    Args:
        perspective (str): The coordinate projection to use ("scene" or "view"). Defaults to "scene".
        json_int64_workaround (bool): Include workaround for 64-bit JSON ints. Defaults to False.
        tracked_objects (bool): Include tracked objects overlay data. Defaults to True.
        events (bool): Include events overlay data. Defaults to True.

    Returns:
        bytes: The raw tarball binary data containing 'image.jpg' and 'X-Image-Metadata.json'.
    """
    params = {
        "json_int64_workaround": str(json_int64_workaround).lower(),
        "tracked_objects": str(tracked_objects).lower(),
        "events": str(events).lower(),
    }
    headers = {"accept": "application/x-tar"}
    response = await self._http.get(f"{self._resolve_path(perspective)}/background.tar", params=params, headers=headers)
    return response.content
get_depth_map(colored=False) async

Fetches the depth map image.

Parameters:

Name Type Description Default
colored bool

If True, requests the colored depth map. Defaults to False.

False

Returns:

Name Type Description
bytes bytes

The depth map PNG binary data.

Source code in src/xovis/api/device/resources/images.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
async def get_depth_map(self, colored: bool = False) -> bytes:
    """
    Fetches the depth map image.

    Args:
        colored (bool): If True, requests the colored depth map.
            Defaults to False.

    Returns:
        bytes: The depth map PNG binary data.
    """
    endpoint = (
        "/api/v5/singlesensor/experimental/view/images/depth_color.png" if colored else "/api/v5/singlesensor/experimental/view/images/depth.png"
    )
    response = await self._http.get(endpoint)
    return response.content
get_live(perspective='scene') async

Fetches the live image.

Parameters:

Name Type Description Default
perspective str

The coordinate projection to use ("scene" or "view"). Defaults to "scene".

'scene'

Returns:

Type Description
tuple[bytes, dict[str, Any]]

Tuple[bytes, Dict[str, Any]]: A tuple containing the JPEG binary data and a dictionary of the parsed image metadata.

Source code in src/xovis/api/device/resources/images.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
async def get_live(self, perspective: str = "scene") -> tuple[bytes, dict[str, Any]]:
    """
    Fetches the live image.

    Args:
        perspective (str): The coordinate projection to use ("scene" or "view").
            Defaults to "scene".

    Returns:
        Tuple[bytes, Dict[str, Any]]: A tuple containing the JPEG binary data
            and a dictionary of the parsed image metadata.
    """
    response = await self._http.get(f"{self._resolve_path(perspective)}/live.jpg")
    metadata = json.loads(response.headers.get("x-image-metadata", "{}"))
    return response.content, metadata
get_raw_left() async

Fetches the raw left lens image.

Returns:

Name Type Description
bytes bytes

The raw JPEG image binary data.

Source code in src/xovis/api/device/resources/images.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
async def get_raw_left(self) -> bytes:
    """
    Fetches the raw left lens image.

    Returns:
        bytes: The raw JPEG image binary data.
    """
    if self.target_id:
        # Spiders don't have raw lenses in multisensor context usually,
        # but we'll follow the pattern if possible.
        # Actually, raw_left is usually only at root.
        pass
    response = await self._http.get("/api/v5/singlesensor/images/raw_left.jpg")
    return response.content
get_raw_right() async

Fetches the raw right lens image.

Returns:

Name Type Description
bytes bytes

The raw JPEG image binary data.

Source code in src/xovis/api/device/resources/images.py
53
54
55
56
57
58
59
60
61
async def get_raw_right(self) -> bytes:
    """
    Fetches the raw right lens image.

    Returns:
        bytes: The raw JPEG image binary data.
    """
    response = await self._http.get("/api/v5/singlesensor/images/raw_right.jpg")
    return response.content
get_stereo() async

Fetches the stereo image.

Returns:

Name Type Description
bytes bytes

The PNG image binary data.

Source code in src/xovis/api/device/resources/images.py
63
64
65
66
67
68
69
70
71
async def get_stereo(self) -> bytes:
    """
    Fetches the stereo image.

    Returns:
        bytes: The PNG image binary data.
    """
    response = await self._http.get("/api/v5/singlesensor/images/stereo.png")
    return response.content
reset_background(perspective='scene') async

Resets the background image.

Parameters:

Name Type Description Default
perspective str

The projection context to reset. Defaults to "scene".

'scene'
Source code in src/xovis/api/device/resources/images.py
150
151
152
153
154
155
156
157
async def reset_background(self, perspective: str = "scene") -> None:
    """
    Resets the background image.

    Args:
        perspective (str): The projection context to reset. Defaults to "scene".
    """
    await self._http.post(f"{self._resolve_path(perspective)}/background/reset")
update_settings_image() async

Updates the settings image used for configuration.

Source code in src/xovis/api/device/resources/images.py
159
160
161
162
163
async def update_settings_image(self) -> None:
    """
    Updates the settings image used for configuration.
    """
    await self._http.post("/api/v5/singlesensor/settings/image/update")