pycloudedge 0.1.3__tar.gz → 0.1.4.dev2__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.3 → pycloudedge-0.1.4.dev2}/PKG-INFO +1 -1
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/_version.py +3 -3
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/client.py +117 -42
- pycloudedge-0.1.4.dev2/cloudedge/constants.py +68 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/PKG-INFO +1 -1
- pycloudedge-0.1.3/cloudedge/constants.py +0 -33
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/.env.example +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/.gitignore +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/LICENSE +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/MANIFEST.in +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/README.md +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/__init__.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/cli.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/exceptions.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/iot_parameters.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/logging_config.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/utils.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/validators.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/examples/README.md +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/examples/basic_example.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/examples/device_control.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/examples/network_ping_status.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/SOURCES.txt +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/dependency_links.txt +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/entry_points.txt +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/requires.txt +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/top_level.txt +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pyproject.toml +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/requirements-dev.txt +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/requirements.txt +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/setup.cfg +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/setup.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/tests/test_basic.py +0 -0
- {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/tests/test_improvements.py +0 -0
|
@@ -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.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.4.dev2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 4, 'dev2')
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gf40c10343'
|
|
@@ -35,11 +35,63 @@ from .iot_parameters import (
|
|
|
35
35
|
get_parameter_code_by_name,
|
|
36
36
|
format_parameter_value
|
|
37
37
|
)
|
|
38
|
-
from .constants import
|
|
38
|
+
from .constants import (
|
|
39
|
+
CA_KEY, DEFAULT_HEADERS, DEFAULT_TIMEOUT,
|
|
40
|
+
REGION_ENDPOINTS, REGION_EU, REGION_US,
|
|
41
|
+
region_for_country,
|
|
42
|
+
)
|
|
39
43
|
from .validators import validate_email, validate_country_code, validate_phone_code
|
|
40
44
|
from .logging_config import get_logger
|
|
41
45
|
from .utils import retry_on_failure
|
|
42
46
|
|
|
47
|
+
# API list keys → readable product type when deviceTypeName is a CDN image URL
|
|
48
|
+
_DEVICE_LIST_CATEGORY_LABELS = {
|
|
49
|
+
"snap": "Camera",
|
|
50
|
+
"ipc": "Camera",
|
|
51
|
+
"nvr": "NVR",
|
|
52
|
+
"doorbell": "Doorbell",
|
|
53
|
+
"chime": "Chime",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _device_icon_url_from_type_name(device_type_name: Any) -> Optional[str]:
|
|
58
|
+
"""Return URL if deviceTypeName is an http(s) icon URL (Meari/OSS), else None."""
|
|
59
|
+
if not isinstance(device_type_name, str):
|
|
60
|
+
return None
|
|
61
|
+
s = device_type_name.strip()
|
|
62
|
+
if s.startswith(("http://", "https://")):
|
|
63
|
+
return s
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _human_type_and_icon_url(
|
|
68
|
+
device: Dict[str, Any],
|
|
69
|
+
list_category: Optional[str] = None,
|
|
70
|
+
) -> tuple[str, Optional[str]]:
|
|
71
|
+
"""
|
|
72
|
+
CloudEdge often puts a product image URL in deviceTypeName instead of a label.
|
|
73
|
+
Return a human-readable type for UIs (e.g. Home Assistant model) and optional icon URL.
|
|
74
|
+
"""
|
|
75
|
+
raw = device.get("deviceTypeName")
|
|
76
|
+
icon_url = _device_icon_url_from_type_name(raw)
|
|
77
|
+
if icon_url is not None:
|
|
78
|
+
label = _DEVICE_LIST_CATEGORY_LABELS.get(list_category or "", "SmartEye Camera")
|
|
79
|
+
lower = icon_url.lower()
|
|
80
|
+
if "doorbell" in lower:
|
|
81
|
+
label = "Doorbell"
|
|
82
|
+
elif "chime" in lower:
|
|
83
|
+
label = "Chime"
|
|
84
|
+
elif "nvr" in lower:
|
|
85
|
+
label = "NVR"
|
|
86
|
+
elif "snap" in lower or "ipc" in lower:
|
|
87
|
+
label = "Camera"
|
|
88
|
+
return label, icon_url
|
|
89
|
+
if raw is not None and str(raw).strip():
|
|
90
|
+
return str(raw).strip(), None
|
|
91
|
+
if list_category:
|
|
92
|
+
return _DEVICE_LIST_CATEGORY_LABELS.get(list_category, "Unknown"), None
|
|
93
|
+
return "Unknown", None
|
|
94
|
+
|
|
43
95
|
|
|
44
96
|
class CloudEdgeClient:
|
|
45
97
|
"""
|
|
@@ -64,9 +116,6 @@ class CloudEdgeClient:
|
|
|
64
116
|
... print(f"Device: {device['name']} - Status: {device['online']}")
|
|
65
117
|
"""
|
|
66
118
|
|
|
67
|
-
BASE_URL = "https://apis-eu-frankfurt.cloudedge360.com"
|
|
68
|
-
OPENAPI_BASE_URL = "https://openapi-euce.mearicloud.com"
|
|
69
|
-
|
|
70
119
|
def __init__(
|
|
71
120
|
self,
|
|
72
121
|
username: str,
|
|
@@ -76,7 +125,8 @@ class CloudEdgeClient:
|
|
|
76
125
|
debug: bool = False,
|
|
77
126
|
session_cache_file: str = ".cloudedge_session_cache",
|
|
78
127
|
enable_network_ping: bool = True,
|
|
79
|
-
ping_timeout: float = 2.0
|
|
128
|
+
ping_timeout: float = 2.0,
|
|
129
|
+
region: Optional[str] = None,
|
|
80
130
|
):
|
|
81
131
|
"""
|
|
82
132
|
Initialize CloudEdge API client.
|
|
@@ -90,6 +140,9 @@ class CloudEdgeClient:
|
|
|
90
140
|
session_cache_file (str): Path to session cache file
|
|
91
141
|
enable_network_ping (bool): Enable ping-based online status when on same network
|
|
92
142
|
ping_timeout (float): Ping timeout in seconds
|
|
143
|
+
region (str, optional): Force a specific region ("eu" or "us").
|
|
144
|
+
When *None* the region is derived automatically from *country_code*:
|
|
145
|
+
European countries use the EU endpoints, everything else uses US.
|
|
93
146
|
|
|
94
147
|
Raises:
|
|
95
148
|
ValidationError: If input validation fails
|
|
@@ -118,6 +171,13 @@ class CloudEdgeClient:
|
|
|
118
171
|
self.country_code = country_code.upper()
|
|
119
172
|
self.phone_code = phone_code if phone_code.startswith('+') else f'+{phone_code}'
|
|
120
173
|
|
|
174
|
+
# Resolve region and endpoints
|
|
175
|
+
resolved_region = region if region in REGION_ENDPOINTS else region_for_country(self.country_code)
|
|
176
|
+
endpoints = REGION_ENDPOINTS[resolved_region]
|
|
177
|
+
self.region = resolved_region
|
|
178
|
+
self.BASE_URL = endpoints["base_url"]
|
|
179
|
+
self.OPENAPI_BASE_URL = endpoints["openapi_base_url"]
|
|
180
|
+
|
|
121
181
|
# Setup proper logging
|
|
122
182
|
self.logger = get_logger("client")
|
|
123
183
|
if debug:
|
|
@@ -775,15 +835,19 @@ class CloudEdgeClient:
|
|
|
775
835
|
device_list = response_data[device_type]
|
|
776
836
|
if isinstance(device_list, list):
|
|
777
837
|
for device in device_list:
|
|
838
|
+
type_str, icon_url = _human_type_and_icon_url(
|
|
839
|
+
device, list_category=device_type
|
|
840
|
+
)
|
|
778
841
|
device_dict = {
|
|
779
842
|
'device_id': device.get('deviceID'),
|
|
780
843
|
'serial_number': device.get('snNum'),
|
|
781
844
|
'name': device.get('deviceName', 'Unnamed'),
|
|
782
|
-
'type':
|
|
845
|
+
'type': type_str,
|
|
783
846
|
'type_id': device.get('devTypeID'),
|
|
784
847
|
'host_key': device.get('hostKey'),
|
|
785
848
|
'online': device.get('devStatus') == 1, # Store original API status
|
|
786
|
-
'home_id': home_id
|
|
849
|
+
'home_id': home_id,
|
|
850
|
+
'device_icon_url': icon_url,
|
|
787
851
|
}
|
|
788
852
|
|
|
789
853
|
# Get enhanced online status
|
|
@@ -908,43 +972,54 @@ class CloudEdgeClient:
|
|
|
908
972
|
import json
|
|
909
973
|
self._log(f"API Response structure: {json.dumps(response_data, indent=2)}")
|
|
910
974
|
|
|
911
|
-
devices = []
|
|
912
|
-
|
|
913
|
-
# Check for devices in different device type keys (working format)
|
|
914
975
|
device_types = ['nvr', 'ipc', 'chime', 'doorbell', 'snap']
|
|
915
|
-
|
|
916
|
-
for device_type in device_types:
|
|
917
|
-
if device_type in response_data and response_data[device_type]:
|
|
918
|
-
device_list = response_data[device_type]
|
|
919
|
-
if isinstance(device_list, list):
|
|
920
|
-
self._log(f"Found {len(device_list)} devices under '{device_type}' key")
|
|
921
|
-
devices.extend(device_list)
|
|
922
|
-
|
|
923
|
-
# Fallback: check for devices in result.deviceList (older format)
|
|
924
|
-
if not devices:
|
|
925
|
-
device_list = response_data.get("result", {}).get("deviceList", [])
|
|
926
|
-
if isinstance(device_list, list) and device_list:
|
|
927
|
-
self._log(f"Found {len(device_list)} devices under 'result.deviceList' key")
|
|
928
|
-
devices.extend(device_list)
|
|
929
|
-
|
|
930
|
-
# Convert to standardized format
|
|
931
976
|
standardized_devices = []
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
977
|
+
|
|
978
|
+
# Modern API: devices grouped by category key
|
|
979
|
+
for device_type in device_types:
|
|
980
|
+
device_list = response_data.get(device_type)
|
|
981
|
+
if not isinstance(device_list, list) or not device_list:
|
|
982
|
+
continue
|
|
983
|
+
self._log(f"Found {len(device_list)} devices under '{device_type}' key")
|
|
984
|
+
for device in device_list:
|
|
985
|
+
type_str, icon_url = _human_type_and_icon_url(
|
|
986
|
+
device, list_category=device_type
|
|
987
|
+
)
|
|
988
|
+
device_dict = {
|
|
989
|
+
'device_id': device.get('deviceID'),
|
|
990
|
+
'serial_number': device.get('snNum'),
|
|
991
|
+
'name': device.get('deviceName', 'Unnamed'),
|
|
992
|
+
'type': type_str,
|
|
993
|
+
'type_id': device.get('devTypeID'),
|
|
994
|
+
'host_key': device.get('hostKey'),
|
|
995
|
+
'online': device.get('onLine') == 1,
|
|
996
|
+
'device_icon_url': icon_url,
|
|
997
|
+
}
|
|
998
|
+
device_dict['online'] = self._get_enhanced_device_status(device_dict)
|
|
999
|
+
standardized_devices.append(device_dict)
|
|
1000
|
+
|
|
1001
|
+
# Older API fallback: devices under result.deviceList
|
|
1002
|
+
if not standardized_devices:
|
|
1003
|
+
fallback_list = response_data.get("result", {}).get("deviceList", [])
|
|
1004
|
+
if isinstance(fallback_list, list) and fallback_list:
|
|
1005
|
+
self._log(f"Found {len(fallback_list)} devices under 'result.deviceList' key")
|
|
1006
|
+
for device in fallback_list:
|
|
1007
|
+
type_str, icon_url = _human_type_and_icon_url(
|
|
1008
|
+
device, list_category=None
|
|
1009
|
+
)
|
|
1010
|
+
device_dict = {
|
|
1011
|
+
'device_id': device.get('deviceID'),
|
|
1012
|
+
'serial_number': device.get('snNum'),
|
|
1013
|
+
'name': device.get('deviceName', 'Unnamed'),
|
|
1014
|
+
'type': type_str,
|
|
1015
|
+
'type_id': device.get('devTypeID'),
|
|
1016
|
+
'host_key': device.get('hostKey'),
|
|
1017
|
+
'online': device.get('onLine') == 1,
|
|
1018
|
+
'device_icon_url': icon_url,
|
|
1019
|
+
}
|
|
1020
|
+
device_dict['online'] = self._get_enhanced_device_status(device_dict)
|
|
1021
|
+
standardized_devices.append(device_dict)
|
|
1022
|
+
|
|
948
1023
|
return standardized_devices
|
|
949
1024
|
else:
|
|
950
1025
|
error_msg = response_data.get('resultMsg', 'Unknown error')
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration constants for CloudEdge API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Region identifiers
|
|
6
|
+
REGION_EU = "eu"
|
|
7
|
+
REGION_US = "us"
|
|
8
|
+
|
|
9
|
+
# Per-region API endpoints
|
|
10
|
+
REGION_ENDPOINTS = {
|
|
11
|
+
REGION_EU: {
|
|
12
|
+
"base_url": "https://apis-eu-frankfurt.cloudedge360.com",
|
|
13
|
+
"openapi_base_url": "https://openapi-euce.mearicloud.com",
|
|
14
|
+
},
|
|
15
|
+
REGION_US: {
|
|
16
|
+
"base_url": "https://apis-us-west.cloudedge360.com",
|
|
17
|
+
"openapi_base_url": "https://openapi-uswe.mearicloud.com",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Default endpoints (EU) – kept for backward compatibility
|
|
22
|
+
BASE_URL = REGION_ENDPOINTS[REGION_EU]["base_url"]
|
|
23
|
+
OPENAPI_BASE_URL = REGION_ENDPOINTS[REGION_EU]["openapi_base_url"]
|
|
24
|
+
|
|
25
|
+
# European country codes (ISO 3166-1 alpha-2)
|
|
26
|
+
EU_COUNTRY_CODES = {
|
|
27
|
+
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
|
|
28
|
+
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
|
|
29
|
+
"PL", "PT", "RO", "SK", "SI", "ES", "SE",
|
|
30
|
+
# EEA + EFTA + UK
|
|
31
|
+
"IS", "LI", "NO", "CH", "GB",
|
|
32
|
+
# Other European countries
|
|
33
|
+
"AL", "AD", "AM", "AZ", "BA", "BY", "GE", "GI", "XK", "MD",
|
|
34
|
+
"MC", "ME", "MK", "RS", "RU", "SM", "TR", "UA", "VA",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def region_for_country(country_code: str) -> str:
|
|
39
|
+
"""Return the region key for a given ISO 3166-1 alpha-2 country code."""
|
|
40
|
+
if country_code.upper() in EU_COUNTRY_CODES:
|
|
41
|
+
return REGION_EU
|
|
42
|
+
return REGION_US
|
|
43
|
+
|
|
44
|
+
# API Keys (these are public keys from the mobile app)
|
|
45
|
+
CA_KEY = "bc29be30292a4309877807e101afbd51"
|
|
46
|
+
|
|
47
|
+
# Default Headers
|
|
48
|
+
DEFAULT_HEADERS = {
|
|
49
|
+
"Accept-Language": "en-US,en;q=0.8",
|
|
50
|
+
"User-Agent": "Mozilla/5.0 (Linux; U; Android 10; en-us; Android SDK built for arm64 Build/QSR1.211112.002) AppleWebKit/533.1 (KHTML, like Gecko) Version/5.0 Mobile Safari/533.1",
|
|
51
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
52
|
+
"Accept-Encoding": "gzip, deflate, br"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# API Constants
|
|
56
|
+
PHONE_TYPE = "a"
|
|
57
|
+
SOURCE_APP = "8"
|
|
58
|
+
APP_VERSION = "5.5.1"
|
|
59
|
+
IOT_TYPE = "4"
|
|
60
|
+
APP_VERSION_CODE = "551"
|
|
61
|
+
DEFAULT_LANGUAGE = "en"
|
|
62
|
+
|
|
63
|
+
# Timeout values (seconds)
|
|
64
|
+
DEFAULT_TIMEOUT = 30
|
|
65
|
+
PING_TIMEOUT = 2.0
|
|
66
|
+
|
|
67
|
+
# Cache settings
|
|
68
|
+
DEFAULT_CACHE_FILE = ".cloudedge_session_cache"
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Configuration constants for CloudEdge API
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
# API Endpoints
|
|
6
|
-
BASE_URL = "https://apis-eu-frankfurt.cloudedge360.com"
|
|
7
|
-
OPENAPI_BASE_URL = "https://openapi-euce.mearicloud.com"
|
|
8
|
-
|
|
9
|
-
# API Keys (these are public keys from the mobile app)
|
|
10
|
-
CA_KEY = "bc29be30292a4309877807e101afbd51"
|
|
11
|
-
|
|
12
|
-
# Default Headers
|
|
13
|
-
DEFAULT_HEADERS = {
|
|
14
|
-
"Accept-Language": "en-US,en;q=0.8",
|
|
15
|
-
"User-Agent": "Mozilla/5.0 (Linux; U; Android 10; en-us; Android SDK built for arm64 Build/QSR1.211112.002) AppleWebKit/533.1 (KHTML, like Gecko) Version/5.0 Mobile Safari/533.1",
|
|
16
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
17
|
-
"Accept-Encoding": "gzip, deflate, br"
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
# API Constants
|
|
21
|
-
PHONE_TYPE = "a"
|
|
22
|
-
SOURCE_APP = "8"
|
|
23
|
-
APP_VERSION = "5.5.1"
|
|
24
|
-
IOT_TYPE = "4"
|
|
25
|
-
APP_VERSION_CODE = "551"
|
|
26
|
-
DEFAULT_LANGUAGE = "en"
|
|
27
|
-
|
|
28
|
-
# Timeout values (seconds)
|
|
29
|
-
DEFAULT_TIMEOUT = 30
|
|
30
|
-
PING_TIMEOUT = 2.0
|
|
31
|
-
|
|
32
|
-
# Cache settings
|
|
33
|
-
DEFAULT_CACHE_FILE = ".cloudedge_session_cache"
|
|
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
|
|
File without changes
|