span-panel-api 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.
- span_panel_api-0.1.0/LICENSE +21 -0
- span_panel_api-0.1.0/PKG-INFO +457 -0
- span_panel_api-0.1.0/README.md +436 -0
- span_panel_api-0.1.0/pyproject.toml +132 -0
- span_panel_api-0.1.0/src/span_panel_api/__init__.py +30 -0
- span_panel_api-0.1.0/src/span_panel_api/client.py +773 -0
- span_panel_api-0.1.0/src/span_panel_api/const.py +24 -0
- span_panel_api-0.1.0/src/span_panel_api/exceptions.py +45 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/__init__.py +7 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/__init__.py +1 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/__init__.py +1 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/delete_client_api_v1_auth_clients_name_delete.py +154 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/deprecated_spaces_endpoint_stub_api_v1_spaces_get.py +161 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/deprecated_spaces_endpoint_stub_api_v1_spaces_spaces_id_get.py +153 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/deprecated_spaces_endpoint_stub_api_v1_spaces_spaces_id_post.py +153 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/generate_jwt_api_v1_auth_register_post.py +165 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_all_clients_api_v1_auth_clients_get.py +122 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_circuit_state_api_v_1_circuits_circuit_id_get.py +155 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_circuits_api_v1_circuits_get.py +122 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_client_api_v1_auth_clients_name_get.py +154 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_islanding_state_api_v1_islanding_state_get.py +126 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_main_relay_state_api_v1_panel_grid_get.py +122 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_panel_meter_api_v1_panel_meter_get.py +122 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_panel_power_api_v1_panel_power_get.py +122 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_panel_state_api_v1_panel_get.py +122 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_storage_nice_to_have_threshold_api_v1_storage_nice_to_have_thresh_get.py +126 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_storage_soe_api_v1_storage_soe_get.py +126 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/get_wifi_scan_api_v1_wifi_scan_get.py +122 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/run_panel_emergency_reconnect_api_v1_panel_emergency_reconnect_post.py +79 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/run_wifi_connect_api_v1_wifi_connect_post.py +165 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/set_circuit_state_api_v_1_circuits_circuit_id_post.py +180 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/set_main_relay_state_api_v1_panel_grid_post.py +163 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/set_storage_nice_to_have_threshold_api_v1_storage_nice_to_have_thresh_post.py +164 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/set_storage_soe_api_v1_storage_soe_post.py +164 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/api/default/system_status_api_v1_status_get.py +122 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/client.py +268 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/errors.py +16 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/__init__.py +81 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/allowed_endpoint_groups.py +83 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/auth_in.py +88 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/auth_out.py +75 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/battery_storage.py +65 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/body_set_circuit_state_api_v1_circuits_circuit_id_post.py +158 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/boolean_in.py +59 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/branch.py +117 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/circuit.py +163 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/circuit_name_in.py +59 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/circuits_out.py +65 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/circuits_out_circuits.py +57 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/client.py +85 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/clients.py +65 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/clients_clients.py +57 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/door_state.py +10 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/feedthrough_energy.py +67 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/http_validation_error.py +75 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/islanding_state.py +59 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/main_meter_energy.py +67 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/network_status.py +75 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/nice_to_have_threshold.py +88 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/panel_meter.py +75 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/panel_power.py +67 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/panel_state.py +159 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/priority.py +11 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/priority_in.py +61 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/relay_state.py +10 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/relay_state_in.py +61 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/relay_state_out.py +59 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/software_status.py +75 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/state_of_energy.py +59 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/status_out.py +85 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/system_status.py +101 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/validation_error.py +75 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/wifi_access_point.py +110 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/wifi_connect_in.py +67 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/wifi_connect_out.py +99 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/models/wifi_scan_out.py +73 -0
- span_panel_api-0.1.0/src/span_panel_api/generated_client/types.py +46 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 SpanPanel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: span-panel-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A client library for SPAN Panel API
|
|
5
|
+
Author: SpanPanel
|
|
6
|
+
Requires-Python: >=3.9,<4.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: attrs (>=22.2.0)
|
|
14
|
+
Requires-Dist: click (>=8.0.0,<9.0.0)
|
|
15
|
+
Requires-Dist: httpx (>=0.20.0,<0.29.0)
|
|
16
|
+
Requires-Dist: python-dateutil (>=2.8.0,<3.0.0)
|
|
17
|
+
Project-URL: Homepage, https://github.com/SpanPanel/span-panel-api
|
|
18
|
+
Project-URL: Issues, https://github.com/SpanPanel/span-panel-api/issues
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# SPAN Panel OpenAPI Client
|
|
22
|
+
|
|
23
|
+
A Python client library for accessing the SPAN Panel API.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install span-panel-api
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage Patterns
|
|
33
|
+
|
|
34
|
+
The client supports two usage patterns depending on your use case:
|
|
35
|
+
|
|
36
|
+
### Context Manager Pattern (Recommended for Scripts)
|
|
37
|
+
|
|
38
|
+
**Best for**: Scripts, one-off operations, short-lived applications
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
from span_panel_api import SpanPanelClient
|
|
43
|
+
|
|
44
|
+
async def main():
|
|
45
|
+
# Context manager automatically handles connection lifecycle
|
|
46
|
+
async with SpanPanelClient("192.168.1.100") as client:
|
|
47
|
+
# Authenticate
|
|
48
|
+
auth = await client.authenticate("my-script", "SPAN Control Script")
|
|
49
|
+
|
|
50
|
+
# Get panel status (no auth required)
|
|
51
|
+
status = await client.get_status()
|
|
52
|
+
print(f"Panel: {status.system.manufacturer}")
|
|
53
|
+
|
|
54
|
+
# Get circuits (requires auth)
|
|
55
|
+
circuits = await client.get_circuits()
|
|
56
|
+
for circuit_id, circuit in circuits.circuits.additional_properties.items():
|
|
57
|
+
print(f"{circuit.name}: {circuit.instant_power_w}W")
|
|
58
|
+
|
|
59
|
+
# Control a circuit
|
|
60
|
+
await client.set_circuit_relay("circuit-1", "OPEN")
|
|
61
|
+
await client.set_circuit_priority("circuit-1", "MUST_HAVE")
|
|
62
|
+
|
|
63
|
+
# Client is automatically closed when exiting context
|
|
64
|
+
|
|
65
|
+
asyncio.run(main())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Long-Lived Pattern (Services and Integrations)
|
|
69
|
+
|
|
70
|
+
**Best for**: Long-running services, persistent connections, integration platforms
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
from span_panel_api import SpanPanelClient
|
|
75
|
+
|
|
76
|
+
class SpanPanelIntegration:
|
|
77
|
+
"""Example long-running service integration pattern."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, host: str):
|
|
80
|
+
# Create client but don't use context manager
|
|
81
|
+
self.client = SpanPanelClient(host)
|
|
82
|
+
self._authenticated = False
|
|
83
|
+
|
|
84
|
+
async def setup(self) -> None:
|
|
85
|
+
"""Initialize the integration (called once)."""
|
|
86
|
+
try:
|
|
87
|
+
# Authenticate once during setup
|
|
88
|
+
await self.client.authenticate("my-service", "Panel Integration Service")
|
|
89
|
+
self._authenticated = True
|
|
90
|
+
except Exception as e:
|
|
91
|
+
await self.client.close() # Clean up on setup failure
|
|
92
|
+
raise
|
|
93
|
+
|
|
94
|
+
async def update_data(self) -> dict:
|
|
95
|
+
"""Update all data (called periodically by coordinator)."""
|
|
96
|
+
if not self._authenticated:
|
|
97
|
+
await self.client.authenticate("my-service", "Panel Integration Service")
|
|
98
|
+
self._authenticated = True
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
# Get all data in one update cycle
|
|
102
|
+
status = await self.client.get_status()
|
|
103
|
+
panel_state = await self.client.get_panel_state()
|
|
104
|
+
circuits = await self.client.get_circuits()
|
|
105
|
+
storage = await self.client.get_storage_soe()
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"status": status,
|
|
109
|
+
"panel": panel_state,
|
|
110
|
+
"circuits": circuits,
|
|
111
|
+
"storage": storage
|
|
112
|
+
}
|
|
113
|
+
except Exception:
|
|
114
|
+
self._authenticated = False # Reset auth on error
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
async def set_circuit_priority(self, circuit_id: str, priority: str) -> None:
|
|
118
|
+
"""Set circuit priority (called by service)."""
|
|
119
|
+
if not self._authenticated:
|
|
120
|
+
await self.client.authenticate("my-service", "Panel Integration Service")
|
|
121
|
+
self._authenticated = True
|
|
122
|
+
|
|
123
|
+
await self.client.set_circuit_priority(circuit_id, priority)
|
|
124
|
+
|
|
125
|
+
async def cleanup(self) -> None:
|
|
126
|
+
"""Cleanup when integration is unloaded."""
|
|
127
|
+
await self.client.close()
|
|
128
|
+
|
|
129
|
+
# Usage in long-running service
|
|
130
|
+
async def main():
|
|
131
|
+
integration = SpanPanelIntegration("192.168.1.100")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
await integration.setup()
|
|
135
|
+
|
|
136
|
+
# Simulate coordinator updates
|
|
137
|
+
for i in range(10):
|
|
138
|
+
data = await integration.update_data()
|
|
139
|
+
print(f"Update {i}: {len(data['circuits'].circuits.additional_properties)} circuits")
|
|
140
|
+
await asyncio.sleep(30) # Service typically updates every 30 seconds
|
|
141
|
+
|
|
142
|
+
finally:
|
|
143
|
+
await integration.cleanup()
|
|
144
|
+
|
|
145
|
+
asyncio.run(main())
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Manual Pattern (Advanced Usage)
|
|
149
|
+
|
|
150
|
+
**Best for**: Custom connection management, special requirements
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
import asyncio
|
|
154
|
+
from span_panel_api import SpanPanelClient
|
|
155
|
+
|
|
156
|
+
async def manual_example():
|
|
157
|
+
"""Manual client lifecycle management."""
|
|
158
|
+
client = SpanPanelClient("192.168.1.100")
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
# Manually authenticate
|
|
162
|
+
await client.authenticate("manual-app", "Manual Application")
|
|
163
|
+
|
|
164
|
+
# Do work
|
|
165
|
+
status = await client.get_status()
|
|
166
|
+
circuits = await client.get_circuits()
|
|
167
|
+
|
|
168
|
+
print(f"Found {len(circuits.circuits.additional_properties)} circuits")
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f"Error: {e}")
|
|
172
|
+
finally:
|
|
173
|
+
# IMPORTANT: Always close the client to free resources
|
|
174
|
+
await client.close()
|
|
175
|
+
|
|
176
|
+
asyncio.run(manual_example())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## When to Use Each Pattern
|
|
180
|
+
|
|
181
|
+
| Pattern | Use Case | Pros | Cons |
|
|
182
|
+
|---------|----------|------|------|
|
|
183
|
+
| **Context Manager** | Scripts, one-off tasks, testing | Automatic cleanup • Exception safe • Simple code | Creates/destroys connection each time • Not efficient for frequent calls |
|
|
184
|
+
| **Long-Lived** | Services, daemons, integration platforms | Efficient connection reuse • Better performance • Authentication persistence | Manual lifecycle management • Must handle cleanup |
|
|
185
|
+
| **Manual** | Custom requirements, debugging | Full control • Custom error handling | Must remember to call close() • More error-prone |
|
|
186
|
+
|
|
187
|
+
## Error Handling
|
|
188
|
+
|
|
189
|
+
The client provides error categorization for different retry strategies:
|
|
190
|
+
|
|
191
|
+
### Exception Types
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from span_panel_api.exceptions import (
|
|
195
|
+
SpanPanelError, # Base exception
|
|
196
|
+
SpanPanelAPIError, # General API errors
|
|
197
|
+
SpanPanelAuthError, # 401/403 - need re-authentication
|
|
198
|
+
SpanPanelConnectionError, # Network connectivity issues
|
|
199
|
+
SpanPanelTimeoutError, # Request timeouts
|
|
200
|
+
SpanPanelRetriableError, # 502/503/504 - temporary issues, SHOULD retry
|
|
201
|
+
SpanPanelServerError, # 500 - application bugs, DO NOT retry
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### HTTP Error Code Mapping
|
|
206
|
+
|
|
207
|
+
| Status Code | Exception | Retry? | Description | Action |
|
|
208
|
+
|-------------|-----------|--------|-------------|--------|
|
|
209
|
+
| **Authentication Errors** |
|
|
210
|
+
| 401, 403 | `SpanPanelAuthError` | Once (after re-auth) | Authentication required/failed | Re-authenticate and retry once |
|
|
211
|
+
| **Non-Retriable Server Errors** |
|
|
212
|
+
| 500 | `SpanPanelServerError` | **NO** | Internal server error (SPAN bug) | Show error, do not retry |
|
|
213
|
+
| **Retriable Server Errors** |
|
|
214
|
+
| 502 | `SpanPanelRetriableError` | Yes | Bad Gateway (proxy error) | Retry with exponential backoff |
|
|
215
|
+
| 503 | `SpanPanelRetriableError` | Yes | Service Unavailable | Retry with exponential backoff |
|
|
216
|
+
| 504 | `SpanPanelRetriableError` | Yes | Gateway Timeout | Retry with exponential backoff |
|
|
217
|
+
| **Other HTTP Errors** |
|
|
218
|
+
| 404, 400, etc | `SpanPanelAPIError` | Case by case | Client/request errors | Check request parameters |
|
|
219
|
+
| **Network Errors** |
|
|
220
|
+
| Connection failures | `SpanPanelConnectionError` | Yes | Network connectivity issues | Retry with backoff |
|
|
221
|
+
| Timeouts | `SpanPanelTimeoutError` | Yes | Request timed out | Retry with backoff |
|
|
222
|
+
|
|
223
|
+
### Retry Strategy
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
async def example_request_with_retry():
|
|
227
|
+
"""Example showing appropriate error handling."""
|
|
228
|
+
try:
|
|
229
|
+
return await client.get_circuits()
|
|
230
|
+
except SpanPanelAuthError:
|
|
231
|
+
# Re-authenticate and retry once
|
|
232
|
+
await client.authenticate("my-app", "My Application")
|
|
233
|
+
return await client.get_circuits()
|
|
234
|
+
except SpanPanelRetriableError as e:
|
|
235
|
+
# Temporary server issues - should retry with backoff
|
|
236
|
+
# 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
|
|
237
|
+
logger.warning(f"Retriable error {e.status_code}, will retry: {e}")
|
|
238
|
+
raise # Let retry logic handle the retry
|
|
239
|
+
except SpanPanelServerError as e:
|
|
240
|
+
# Application bugs on SPAN side - DO NOT retry
|
|
241
|
+
# 500 Internal Server Error (SPAN Panel bug, not your fault!)
|
|
242
|
+
logger.error(f"Server error {e.status_code}, not retrying: {e}")
|
|
243
|
+
raise # Show notification but don't waste resources retrying
|
|
244
|
+
except (SpanPanelConnectionError, SpanPanelTimeoutError):
|
|
245
|
+
# Network issues - should retry
|
|
246
|
+
raise
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Exception Handling
|
|
250
|
+
|
|
251
|
+
The client configures the underlying OpenAPI client with `raise_on_unexpected_status=True`, ensuring that HTTP errors (especially 500 responses) are converted to appropriate exceptions rather than being silently ignored.
|
|
252
|
+
|
|
253
|
+
## API Reference
|
|
254
|
+
|
|
255
|
+
### Client Initialization
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
client = SpanPanelClient(
|
|
259
|
+
host="192.168.1.100", # Required: SPAN Panel IP
|
|
260
|
+
port=80, # Optional: default 80
|
|
261
|
+
timeout=30.0, # Optional: request timeout
|
|
262
|
+
use_ssl=False # Optional: HTTPS (usually False for local)
|
|
263
|
+
)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Authentication
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
# Register a new API client (one-time setup)
|
|
270
|
+
auth = await client.authenticate(
|
|
271
|
+
name="my-integration", # Required: client name
|
|
272
|
+
description="My Application" # Optional: description
|
|
273
|
+
)
|
|
274
|
+
# Token is stored and used for subsequent requests
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Panel Information
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
# System status (no authentication required)
|
|
281
|
+
status = await client.get_status()
|
|
282
|
+
print(f"System: {status.system}")
|
|
283
|
+
print(f"Network: {status.network}")
|
|
284
|
+
|
|
285
|
+
# Detailed panel state (requires authentication)
|
|
286
|
+
panel = await client.get_panel_state()
|
|
287
|
+
print(f"Grid power: {panel.instant_grid_power_w}W")
|
|
288
|
+
print(f"Main relay: {panel.main_relay_state}")
|
|
289
|
+
|
|
290
|
+
# Battery storage information
|
|
291
|
+
storage = await client.get_storage_soe()
|
|
292
|
+
print(f"Battery SOE: {storage.soe * 100:.1f}%")
|
|
293
|
+
print(f"Max capacity: {storage.max_energy_kwh}kWh")
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Circuit Control
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
# Get all circuits
|
|
300
|
+
circuits = await client.get_circuits()
|
|
301
|
+
for circuit_id, circuit in circuits.circuits.additional_properties.items():
|
|
302
|
+
print(f"Circuit {circuit_id}: {circuit.name}")
|
|
303
|
+
print(f" Power: {circuit.instant_power_w}W")
|
|
304
|
+
print(f" Relay: {circuit.relay_state}")
|
|
305
|
+
print(f" Priority: {circuit.priority}")
|
|
306
|
+
|
|
307
|
+
# Control circuit relay (OPEN/CLOSED)
|
|
308
|
+
await client.set_circuit_relay("circuit-1", "OPEN") # Turn off
|
|
309
|
+
await client.set_circuit_relay("circuit-1", "CLOSED") # Turn on
|
|
310
|
+
|
|
311
|
+
# Set circuit priority
|
|
312
|
+
await client.set_circuit_priority("circuit-1", "MUST_HAVE")
|
|
313
|
+
await client.set_circuit_priority("circuit-1", "NICE_TO_HAVE")
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Timeout and Retry Control
|
|
317
|
+
|
|
318
|
+
The SPAN Panel API client provides timeout and retry configuration:
|
|
319
|
+
|
|
320
|
+
- `timeout` (float, default: 30.0): The maximum time (in seconds) to wait for a response from the panel for each attempt.
|
|
321
|
+
- `retries` (int, default: 0): The number of times to retry a failed request due to network or retriable server errors. `retries=0` means no retries (1 total attempt), `retries=1` means 1 retry (2 total attempts), etc.
|
|
322
|
+
- `retry_timeout` (float, default: 0.5): The base wait time (in seconds) between retries, with exponential backoff.
|
|
323
|
+
- `retry_backoff_multiplier` (float, default: 2.0): The multiplier for exponential backoff between retries.
|
|
324
|
+
|
|
325
|
+
### Example Usage
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
# No retries (default, fast feedback)
|
|
329
|
+
client = SpanPanelClient("192.168.1.100", timeout=10.0)
|
|
330
|
+
|
|
331
|
+
# Add retries for production
|
|
332
|
+
client = SpanPanelClient("192.168.1.100", timeout=10.0, retries=2, retry_timeout=1.0)
|
|
333
|
+
|
|
334
|
+
# Full retry configuration
|
|
335
|
+
client = SpanPanelClient(
|
|
336
|
+
"192.168.1.100",
|
|
337
|
+
timeout=10.0,
|
|
338
|
+
retries=3,
|
|
339
|
+
retry_timeout=0.5,
|
|
340
|
+
retry_backoff_multiplier=2.0
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Change retry settings at runtime
|
|
344
|
+
client.retries = 3
|
|
345
|
+
client.retry_timeout = 2.0
|
|
346
|
+
client.retry_backoff_multiplier = 1.5
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### What does 'retries' mean?
|
|
350
|
+
|
|
351
|
+
| retries | Total Attempts | Description |
|
|
352
|
+
|---------|---------------|---------------------|
|
|
353
|
+
| 0 | 1 | No retries (default) |
|
|
354
|
+
| 1 | 2 | 1 retry |
|
|
355
|
+
| 2 | 3 | 2 retries |
|
|
356
|
+
|
|
357
|
+
Retry and timeout settings can be queried and changed at runtime.
|
|
358
|
+
|
|
359
|
+
## Development Setup
|
|
360
|
+
|
|
361
|
+
### Prerequisites
|
|
362
|
+
- Python 3.12+ (SPAN Panel requires Python 3.12+)
|
|
363
|
+
- [Poetry](https://python-poetry.org/) for dependency management
|
|
364
|
+
|
|
365
|
+
### Installation
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
# Clone and install
|
|
369
|
+
git clone <repository code URL>
|
|
370
|
+
cd span-panel-api
|
|
371
|
+
poetry install
|
|
372
|
+
poetry env activate
|
|
373
|
+
|
|
374
|
+
# Run tests
|
|
375
|
+
poetry run pytest
|
|
376
|
+
|
|
377
|
+
# Check coverage
|
|
378
|
+
python scripts/coverage.py
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Project Structure
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
span_openapi/
|
|
385
|
+
├── src/span_panel_api/ # Main client library
|
|
386
|
+
│ ├── client.py # SpanPanelClient (high-level wrapper)
|
|
387
|
+
│ ├── exceptions.py # Exception hierarchy
|
|
388
|
+
│ ├── const.py # HTTP status constants
|
|
389
|
+
│ └── generated_client/ # Auto-generated OpenAPI client
|
|
390
|
+
├── tests/ # test suite
|
|
391
|
+
├── scripts/coverage.py # Coverage checking utility
|
|
392
|
+
├── openapi.json # SPAN Panel OpenAPI specification
|
|
393
|
+
└── pyproject.toml # Poetry configuration
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
## Advanced Usage
|
|
399
|
+
|
|
400
|
+
### SSL Configuration
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
# For panels that support SSL locally
|
|
404
|
+
# Note: We do not currently observe panels supporting SSL for local access
|
|
405
|
+
client = SpanPanelClient(
|
|
406
|
+
host="span-panel.local",
|
|
407
|
+
use_ssl=True,
|
|
408
|
+
port=443
|
|
409
|
+
)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Timeout Configuration
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
# Custom timeout for slow networks
|
|
416
|
+
client = SpanPanelClient(
|
|
417
|
+
host="192.168.1.100",
|
|
418
|
+
timeout=60.0 # 60 second timeout
|
|
419
|
+
)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Testing and Coverage
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
# Run full test suite
|
|
426
|
+
poetry run pytest
|
|
427
|
+
|
|
428
|
+
# Generate coverage report
|
|
429
|
+
python scripts/coverage.py --full
|
|
430
|
+
|
|
431
|
+
# Run just context manager tests
|
|
432
|
+
poetry run pytest tests/test_context_manager.py -v
|
|
433
|
+
|
|
434
|
+
# Check coverage meets threshold
|
|
435
|
+
python scripts/coverage.py --check --threshold 95
|
|
436
|
+
|
|
437
|
+
# Run with coverage
|
|
438
|
+
poetry run pytest --cov=span_panel_api tests/
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Contributing
|
|
442
|
+
|
|
443
|
+
1. Get `openapi.json` SPAN Panel API specs
|
|
444
|
+
|
|
445
|
+
(for example via REST Client extension)
|
|
446
|
+
|
|
447
|
+
GET <https://span-panel-ip/api/v1/openapi.json>
|
|
448
|
+
|
|
449
|
+
2. Regenerate client: `poetry run python generate_client.py`
|
|
450
|
+
3. Update wrapper client in `src/span_panel_api/client.py` if needed
|
|
451
|
+
4. Add tests for new functionality
|
|
452
|
+
5. Update this README if adding new features
|
|
453
|
+
|
|
454
|
+
## License
|
|
455
|
+
|
|
456
|
+
MIT License - see LICENSE file for details.
|
|
457
|
+
|