pycloudedge 0.1.4.dev2__tar.gz → 0.1.4.dev4__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.
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/.gitignore +3 -1
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/PKG-INFO +1 -1
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/__init__.py +9 -1
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/_version.py +3 -3
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/client.py +355 -35
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/pycloudedge.egg-info/PKG-INFO +1 -1
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/.env.example +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/LICENSE +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/MANIFEST.in +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/README.md +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/cli.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/constants.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/exceptions.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/iot_parameters.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/logging_config.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/utils.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/cloudedge/validators.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/examples/README.md +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/examples/basic_example.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/examples/device_control.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/examples/network_ping_status.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/pycloudedge.egg-info/SOURCES.txt +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/pycloudedge.egg-info/dependency_links.txt +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/pycloudedge.egg-info/entry_points.txt +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/pycloudedge.egg-info/requires.txt +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/pycloudedge.egg-info/top_level.txt +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/pyproject.toml +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/requirements-dev.txt +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/requirements.txt +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/setup.cfg +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/setup.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/tests/test_basic.py +0 -0
- {pycloudedge-0.1.4.dev2 → pycloudedge-0.1.4.dev4}/tests/test_improvements.py +0 -0
|
@@ -9,7 +9,12 @@ Author: Francesco D'Aloisio
|
|
|
9
9
|
Date: September 16, 2025
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
-
from .client import
|
|
12
|
+
from .client import (
|
|
13
|
+
CloudEdgeClient,
|
|
14
|
+
DEVICE_STATUS_ONLINE,
|
|
15
|
+
DEVICE_STATUS_DORMANCY,
|
|
16
|
+
DEVICE_STATUS_OFFLINE,
|
|
17
|
+
)
|
|
13
18
|
from .exceptions import (
|
|
14
19
|
CloudEdgeError, AuthenticationError, DeviceNotFoundError,
|
|
15
20
|
ConfigurationError, NetworkError, ValidationError, RateLimitError
|
|
@@ -24,6 +29,9 @@ __author__ = "Francesco D'Aloisio"
|
|
|
24
29
|
|
|
25
30
|
__all__ = [
|
|
26
31
|
'CloudEdgeClient',
|
|
32
|
+
'DEVICE_STATUS_ONLINE',
|
|
33
|
+
'DEVICE_STATUS_DORMANCY',
|
|
34
|
+
'DEVICE_STATUS_OFFLINE',
|
|
27
35
|
'CloudEdgeError',
|
|
28
36
|
'AuthenticationError',
|
|
29
37
|
'DeviceNotFoundError',
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.4.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1, 4, '
|
|
31
|
+
__version__ = version = '0.1.4.dev4'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 4, 'dev4')
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gc5b1ac2e4'
|
|
@@ -44,6 +44,11 @@ from .validators import validate_email, validate_country_code, validate_phone_co
|
|
|
44
44
|
from .logging_config import get_logger
|
|
45
45
|
from .utils import retry_on_failure
|
|
46
46
|
|
|
47
|
+
# Device online status values returned by get_device_online_status()
|
|
48
|
+
DEVICE_STATUS_ONLINE = "online"
|
|
49
|
+
DEVICE_STATUS_DORMANCY = "dormancy"
|
|
50
|
+
DEVICE_STATUS_OFFLINE = "offline"
|
|
51
|
+
|
|
47
52
|
# API list keys → readable product type when deviceTypeName is a CDN image URL
|
|
48
53
|
_DEVICE_LIST_CATEGORY_LABELS = {
|
|
49
54
|
"snap": "Camera",
|
|
@@ -1091,7 +1096,279 @@ class CloudEdgeClient:
|
|
|
1091
1096
|
raise NetworkError(f"Device status request failed: {e}")
|
|
1092
1097
|
except json.JSONDecodeError:
|
|
1093
1098
|
raise CloudEdgeError("Failed to parse device status response")
|
|
1094
|
-
|
|
1099
|
+
|
|
1100
|
+
def get_device_online_status(self, serial_number: str) -> str:
|
|
1101
|
+
"""Query the precise online state of a device via OpenAPI.
|
|
1102
|
+
|
|
1103
|
+
Unlike :meth:`get_device_status` (which uses a legacy endpoint and
|
|
1104
|
+
only distinguishes online/offline), this method calls
|
|
1105
|
+
``/openapi/device/status`` and can distinguish three states:
|
|
1106
|
+
|
|
1107
|
+
* ``"online"`` – camera is awake and reachable
|
|
1108
|
+
* ``"dormancy"`` – battery camera is asleep (must be woken before commands)
|
|
1109
|
+
* ``"offline"`` – camera is unreachable
|
|
1110
|
+
|
|
1111
|
+
Use the module-level constants :data:`DEVICE_STATUS_ONLINE`,
|
|
1112
|
+
:data:`DEVICE_STATUS_DORMANCY`, :data:`DEVICE_STATUS_OFFLINE` for
|
|
1113
|
+
comparisons.
|
|
1114
|
+
|
|
1115
|
+
Args:
|
|
1116
|
+
serial_number (str): Device serial number (snNum).
|
|
1117
|
+
|
|
1118
|
+
Returns:
|
|
1119
|
+
str: ``"online"``, ``"dormancy"``, ``"offline"``, or ``"unknown"``.
|
|
1120
|
+
|
|
1121
|
+
Raises:
|
|
1122
|
+
AuthenticationError: If not authenticated.
|
|
1123
|
+
NetworkError: If the HTTP request fails.
|
|
1124
|
+
"""
|
|
1125
|
+
if not self.session_data:
|
|
1126
|
+
raise AuthenticationError("Not authenticated - call authenticate() first")
|
|
1127
|
+
|
|
1128
|
+
iot_keys = self.session_data.get('iotPlatformKeys', {})
|
|
1129
|
+
if not iot_keys or 'accessid' not in iot_keys or 'accesskey' not in iot_keys:
|
|
1130
|
+
self._log("No OpenAPI credentials — cannot query dormancy status")
|
|
1131
|
+
return "unknown"
|
|
1132
|
+
|
|
1133
|
+
access_id = iot_keys['accessid']
|
|
1134
|
+
access_key = iot_keys['accesskey']
|
|
1135
|
+
formatted_sn = self._format_sn(serial_number)
|
|
1136
|
+
|
|
1137
|
+
signature, timeout = self._get_signature_for_openapi(
|
|
1138
|
+
'/openapi/device/status', 'query', access_key
|
|
1139
|
+
)
|
|
1140
|
+
params = {
|
|
1141
|
+
'accessid': access_id,
|
|
1142
|
+
'expires': timeout,
|
|
1143
|
+
'signature': signature,
|
|
1144
|
+
'action': 'query',
|
|
1145
|
+
'deviceid': formatted_sn,
|
|
1146
|
+
}
|
|
1147
|
+
headers = {
|
|
1148
|
+
"Accept": "*/*",
|
|
1149
|
+
"User-Agent": DEFAULT_HEADERS['User-Agent'],
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
try:
|
|
1153
|
+
response = self._make_request(
|
|
1154
|
+
'GET',
|
|
1155
|
+
f"{self.OPENAPI_BASE_URL}/openapi/device/status",
|
|
1156
|
+
headers=headers,
|
|
1157
|
+
params=params,
|
|
1158
|
+
timeout=DEFAULT_TIMEOUT,
|
|
1159
|
+
)
|
|
1160
|
+
data = response.json()
|
|
1161
|
+
status = data.get('status', 'unknown')
|
|
1162
|
+
self._log(f"Device {serial_number} online status: {status}")
|
|
1163
|
+
return status
|
|
1164
|
+
except requests.exceptions.RequestException as e:
|
|
1165
|
+
raise NetworkError(f"Device status request failed: {e}")
|
|
1166
|
+
except json.JSONDecodeError:
|
|
1167
|
+
raise CloudEdgeError("Failed to parse device status response")
|
|
1168
|
+
|
|
1169
|
+
def wake_device(
|
|
1170
|
+
self,
|
|
1171
|
+
serial_number: str,
|
|
1172
|
+
device_id: Optional[Union[int, str]] = None,
|
|
1173
|
+
) -> bool:
|
|
1174
|
+
"""Send a wake signal to a dormant battery camera.
|
|
1175
|
+
|
|
1176
|
+
Uses two independent mechanisms for reliability:
|
|
1177
|
+
|
|
1178
|
+
1. **OpenAPI** ``/openapi/device/awaken`` — cloud-to-camera push via
|
|
1179
|
+
the IoT platform (requires OpenAPI credentials).
|
|
1180
|
+
2. **REST** ``/v1/app/bell/remote/wake`` — legacy bell/doorbell wake
|
|
1181
|
+
signal (requires *device_id*).
|
|
1182
|
+
|
|
1183
|
+
Both methods are attempted; success is reported if at least one
|
|
1184
|
+
succeeds. Call :meth:`wait_for_online` or :meth:`ensure_online`
|
|
1185
|
+
afterwards to confirm the camera has woken.
|
|
1186
|
+
|
|
1187
|
+
Args:
|
|
1188
|
+
serial_number (str): Device serial number (snNum).
|
|
1189
|
+
device_id (int | str | None): Numeric device ID (deviceID).
|
|
1190
|
+
Optional but recommended — without it only method 1 is used.
|
|
1191
|
+
|
|
1192
|
+
Returns:
|
|
1193
|
+
bool: ``True`` if at least one wake method succeeded.
|
|
1194
|
+
|
|
1195
|
+
Raises:
|
|
1196
|
+
AuthenticationError: If not authenticated.
|
|
1197
|
+
"""
|
|
1198
|
+
if not self.session_data:
|
|
1199
|
+
raise AuthenticationError("Not authenticated - call authenticate() first")
|
|
1200
|
+
|
|
1201
|
+
success = False
|
|
1202
|
+
|
|
1203
|
+
# Method 1 — OpenAPI /openapi/device/awaken
|
|
1204
|
+
iot_keys = self.session_data.get('iotPlatformKeys', {})
|
|
1205
|
+
if iot_keys and 'accessid' in iot_keys and 'accesskey' in iot_keys:
|
|
1206
|
+
try:
|
|
1207
|
+
access_id = iot_keys['accessid']
|
|
1208
|
+
access_key = iot_keys['accesskey']
|
|
1209
|
+
formatted_sn = self._format_sn(serial_number)
|
|
1210
|
+
sid = (formatted_sn + str(int(time.time() * 1000)))[:30]
|
|
1211
|
+
|
|
1212
|
+
signature, timeout = self._get_signature_for_openapi(
|
|
1213
|
+
'/openapi/device/awaken', 'set', access_key
|
|
1214
|
+
)
|
|
1215
|
+
params = {
|
|
1216
|
+
'accessid': access_id,
|
|
1217
|
+
'expires': timeout,
|
|
1218
|
+
'signature': signature,
|
|
1219
|
+
'action': 'set',
|
|
1220
|
+
'deviceid': formatted_sn,
|
|
1221
|
+
'sid': sid,
|
|
1222
|
+
}
|
|
1223
|
+
headers = {
|
|
1224
|
+
"Accept": "*/*",
|
|
1225
|
+
"User-Agent": DEFAULT_HEADERS['User-Agent'],
|
|
1226
|
+
}
|
|
1227
|
+
response = self._make_request(
|
|
1228
|
+
'GET',
|
|
1229
|
+
f"{self.OPENAPI_BASE_URL}/openapi/device/awaken",
|
|
1230
|
+
headers=headers,
|
|
1231
|
+
params=params,
|
|
1232
|
+
timeout=DEFAULT_TIMEOUT,
|
|
1233
|
+
)
|
|
1234
|
+
if response.status_code == 200:
|
|
1235
|
+
self._log(f"OpenAPI wake sent for {serial_number}")
|
|
1236
|
+
success = True
|
|
1237
|
+
except Exception as e:
|
|
1238
|
+
self._log(f"OpenAPI wake failed: {e}")
|
|
1239
|
+
|
|
1240
|
+
# Method 2 — /v1/app/bell/remote/wake (requires device_id)
|
|
1241
|
+
if device_id is not None:
|
|
1242
|
+
try:
|
|
1243
|
+
device_body = self._generate_device_body({'deviceID': str(device_id)})
|
|
1244
|
+
headers = {
|
|
1245
|
+
"Accept": "*/*",
|
|
1246
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1247
|
+
"User-Agent": DEFAULT_HEADERS['User-Agent'],
|
|
1248
|
+
}
|
|
1249
|
+
response = self._make_request(
|
|
1250
|
+
'POST',
|
|
1251
|
+
f"{self.BASE_URL}/v1/app/bell/remote/wake",
|
|
1252
|
+
headers=headers,
|
|
1253
|
+
data=device_body,
|
|
1254
|
+
timeout=DEFAULT_TIMEOUT,
|
|
1255
|
+
)
|
|
1256
|
+
if response.json().get('resultCode') in ('1001', '0'):
|
|
1257
|
+
self._log(f"Bell wake sent for device_id={device_id}")
|
|
1258
|
+
success = True
|
|
1259
|
+
except Exception as e:
|
|
1260
|
+
self._log(f"Bell wake failed: {e}")
|
|
1261
|
+
|
|
1262
|
+
if not success:
|
|
1263
|
+
self._log(
|
|
1264
|
+
f"All wake methods failed for {serial_number} — device may be offline"
|
|
1265
|
+
)
|
|
1266
|
+
return success
|
|
1267
|
+
|
|
1268
|
+
def wait_for_online(
|
|
1269
|
+
self,
|
|
1270
|
+
serial_number: str,
|
|
1271
|
+
timeout: float = 30.0,
|
|
1272
|
+
poll_interval: float = 2.0,
|
|
1273
|
+
) -> bool:
|
|
1274
|
+
"""Poll until the device reports ``"online"`` status or the timeout expires.
|
|
1275
|
+
|
|
1276
|
+
Typically called right after :meth:`wake_device` to confirm the camera
|
|
1277
|
+
has woken up before issuing commands.
|
|
1278
|
+
|
|
1279
|
+
Args:
|
|
1280
|
+
serial_number (str): Device serial number.
|
|
1281
|
+
timeout (float): Maximum seconds to wait (default 30).
|
|
1282
|
+
poll_interval (float): Seconds between status checks (default 2).
|
|
1283
|
+
|
|
1284
|
+
Returns:
|
|
1285
|
+
bool: ``True`` if the camera came online within *timeout* seconds.
|
|
1286
|
+
"""
|
|
1287
|
+
deadline = time.time() + timeout
|
|
1288
|
+
while time.time() < deadline:
|
|
1289
|
+
try:
|
|
1290
|
+
status = self.get_device_online_status(serial_number)
|
|
1291
|
+
if status == DEVICE_STATUS_ONLINE:
|
|
1292
|
+
self._log(f"Device {serial_number} is now online")
|
|
1293
|
+
return True
|
|
1294
|
+
self._log(
|
|
1295
|
+
f"Device {serial_number} status={status}, "
|
|
1296
|
+
f"waiting ({deadline - time.time():.0f}s left)..."
|
|
1297
|
+
)
|
|
1298
|
+
except Exception as e:
|
|
1299
|
+
self._log(f"Status check error: {e}")
|
|
1300
|
+
|
|
1301
|
+
remaining = deadline - time.time()
|
|
1302
|
+
if remaining > 0:
|
|
1303
|
+
time.sleep(min(poll_interval, remaining))
|
|
1304
|
+
|
|
1305
|
+
self._log(f"Timeout waiting for {serial_number} to come online")
|
|
1306
|
+
return False
|
|
1307
|
+
|
|
1308
|
+
def ensure_online(
|
|
1309
|
+
self,
|
|
1310
|
+
serial_number: str,
|
|
1311
|
+
device_id: Optional[Union[int, str]] = None,
|
|
1312
|
+
timeout: float = 35.0,
|
|
1313
|
+
auto_wake: bool = True,
|
|
1314
|
+
) -> bool:
|
|
1315
|
+
"""Ensure a camera is online, waking it first if dormant.
|
|
1316
|
+
|
|
1317
|
+
This is the recommended guard to call before any command that requires
|
|
1318
|
+
the camera to be awake (e.g. :meth:`set_device_config`,
|
|
1319
|
+
:meth:`get_device_config` on live parameters).
|
|
1320
|
+
|
|
1321
|
+
Behaviour:
|
|
1322
|
+
|
|
1323
|
+
* **online** → returns ``True`` immediately.
|
|
1324
|
+
* **dormancy** → sends a wake signal (if *auto_wake* is ``True``),
|
|
1325
|
+
then polls until online or *timeout* expires.
|
|
1326
|
+
* **offline** → returns ``False`` immediately (offline cameras cannot
|
|
1327
|
+
be woken over the cloud).
|
|
1328
|
+
* **unknown** → returns ``False`` (credentials or network issue).
|
|
1329
|
+
|
|
1330
|
+
Args:
|
|
1331
|
+
serial_number (str): Device serial number.
|
|
1332
|
+
device_id (int | str | None): Numeric device ID, passed to
|
|
1333
|
+
:meth:`wake_device` for the secondary wake method.
|
|
1334
|
+
timeout (float): Max seconds to wait after waking (default 35).
|
|
1335
|
+
auto_wake (bool): Send wake signal when dormant (default ``True``).
|
|
1336
|
+
|
|
1337
|
+
Returns:
|
|
1338
|
+
bool: ``True`` if the camera is (or becomes) online.
|
|
1339
|
+
|
|
1340
|
+
Example::
|
|
1341
|
+
|
|
1342
|
+
if client.ensure_online(sn, device_id):
|
|
1343
|
+
client.set_device_config(sn, {"1": 1})
|
|
1344
|
+
else:
|
|
1345
|
+
print("Camera unreachable — skipping command")
|
|
1346
|
+
"""
|
|
1347
|
+
status = self.get_device_online_status(serial_number)
|
|
1348
|
+
|
|
1349
|
+
if status == DEVICE_STATUS_ONLINE:
|
|
1350
|
+
return True
|
|
1351
|
+
|
|
1352
|
+
if status == DEVICE_STATUS_OFFLINE:
|
|
1353
|
+
self._log(f"Device {serial_number} is offline — cannot wake")
|
|
1354
|
+
return False
|
|
1355
|
+
|
|
1356
|
+
if status == DEVICE_STATUS_DORMANCY:
|
|
1357
|
+
if not auto_wake:
|
|
1358
|
+
self._log(
|
|
1359
|
+
f"Device {serial_number} is dormant but auto_wake=False"
|
|
1360
|
+
)
|
|
1361
|
+
return False
|
|
1362
|
+
self._log(f"Device {serial_number} is dormant — sending wake signal")
|
|
1363
|
+
self.wake_device(serial_number, device_id)
|
|
1364
|
+
return self.wait_for_online(serial_number, timeout=timeout)
|
|
1365
|
+
|
|
1366
|
+
# Unknown status
|
|
1367
|
+
self._log(
|
|
1368
|
+
f"Device {serial_number} status={status!r} — cannot determine reachability"
|
|
1369
|
+
)
|
|
1370
|
+
return False
|
|
1371
|
+
|
|
1095
1372
|
def get_device_config(self, device_serial: str,
|
|
1096
1373
|
parameter_codes: Optional[List[str]] = None) -> Optional[Dict]:
|
|
1097
1374
|
"""
|
|
@@ -1178,24 +1455,58 @@ class CloudEdgeClient:
|
|
|
1178
1455
|
except json.JSONDecodeError as e:
|
|
1179
1456
|
raise ConfigurationError(f"Failed to parse config response: {e}")
|
|
1180
1457
|
|
|
1181
|
-
def set_device_config(
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1458
|
+
def set_device_config(
|
|
1459
|
+
self,
|
|
1460
|
+
device_serial: str,
|
|
1461
|
+
parameters: Dict[str, Any],
|
|
1462
|
+
auto_wake: bool = True,
|
|
1463
|
+
device_id: Optional[Union[int, str]] = None,
|
|
1464
|
+
) -> bool:
|
|
1465
|
+
"""Set device configuration parameters.
|
|
1466
|
+
|
|
1467
|
+
For battery cameras that may be in ``dormancy`` state, pass
|
|
1468
|
+
``auto_wake=True`` (default) together with *device_id* so the method
|
|
1469
|
+
will automatically wake the camera and wait for it to come online
|
|
1470
|
+
before sending the command.
|
|
1471
|
+
|
|
1185
1472
|
Args:
|
|
1186
|
-
device_serial (str): Device serial number
|
|
1187
|
-
parameters (Dict[str, Any]): Parameter codes and values to set
|
|
1188
|
-
|
|
1473
|
+
device_serial (str): Device serial number.
|
|
1474
|
+
parameters (Dict[str, Any]): Parameter codes and values to set,
|
|
1475
|
+
e.g. ``{"1": 1}`` or ``{"PIR_SWITCH": 1}``.
|
|
1476
|
+
auto_wake (bool): Wake a dormant camera automatically before
|
|
1477
|
+
issuing the command (default ``True``).
|
|
1478
|
+
device_id (int | str | None): Numeric device ID used by the
|
|
1479
|
+
secondary wake method. Ignored when *auto_wake* is ``False``.
|
|
1480
|
+
|
|
1189
1481
|
Returns:
|
|
1190
|
-
bool: True if successful
|
|
1191
|
-
|
|
1482
|
+
bool: ``True`` if successful.
|
|
1483
|
+
|
|
1192
1484
|
Raises:
|
|
1193
|
-
AuthenticationError: If not authenticated
|
|
1194
|
-
ConfigurationError: If configuration setting fails
|
|
1485
|
+
AuthenticationError: If not authenticated.
|
|
1486
|
+
ConfigurationError: If configuration setting fails.
|
|
1195
1487
|
"""
|
|
1196
1488
|
if not self.session_data:
|
|
1197
1489
|
raise AuthenticationError("Not authenticated - call authenticate() first")
|
|
1198
|
-
|
|
1490
|
+
|
|
1491
|
+
# Wake dormant battery cameras before sending the command
|
|
1492
|
+
if auto_wake:
|
|
1493
|
+
status = self.get_device_online_status(device_serial)
|
|
1494
|
+
if status == DEVICE_STATUS_DORMANCY:
|
|
1495
|
+
self._log(
|
|
1496
|
+
f"set_device_config: device {device_serial} is dormant — waking first"
|
|
1497
|
+
)
|
|
1498
|
+
self.wake_device(device_serial, device_id)
|
|
1499
|
+
if not self.wait_for_online(device_serial):
|
|
1500
|
+
raise ConfigurationError(
|
|
1501
|
+
"Device did not come online after wake signal",
|
|
1502
|
+
details={"device_serial": device_serial},
|
|
1503
|
+
)
|
|
1504
|
+
elif status == DEVICE_STATUS_OFFLINE:
|
|
1505
|
+
raise ConfigurationError(
|
|
1506
|
+
"Device is offline — cannot send configuration",
|
|
1507
|
+
details={"device_serial": device_serial},
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1199
1510
|
self._log(f"Setting device configuration for SN: {device_serial}")
|
|
1200
1511
|
|
|
1201
1512
|
# Check if we have OpenAPI credentials
|
|
@@ -1294,45 +1605,54 @@ class CloudEdgeClient:
|
|
|
1294
1605
|
|
|
1295
1606
|
return None
|
|
1296
1607
|
|
|
1297
|
-
def set_device_parameter(
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1608
|
+
def set_device_parameter(
|
|
1609
|
+
self,
|
|
1610
|
+
device_name: str,
|
|
1611
|
+
parameter_name: str,
|
|
1612
|
+
value: Union[int, str, float],
|
|
1613
|
+
auto_wake: bool = True,
|
|
1614
|
+
) -> bool:
|
|
1615
|
+
"""Set a single device parameter by name.
|
|
1616
|
+
|
|
1302
1617
|
Args:
|
|
1303
|
-
device_name (str): Device name
|
|
1304
|
-
parameter_name (str): Parameter name
|
|
1305
|
-
value (
|
|
1306
|
-
|
|
1618
|
+
device_name (str): Device name.
|
|
1619
|
+
parameter_name (str): Parameter name, e.g. ``"FRONT_LIGHT_SWITCH"``.
|
|
1620
|
+
value (int | str | float): Parameter value.
|
|
1621
|
+
auto_wake (bool): Wake a dormant camera before sending the command
|
|
1622
|
+
(default ``True``). The device's numeric ID is resolved
|
|
1623
|
+
automatically from the device list.
|
|
1624
|
+
|
|
1307
1625
|
Returns:
|
|
1308
|
-
bool: True if successful
|
|
1309
|
-
|
|
1626
|
+
bool: ``True`` if successful.
|
|
1627
|
+
|
|
1310
1628
|
Raises:
|
|
1311
|
-
DeviceNotFoundError: If device not found
|
|
1312
|
-
ConfigurationError: If parameter is invalid or setting fails
|
|
1629
|
+
DeviceNotFoundError: If device not found.
|
|
1630
|
+
ConfigurationError: If parameter is invalid or setting fails.
|
|
1313
1631
|
"""
|
|
1314
|
-
# Find device
|
|
1315
1632
|
device = self.find_device_by_name(device_name)
|
|
1316
1633
|
if not device:
|
|
1317
1634
|
raise DeviceNotFoundError(f"Device '{device_name}' not found")
|
|
1318
|
-
|
|
1319
|
-
# Get parameter code
|
|
1635
|
+
|
|
1320
1636
|
parameter_code = get_parameter_code_by_name(parameter_name)
|
|
1321
1637
|
if not parameter_code:
|
|
1322
1638
|
raise ConfigurationError(
|
|
1323
1639
|
f"Unknown parameter: {parameter_name}",
|
|
1324
|
-
details={"parameter_name": parameter_name, "device": device_name}
|
|
1640
|
+
details={"parameter_name": parameter_name, "device": device_name},
|
|
1325
1641
|
)
|
|
1326
|
-
|
|
1327
|
-
# Set parameter
|
|
1642
|
+
|
|
1328
1643
|
parameters = {parameter_code: value}
|
|
1329
|
-
success = self.set_device_config(
|
|
1330
|
-
|
|
1644
|
+
success = self.set_device_config(
|
|
1645
|
+
device['serial_number'],
|
|
1646
|
+
parameters,
|
|
1647
|
+
auto_wake=auto_wake,
|
|
1648
|
+
device_id=device.get('device_id'),
|
|
1649
|
+
)
|
|
1650
|
+
|
|
1331
1651
|
if success:
|
|
1332
1652
|
param_display = get_parameter_name(parameter_code)
|
|
1333
1653
|
formatted_value = format_parameter_value(param_display, value)
|
|
1334
1654
|
self._log(f"Set {param_display} = {formatted_value} on device '{device_name}'")
|
|
1335
|
-
|
|
1655
|
+
|
|
1336
1656
|
return success
|
|
1337
1657
|
|
|
1338
1658
|
def get_device_info(self, device_name: str, include_config: bool = True) -> Optional[Dict]:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|