Skip to content

Commit 83b5f45

Browse files
authored
feat: Asset Utilization API methods (#178)
1 parent 2afb3ee commit 83b5f45

14 files changed

+885
-9
lines changed

docs/api_reference/assetmanagement.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ nisystemlink.clients.assetmanagement
1111
.. automethod:: query_assets
1212
.. automethod:: delete_assets
1313
.. automethod:: link_files
14+
.. automethod:: start_utilization
15+
.. automethod:: utilization_heartbeat
16+
.. automethod:: end_utilization
17+
.. automethod:: query_asset_utilization_history
1418

1519
.. automodule:: nisystemlink.clients.assetmanagement.models
1620
:members:

docs/getting_started.rst

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,17 +387,25 @@ default connection. The default connection depends on your environment.
387387

388388
With a :class:`.AssetManagementClient` object, you can:
389389

390-
* Create, delete, get the list of assets and link files to assets.
390+
* Create, delete, query assets and link files to assets.
391+
392+
* Track asset utilization with start, heartbeat, end, and query history operations.
391393

392394
Examples
393395
~~~~~~~~
394396

395-
create, delete, query asset and link files to assets.
397+
Create, delete, and query assets and link files to assets.
396398

397399
.. literalinclude:: ../examples/assetmanagement/assets.py
398400
:language: python
399401
:linenos:
400402

403+
Track asset utilization.
404+
405+
.. literalinclude:: ../examples/assetmanagement/asset_utilization.py
406+
:language: python
407+
:linenos:
408+
401409
Systems API
402410
-------
403411

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import threading
2+
import time
3+
from datetime import datetime
4+
from uuid import uuid4
5+
6+
from nisystemlink.clients.assetmanagement import AssetManagementClient
7+
from nisystemlink.clients.assetmanagement.models import (
8+
AssetBusType,
9+
AssetIdentification,
10+
AssetLocationForCreate,
11+
AssetPresence,
12+
AssetPresenceStatus,
13+
CreateAssetRequest,
14+
StartUtilizationRequest,
15+
)
16+
from nisystemlink.clients.core import HttpConfiguration
17+
from nisystemlink.clients.core.helpers import read_minion_id
18+
19+
# Configure connection to SystemLink server
20+
server_configuration = HttpConfiguration(
21+
server_uri="https://yourserver.yourcompany.com",
22+
api_key="YourAPIKeyGeneratedFromSystemLink",
23+
)
24+
25+
client = AssetManagementClient(configuration=server_configuration)
26+
27+
# Generate a unique identifier for this utilization session
28+
utilization_id = str(uuid4())
29+
30+
# Create the assets first
31+
# Define the assets to be created and used in the test
32+
create_assets_request = [
33+
CreateAssetRequest(
34+
model_name="NI PXIe-6368",
35+
model_number=4000,
36+
serial_number="01BB877A",
37+
vendor_name="NI",
38+
vendor_number=4244,
39+
bus_type=AssetBusType.ACCESSORY,
40+
name="DAQ Device - 01BB877A",
41+
location=AssetLocationForCreate(
42+
state=AssetPresence(asset_presence=AssetPresenceStatus.PRESENT)
43+
),
44+
),
45+
CreateAssetRequest(
46+
model_name="NI PXIe-5163",
47+
model_number=5000,
48+
serial_number="02CC988B",
49+
vendor_name="NI",
50+
vendor_number=4244,
51+
bus_type=AssetBusType.ACCESSORY,
52+
name="Oscilloscope - 02CC988B",
53+
location=AssetLocationForCreate(
54+
state=AssetPresence(asset_presence=AssetPresenceStatus.PRESENT)
55+
),
56+
),
57+
]
58+
59+
# Create the assets in SystemLink
60+
create_assets_response = client.create_assets(assets=create_assets_request)
61+
62+
if create_assets_response.assets:
63+
print(f"Created {len(create_assets_response.assets)} asset(s)")
64+
created_asset_ids = [
65+
asset.id for asset in create_assets_response.assets if asset.id
66+
]
67+
else:
68+
print("Failed to create assets")
69+
exit(1)
70+
71+
# Define the asset identifications for utilization tracking
72+
test_assets = [
73+
AssetIdentification(
74+
model_name="NI PXIe-6368",
75+
model_number=4000,
76+
serial_number="01BB877A",
77+
vendor_name="NI",
78+
vendor_number=4244,
79+
bus_type=AssetBusType.ACCESSORY,
80+
),
81+
AssetIdentification(
82+
model_name="NI PXIe-5163",
83+
model_number=5000,
84+
serial_number="02CC988B",
85+
vendor_name="NI",
86+
vendor_number=4244,
87+
bus_type=AssetBusType.ACCESSORY,
88+
),
89+
]
90+
91+
# Start asset utilization tracking
92+
# This marks the assets as "in use" in the SystemLink UI
93+
# Read the minion ID from the Salt configuration
94+
minion_id = read_minion_id() or "test-station-01" # Fallback minion ID if not found
95+
96+
start_utilization_request = StartUtilizationRequest(
97+
utilization_identifier=utilization_id,
98+
minion_id=minion_id,
99+
asset_identifications=test_assets,
100+
utilization_category="Automated Testing",
101+
task_name="DUT Validation Suite",
102+
user_name="automation_user",
103+
utilization_timestamp=datetime.now(),
104+
)
105+
106+
start_utilization_response = client.start_utilization(request=start_utilization_request)
107+
108+
print(start_utilization_response)
109+
110+
# Verify utilization started successfully
111+
if start_utilization_response.assets_with_started_utilization:
112+
print(
113+
f"Utilization started for {len(start_utilization_response.assets_with_started_utilization)} asset(s)"
114+
)
115+
else:
116+
print("Failed to start utilization")
117+
118+
119+
# Heartbeat mechanism using a background thread
120+
# IMPORTANT: Heartbeats are for UI purposes only, to keep assets visually
121+
# marked as "in use" in the SystemLink UI. This applies only to utilizations
122+
# that have not been ended. While the standard heartbeat interval is 5 minutes,
123+
# the UI requires heartbeats at least every 10 minutes to continue showing
124+
# assets as actively utilized. If heartbeats stop, the assets will no longer
125+
# appear as "in use" in the UI.
126+
heartbeat_interval = 300 # 5 minutes in seconds
127+
stop_event = threading.Event()
128+
129+
130+
def heartbeat_loop():
131+
"""Background thread that sends periodic heartbeats.
132+
133+
This keeps the asset visually marked as "in use" in the SystemLink UI
134+
(for UI purposes only). Applies only to utilizations that have not been ended.
135+
The UI requires heartbeats at least every 10 minutes to continue displaying
136+
the asset as actively utilized.
137+
"""
138+
while not stop_event.wait(heartbeat_interval):
139+
heartbeat_response = client.utilization_heartbeat(
140+
ids=[utilization_id],
141+
timestamp=datetime.now(),
142+
)
143+
144+
if heartbeat_response.updated_utilization_ids:
145+
print(
146+
f"Heartbeat sent at {datetime.now().strftime('%H:%M:%S')} - asset remains 'in use'"
147+
)
148+
149+
150+
# Start the heartbeat thread
151+
heartbeat_thread = threading.Thread(target=heartbeat_loop, daemon=True)
152+
heartbeat_thread.start()
153+
154+
# Simulate a long-running operation where assets are in use
155+
# In a real scenario, this would be your actual test or operation
156+
print("\nAssets are now in use. Heartbeats will be sent every 5 minutes...")
157+
print("Waiting for 10 minutes to demonstrate heartbeats...\n")
158+
159+
time.sleep(600) # Wait 10 minutes
160+
161+
# Stop the heartbeat thread
162+
stop_event.set()
163+
heartbeat_thread.join()
164+
165+
# End asset utilization tracking
166+
end_utilization_response = client.end_utilization(
167+
ids=[utilization_id],
168+
timestamp=datetime.now(),
169+
)
170+
171+
if end_utilization_response.updated_utilization_ids:
172+
print("\nUtilization ended - asset(s) released")
173+
174+
# Clean up: delete the created assets
175+
if created_asset_ids:
176+
client.delete_assets(ids=created_asset_ids)
177+
print(f"Deleted {len(created_asset_ids)} asset(s)")

nisystemlink/clients/assetmanagement/_asset_management_client.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
"""Implementation of AssetManagementClient."""
22

3-
from typing import List
3+
from datetime import datetime
4+
from typing import List, Optional
45

56
from nisystemlink.clients import core
7+
from nisystemlink.clients.assetmanagement.models._update_utilization_request import (
8+
_UpdateUtilizationRequest,
9+
)
610
from nisystemlink.clients.core._http_configuration import HttpConfiguration
711
from nisystemlink.clients.core._uplink._base_client import BaseClient
812
from nisystemlink.clients.core._uplink._methods import post
@@ -141,3 +145,121 @@ def link_files(
141145
ApiException: If unable to communicate with the asset management service or if there are invalid arguments.
142146
"""
143147
...
148+
149+
@post("query-asset-utilization-history")
150+
def query_asset_utilization_history(
151+
self, request: models.QueryAssetUtilizationHistoryRequest
152+
) -> models.AssetUtilizationHistoryResponse:
153+
"""Query asset utilization history.
154+
155+
Args:
156+
request: Object containing filters for asset utilization and assets, including
157+
utilization_filter, asset_filter, date range, and pagination options.
158+
159+
Returns:
160+
AssetUtilizationHistoryResponse: Response containing the list of asset utilization
161+
history records that match the query, along with optional continuation token for pagination.
162+
163+
Raises:
164+
ApiException: If unable to communicate with the asset management service or if there are invalid arguments.
165+
"""
166+
...
167+
168+
@post("assets/start-utilization")
169+
def start_utilization(
170+
self, request: models.StartUtilizationRequest
171+
) -> models.StartUtilizationPartialSuccessResponse:
172+
"""Start asset utilization tracking.
173+
174+
Args:
175+
request: Object containing the utilization identifier, minion ID, asset identifications,
176+
utilization category, task name, user name, and utilization timestamp.
177+
178+
Returns:
179+
StartUtilizationPartialSuccessResponse: Response containing arrays of assets that successfully
180+
started utilization and those that failed, along with error information if any failures occurred.
181+
182+
Raises:
183+
ApiException: If unable to communicate with the asset management service or if there are invalid arguments.
184+
"""
185+
...
186+
187+
@post("assets/end-utilization")
188+
def __end_utilization(
189+
self, request: _UpdateUtilizationRequest
190+
) -> models.UpdateUtilizationPartialSuccessResponse:
191+
"""End asset utilization tracking.
192+
193+
Args:
194+
request: The request object containing utilization identifiers and timestamp.
195+
196+
Returns:
197+
UpdateUtilizationPartialSuccessResponse: Response containing updated utilization IDs.
198+
"""
199+
...
200+
201+
def end_utilization(
202+
self,
203+
ids: List[str],
204+
timestamp: Optional[datetime] = None,
205+
) -> models.UpdateUtilizationPartialSuccessResponse:
206+
"""End asset utilization tracking.
207+
208+
Args:
209+
ids: Array of utilization identifiers representing the unique identifier
210+
of asset utilization history records to end.
211+
timestamp: The timestamp to use when ending the utilization.
212+
If not provided, the current server time will be used.
213+
214+
Returns:
215+
UpdateUtilizationPartialSuccessResponse: Response containing arrays of utilization IDs that were
216+
successfully updated and those that failed, along with error information if any failures occurred.
217+
218+
Raises:
219+
ApiException: If unable to communicate with the asset management service or if there are invalid arguments.
220+
"""
221+
request = _UpdateUtilizationRequest(
222+
utilization_identifiers=ids,
223+
utilization_timestamp=timestamp,
224+
)
225+
return self.__end_utilization(request)
226+
227+
@post("assets/utilization-heartbeat")
228+
def __utilization_heartbeat(
229+
self, request: _UpdateUtilizationRequest
230+
) -> models.UpdateUtilizationPartialSuccessResponse:
231+
"""Send utilization heartbeat.
232+
233+
Args:
234+
request: The request object containing utilization identifiers and timestamp.
235+
236+
Returns:
237+
UpdateUtilizationPartialSuccessResponse: Response containing updated utilization IDs.
238+
"""
239+
...
240+
241+
def utilization_heartbeat(
242+
self,
243+
ids: List[str],
244+
timestamp: Optional[datetime] = None,
245+
) -> models.UpdateUtilizationPartialSuccessResponse:
246+
"""Send utilization heartbeat to update asset utilization tracking.
247+
248+
Args:
249+
ids: Array of utilization identifiers representing the unique identifier
250+
of asset utilization history records to update.
251+
timestamp: The timestamp to use for the heartbeat.
252+
If not provided, the current server time will be used.
253+
254+
Returns:
255+
UpdateUtilizationPartialSuccessResponse: Response containing arrays of utilization IDs that were
256+
successfully updated and those that failed, along with error information if any failures occurred.
257+
258+
Raises:
259+
ApiException: If unable to communicate with the asset management service or if there are invalid arguments.
260+
"""
261+
request = _UpdateUtilizationRequest(
262+
utilization_identifiers=ids,
263+
utilization_timestamp=timestamp,
264+
)
265+
return self.__utilization_heartbeat(request)

nisystemlink/clients/assetmanagement/models/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,19 @@
2121
)
2222
from ._asset_types import AssetBusType, AssetDiscoveryType, AssetType
2323
from ._link_files_partial_success_response import LinkFilesPartialSuccessResponse
24+
from ._query_asset_utilization_history_request import (
25+
QueryAssetUtilizationHistoryRequest,
26+
UtilizationOrderBy,
27+
)
28+
from ._asset_utilization_history_item import AssetUtilizationHistoryItem
29+
from ._asset_utilization_history_response import AssetUtilizationHistoryResponse
30+
from ._asset_identification import AssetIdentification
31+
from ._start_utilization_request import StartUtilizationRequest
32+
from ._start_utilization_partial_success_response import (
33+
StartUtilizationPartialSuccessResponse,
34+
)
35+
from ._update_utilization_partial_success_response import (
36+
UpdateUtilizationPartialSuccessResponse,
37+
)
2438

2539
# flake8: noqa

0 commit comments

Comments
 (0)