cresnextws 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,564 @@
1
+ Metadata-Version: 2.1
2
+ Name: cresnextws
3
+ Version: 0.1.0
4
+ Summary: Crestron CresNext WebSocket API Client
5
+ Author: Scott Shanafelt
6
+ Project-URL: Homepage, https://github.com/jetsoncontrols/cresnextws
7
+ Project-URL: Bug Reports, https://github.com/jetsoncontrols/cresnextws/issues
8
+ Project-URL: Source, https://github.com/jetsoncontrols/cresnextws
9
+ Keywords: crestron,cresnext,websocket,api,client
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: System :: Hardware :: Hardware Drivers
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: dev
24
+
25
+ # cresnextws
26
+
27
+ Crestron CresNext WebSocket API Client
28
+
29
+ A Python library for interacting with Crestron CresNext systems via WebSocket API.
30
+
31
+ ## Installation
32
+
33
+ Install from PyPI (when published):
34
+
35
+ ```bash
36
+ pip install cresnextws
37
+ ```
38
+
39
+ Or install from source:
40
+
41
+ ```bash
42
+ git clone https://github.com/jetsoncontrols/cresnextws.git
43
+ cd cresnextws
44
+ pip install .
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ### Basic HTTP Operations
50
+
51
+ ```python
52
+ import asyncio
53
+ from cresnextws import CresNextWSClient, ClientConfig
54
+
55
+ async def main():
56
+ # Create configuration (required)
57
+ config = ClientConfig(
58
+ host="your-cresnext-host.local",
59
+ username="your_username",
60
+ password="your_password",
61
+ auto_reconnect=True # Enable automatic reconnection
62
+ )
63
+
64
+ # Create client instance with config
65
+ client = CresNextWSClient(config)
66
+
67
+ # Connect to the system
68
+ await client.connect()
69
+
70
+ # HTTP GET request
71
+ response = await client.http_get("/Device/Ethernet/HostName")
72
+ print(f"Hostname: {response}")
73
+
74
+ # HTTP POST request (update configuration)
75
+ data = {"Device": {"Ethernet": {"HostName": "new-hostname"}}}
76
+ response = await client.http_post("/Device/Ethernet/HostName", data)
77
+ print(f"Update response: {response}")
78
+
79
+ # Disconnect when done
80
+ await client.disconnect()
81
+
82
+ # Run the example
83
+ asyncio.run(main())
84
+ ```
85
+
86
+ ### Health Check Configuration
87
+
88
+ The library includes a health check mechanism to detect stale connections (particularly after system sleep/wake cycles) and automatically trigger reconnection:
89
+
90
+ ```python
91
+ import asyncio
92
+ from cresnextws import CresNextWSClient, ClientConfig
93
+
94
+ async def main():
95
+ config = ClientConfig(
96
+ host="your-cresnext-host.local",
97
+ username="your_username",
98
+ password="your_password",
99
+ auto_reconnect=True, # Enable automatic reconnection (required for health check)
100
+ health_check_interval=30.0, # Check connection health every 30 seconds (default)
101
+ health_check_timeout=5.0 # Health check ping timeout in seconds (default)
102
+ )
103
+
104
+ client = CresNextWSClient(config)
105
+ await client.connect()
106
+
107
+ # Health check runs automatically in the background
108
+ # If a ping fails or times out, it will trigger reconnection
109
+
110
+ # Your application logic here...
111
+ await asyncio.sleep(300) # Run for 5 minutes
112
+
113
+ await client.disconnect()
114
+
115
+ asyncio.run(main())
116
+ ```
117
+
118
+ **Health Check Features:**
119
+ - **Automatic Detection**: Detects stale WebSocket connections after system sleep/wake cycles
120
+ - **Configurable Intervals**: Customize how often to check connection health
121
+ - **Timeout Handling**: Configurable timeout for ping responses
122
+ - **Seamless Integration**: Works alongside existing reconnection system
123
+ - **Zero Configuration**: Enabled by default with sensible defaults when `auto_reconnect=True`
124
+
125
+ **Note**: Health check only runs when `auto_reconnect=True`. If auto-reconnection is disabled, health checks are automatically disabled as well.
126
+ ```
127
+
128
+ ### WebSocket Operations
129
+
130
+ ```python
131
+ import asyncio
132
+ from cresnextws import CresNextWSClient, ClientConfig
133
+
134
+ async def main():
135
+ config = ClientConfig(
136
+ host="your-cresnext-host.local",
137
+ username="your_username",
138
+ password="your_password",
139
+ health_check_interval=30.0, # Ping every 30 seconds (default)
140
+ health_check_timeout=5.0 # 5 second ping timeout (default)
141
+ )
142
+
143
+ async with CresNextWSClient(config) as client:
144
+ # WebSocket GET - subscribe to data updates
145
+ await client.ws_get("/Device/DeviceInfo/Model")
146
+
147
+ # WebSocket POST - send configuration updates
148
+ data = {"Device": {"Config": {"SomeValue": "new_value"}}}
149
+ await client.ws_post(data)
150
+
151
+ # Listen for incoming messages
152
+ message = await client.next_message(timeout=5.0)
153
+ print(f"Received: {message}")
154
+
155
+ asyncio.run(main())
156
+ ```
157
+
158
+ ### Connection Status Events
159
+
160
+ Monitor connection state changes with event callbacks:
161
+
162
+ ```python
163
+ import asyncio
164
+ from cresnextws import CresNextWSClient, ClientConfig, ConnectionStatus
165
+
166
+ def on_status_change(status: ConnectionStatus):
167
+ if status == ConnectionStatus.CONNECTED:
168
+ print("🟢 Connected to device!")
169
+ elif status == ConnectionStatus.DISCONNECTED:
170
+ print("🔴 Disconnected from device")
171
+ elif status == ConnectionStatus.CONNECTING:
172
+ print("🟡 Connecting...")
173
+ elif status == ConnectionStatus.RECONNECTING:
174
+ print("🟠 Reconnecting...")
175
+
176
+ async def main():
177
+ config = ClientConfig(
178
+ host="your-cresnext-host.local",
179
+ username="your_username",
180
+ password="your_password",
181
+ auto_reconnect=True
182
+ )
183
+
184
+ client = CresNextWSClient(config)
185
+
186
+ # Subscribe to connection status events
187
+ client.add_connection_status_handler(on_status_change)
188
+
189
+ # Get current status
190
+ print(f"Current status: {client.get_connection_status()}")
191
+
192
+ # Connect (will trigger status events)
193
+ await client.connect()
194
+
195
+ # Your application logic here...
196
+
197
+ # Cleanup
198
+ client.remove_connection_status_handler(on_status_change)
199
+ await client.disconnect()
200
+
201
+ asyncio.run(main())
202
+ ```
203
+
204
+ ### Configuration and Utilities
205
+
206
+ Access configuration details and utility methods:
207
+
208
+ ```python
209
+ import asyncio
210
+ from cresnextws import CresNextWSClient, ClientConfig
211
+
212
+ async def main():
213
+ config = ClientConfig(
214
+ host="your-cresnext-host.local",
215
+ username="your_username",
216
+ password="your_password"
217
+ )
218
+
219
+ client = CresNextWSClient(config)
220
+
221
+ # Get the base HTTPS endpoint URL
222
+ base_url = client.get_base_endpoint()
223
+ print(f"Base endpoint: {base_url}") # Output: https://your-cresnext-host.local
224
+
225
+ # This is useful for constructing custom URLs or understanding the connection target
226
+ # The base endpoint is used internally for all HTTP requests and WebSocket origins
227
+
228
+ await client.connect()
229
+ # ... your application logic ...
230
+ await client.disconnect()
231
+
232
+ asyncio.run(main())
233
+ ```
234
+
235
+ ### DataEventManager - Real-time Monitoring
236
+
237
+ The `DataEventManager` provides automatic monitoring of WebSocket messages with path-based subscriptions:
238
+
239
+ ```python
240
+ import asyncio
241
+ from cresnextws import CresNextWSClient, ClientConfig, DataEventManager
242
+
243
+ async def main():
244
+ config = ClientConfig(
245
+ host="your-cresnext-host.local",
246
+ username="your_username",
247
+ password="your_password"
248
+ )
249
+
250
+ client = CresNextWSClient(config)
251
+ await client.connect()
252
+
253
+ # Create data event manager
254
+ data_manager = DataEventManager(client)
255
+
256
+ # Define callback function
257
+ def on_device_update(path: str, data):
258
+ print(f"Device updated: {path} = {data}")
259
+
260
+ def on_network_change(path: str, data):
261
+ print(f"Network change: {path} = {data}")
262
+
263
+ # Subscribe to different data paths
264
+ data_manager.subscribe("/Device/DeviceInfo/*", on_device_update)
265
+ data_manager.subscribe("/Device/Network/*", on_network_change)
266
+
267
+ # Start monitoring
268
+ await data_manager.start_monitoring()
269
+
270
+ # Request data to trigger callbacks
271
+ await client.ws_get("/Device/DeviceInfo/Model")
272
+ await client.ws_get("/Device/Network/Interface")
273
+
274
+ # Monitor for 30 seconds
275
+ await asyncio.sleep(30)
276
+
277
+ # Clean up
278
+ await data_manager.stop_monitoring()
279
+ await client.disconnect()
280
+
281
+ asyncio.run(main())
282
+ ```
283
+
284
+ #### Path Pattern Matching
285
+
286
+ The DataEventManager supports flexible path matching:
287
+
288
+ - **Exact match**: `/Device/Config` - matches only that specific path
289
+ - **Wildcard match**: `/Device/*` - matches any direct child of `/Device/`
290
+ - **Child matching**: `/Device/Config` with `match_children=True` - matches the path and all sub-paths
291
+
292
+ ```python
293
+ # Examples of path patterns
294
+ data_manager.subscribe("/Device/Config", callback) # Exact match
295
+ data_manager.subscribe("/Device/*", callback) # Wildcard
296
+ data_manager.subscribe("/Device/Config", callback, match_children=True) # Include children
297
+ ```
298
+
299
+ #### Full Message Access
300
+
301
+ By default, callbacks receive only the changed value. Use `full_message=True` to access the complete WebSocket message including metadata:
302
+
303
+ ```python
304
+ def value_only_callback(path: str, data):
305
+ print(f"Value: {data}") # Only the changed data
306
+
307
+ def full_message_callback(path: str, message):
308
+ print(f"Full message: {message}") # Complete JSON with timestamps, etc.
309
+
310
+ # Traditional behavior (default)
311
+ data_manager.subscribe("/Device/Config", value_only_callback, full_message=False)
312
+
313
+ # New: Access full message including metadata
314
+ data_manager.subscribe("/Device/Config", full_message_callback, full_message=True)
315
+ ```
316
+
317
+ #### Context Manager Usage
318
+
319
+ ```python
320
+ async def monitor_with_context():
321
+ config = ClientConfig(host="your-host.local", username="admin", password="password")
322
+
323
+ async with CresNextWSClient(config) as client:
324
+ async with DataEventManager(client) as data_manager:
325
+ # Add subscriptions
326
+ data_manager.subscribe("/Device/*", lambda path, data: print(f"{path}: {data}"))
327
+
328
+ # Request data
329
+ await client.ws_get("/Device/Info")
330
+
331
+ # Monitor for a while
332
+ await asyncio.sleep(10)
333
+
334
+ # Automatic cleanup when exiting context
335
+
336
+ asyncio.run(monitor_with_context())
337
+ ```
338
+
339
+ ## API Reference
340
+
341
+ ### CresNextWSClient Methods
342
+
343
+ #### Connection Management
344
+ - `await client.connect()` - Connect to the CresNext system
345
+ - `await client.disconnect()` - Disconnect from the system
346
+ - `client.connected` - Check connection status
347
+
348
+ #### HTTP Operations
349
+ - `await client.http_get(path)` - Send HTTP GET request
350
+ - `await client.http_post(path, data)` - Send HTTP POST request with JSON data
351
+
352
+ #### WebSocket Operations
353
+ - `await client.ws_get(path)` - Subscribe to WebSocket data updates for a path
354
+ - `await client.ws_post(data)` - Send data via WebSocket
355
+ - `await client.next_message(timeout=None)` - Get next WebSocket message
356
+
357
+ ### DataEventManager Methods
358
+
359
+ #### Subscription Management
360
+ - `subscribe(path_pattern, callback, match_children=True, full_message=False)` - Add subscription
361
+ - `full_message=True` - Pass complete JSON message to callback (includes metadata)
362
+ - `full_message=False` - Pass only the changed value to callback (default behavior)
363
+ - `unsubscribe(subscription_id)` - Remove subscription
364
+ - `clear_subscriptions()` - Remove all subscriptions
365
+ - `get_subscriptions()` - List current subscriptions
366
+
367
+ #### Monitoring Control
368
+ - `await start_monitoring()` - Begin monitoring WebSocket messages
369
+ - `await stop_monitoring()` - Stop monitoring
370
+
371
+ ### Common API Paths
372
+
373
+ Based on integration testing, common device paths include:
374
+
375
+ ```python
376
+ # Device Information
377
+ "/Device/DeviceInfo/Model"
378
+ "/Device/DeviceInfo/SerialNumber"
379
+ "/Device/DeviceInfo/FirmwareVersion"
380
+
381
+ # Network Configuration
382
+ "/Device/Ethernet/HostName"
383
+ "/Device/Ethernet/IPAddress"
384
+ "/Device/Ethernet/MACAddress"
385
+
386
+ # Device Configuration
387
+ "/Device/Config/*"
388
+ "/Device/Network/*"
389
+ "/Device/State/*"
390
+ ```
391
+
392
+ ## Examples
393
+
394
+ For comprehensive examples, see `examples.py` in the repository:
395
+
396
+ ```bash
397
+ python3 examples.py
398
+ ```
399
+
400
+ The examples demonstrate:
401
+ - Basic HTTP and WebSocket operations
402
+ - DataEventManager usage with subscriptions
403
+ - Context manager patterns
404
+ - Error handling and cleanup
405
+ - Batch operations
406
+ - Real-time device monitoring
407
+
408
+ ## Development
409
+
410
+ ### Setup Development Environment
411
+
412
+ ```bash
413
+ git clone https://github.com/jetsoncontrols/cresnextws.git
414
+ cd cresnextws
415
+ pip install -e .[dev]
416
+ ```
417
+
418
+ ### Running Tests
419
+
420
+ ```bash
421
+ pytest
422
+
423
+ To run integration tests:
424
+ pytest -m integration --run-integration --systems <systems entries from services.json>
425
+ ```
426
+
427
+ ### Service-driven Integration Tests
428
+
429
+ You can provide real system connection details to pytest without hard-coding them in tests.
430
+
431
+ 1) Create a services file:
432
+ - Copy `tests/services.example.json` to `tests/services.json`
433
+ - Edit values or set environment variables referenced by `${VARS}`
434
+
435
+ 2) Run integration tests by opting in:
436
+
437
+ ```bash
438
+ pytest --run-integration --systems all
439
+ ```
440
+
441
+ Alternatively, since integration tests are marked with `@pytest.mark.integration` and excluded by default via project config, you can select them explicitly:
442
+
443
+ ```bash
444
+ pytest -m integration --run-integration [other flags]
445
+ ```
446
+
447
+ Flags and environment variables:
448
+ - `--services-file PATH` or `CRESNEXTWS_SERVICES_FILE=PATH` to point to a JSON file
449
+ - `--systems name1,name2` or `CRESNEXTWS_SYSTEMS=name1,name2` to select systems
450
+ - Use `--systems all` to include all systems with `"enabled": true`
451
+
452
+ Example JSON structure:
453
+
454
+ ```json
455
+ {
456
+ "systems": {
457
+ "local_sim": {
458
+ "enabled": true,
459
+ "host": "test.local",
460
+ "auth": {"username": "${CRESNEXTWS_USER}", "password": "${CRESNEXTWS_PASS}"}
461
+ }
462
+ }
463
+ }
464
+ ```
465
+
466
+ Notes:
467
+ - Integration tests are skipped unless `--run-integration` is supplied.
468
+ - Missing systems or disabled entries are automatically skipped.
469
+
470
+ ### Code Formatting
471
+
472
+ ```bash
473
+ black cresnextws/
474
+ ```
475
+
476
+ ### Type Checking
477
+
478
+ ```bash
479
+ mypy cresnextws/
480
+ ```
481
+
482
+ ## Features
483
+
484
+ - **Async/await support** for non-blocking operations
485
+ - **HTTP and WebSocket APIs** for comprehensive device interaction
486
+ - **DataEventManager** for real-time monitoring with path-based subscriptions
487
+ - **Full message access** option for receiving complete WebSocket messages with metadata
488
+ - **Connection Status Events** for monitoring connect/disconnect states
489
+ - **Context manager support** for automatic connection management
490
+ - **Type hints** for better development experience
491
+ - **Comprehensive logging** support
492
+ - **Automatic reconnection** capabilities with connection health monitoring
493
+ - **Health check mechanism** to detect stale connections after system sleep/wake cycles
494
+ - **Flexible path pattern matching** with wildcard and child path support
495
+ - Easy-to-use API for Crestron CresNext systems
496
+
497
+ ## Requirements
498
+
499
+ - Python 3.8 or higher
500
+ - websockets>=11.0
501
+ - aiohttp>=3.8.0
502
+
503
+ ## Contributing
504
+
505
+ Contributions are welcome! Please feel free to submit a Pull Request.
506
+
507
+ ## Publishing
508
+
509
+ This project is automatically published to PyPI using GitHub Actions. The publishing workflow is triggered by:
510
+
511
+ ### For Development Releases (Test PyPI)
512
+ - **Pushes to main branch**: Automatically publishes to Test PyPI for testing
513
+ - **Manual workflow dispatch**: Can be triggered manually with option to publish to Test PyPI
514
+
515
+ ### For Production Releases (PyPI)
516
+ - **Version tags**: Create and push a version tag (e.g., `v1.0.0`, `v0.2.1`) to trigger a production release to PyPI
517
+
518
+ ### Setting up PyPI Credentials
519
+
520
+ To enable automatic publishing, you need to configure the following secrets in your GitHub repository:
521
+
522
+ 1. **For Test PyPI publishing** (pushes to main branch):
523
+ - Go to [Test PyPI](https://test.pypi.org/manage/account/), create an API token
524
+ - Add the token as `TEST_PYPI_API_TOKEN` in GitHub repository secrets
525
+
526
+ 2. **For PyPI publishing** (version tags):
527
+ - Go to [PyPI](https://pypi.org/manage/account/), create an API token
528
+ - Add the token as `PYPI_API_TOKEN` in GitHub repository secrets
529
+
530
+ ### Creating a Release
531
+
532
+ To create a new release:
533
+
534
+ 1. Update the version in `pyproject.toml`
535
+ 2. Commit your changes
536
+ 3. Create and push a tag:
537
+ ```bash
538
+ git tag v1.0.0
539
+ git push origin v1.0.0
540
+ ```
541
+
542
+ The GitHub Action will automatically:
543
+ - Run tests across multiple Python versions
544
+ - Build the package
545
+ - Publish to PyPI
546
+ - Create a GitHub release with release notes
547
+
548
+ ### Manual Testing
549
+
550
+ To test the package build locally before releasing:
551
+
552
+ ```bash
553
+ # Install build tools
554
+ pip install build twine
555
+
556
+ # Build the package
557
+ python -m build --no-isolation
558
+
559
+ # Check the package
560
+ python -m twine check dist/*
561
+
562
+ # Test upload to Test PyPI (optional)
563
+ python -m twine upload --repository testpypi dist/*
564
+ ```