pycloudedge 0.1.6.dev1__tar.gz → 0.1.6.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.6.dev1 → pycloudedge-0.1.6.dev2}/.gitignore +1 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/PKG-INFO +1 -1
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/_version.py +2 -2
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/client.py +105 -5
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/constants.py +35 -2
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/PKG-INFO +1 -1
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/.env.example +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/LICENSE +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/MANIFEST.in +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/README.md +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/__init__.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/cli.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/exceptions.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/image_decrypt.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/iot_parameters.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/logging_config.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/mqtt.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/utils.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/validators.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/examples/README.md +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/examples/basic_example.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/examples/device_control.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/examples/network_ping_status.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/SOURCES.txt +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/dependency_links.txt +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/entry_points.txt +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/requires.txt +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/top_level.txt +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pyproject.toml +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/requirements-dev.txt +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/requirements.txt +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/setup.cfg +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/setup.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/tests/test_basic.py +0 -0
- {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.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.6.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1, 6, '
|
|
31
|
+
__version__ = version = '0.1.6.dev2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 6, 'dev2')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -36,8 +36,9 @@ from .iot_parameters import (
|
|
|
36
36
|
format_parameter_value
|
|
37
37
|
)
|
|
38
38
|
from .constants import (
|
|
39
|
-
CA_KEY, DEFAULT_HEADERS, DEFAULT_TIMEOUT,
|
|
40
|
-
REGION_ENDPOINTS, REGION_EU, REGION_US,
|
|
39
|
+
CA_KEY, CA_SECRET, DEFAULT_HEADERS, DEFAULT_TIMEOUT,
|
|
40
|
+
REGION_ENDPOINTS, REGION_EU, REGION_US, REGION_AP,
|
|
41
|
+
REDIRECT_URL,
|
|
41
42
|
region_for_country,
|
|
42
43
|
)
|
|
43
44
|
from .validators import validate_email, validate_country_code, validate_phone_code
|
|
@@ -145,9 +146,10 @@ class CloudEdgeClient:
|
|
|
145
146
|
session_cache_file (str): Path to session cache file
|
|
146
147
|
enable_network_ping (bool): Enable ping-based online status when on same network
|
|
147
148
|
ping_timeout (float): Ping timeout in seconds
|
|
148
|
-
region (str, optional): Force a specific region ("eu" or "
|
|
149
|
+
region (str, optional): Force a specific region ("eu", "us", or "ap").
|
|
149
150
|
When *None* the region is derived automatically from *country_code*:
|
|
150
|
-
European countries
|
|
151
|
+
European countries → EU, Asia-Pacific/Oceania/ME/Africa → AP
|
|
152
|
+
(Singapore), everything else → US.
|
|
151
153
|
|
|
152
154
|
Raises:
|
|
153
155
|
ValidationError: If input validation fails
|
|
@@ -547,6 +549,95 @@ class CloudEdgeClient:
|
|
|
547
549
|
except Exception as e:
|
|
548
550
|
self._log(f"Failed to save session cache: {e}")
|
|
549
551
|
|
|
552
|
+
def _discover_endpoints(self) -> None:
|
|
553
|
+
"""Call the global redirect API to discover the correct regional server.
|
|
554
|
+
|
|
555
|
+
The CloudEdge mobile app calls ``/ppstrongs/redirect`` on the global
|
|
556
|
+
gateway (``apis.cloudedge360.com``) **before** login. The response
|
|
557
|
+
contains the regional ``apiServer`` and ``openapi`` domain that must be
|
|
558
|
+
used for the given account/country.
|
|
559
|
+
|
|
560
|
+
On success, ``self.BASE_URL`` and ``self.OPENAPI_BASE_URL`` are updated.
|
|
561
|
+
On failure the previously configured (static) endpoints are kept.
|
|
562
|
+
"""
|
|
563
|
+
import random
|
|
564
|
+
|
|
565
|
+
timestamp = int(time.time() * 1000)
|
|
566
|
+
encrypted_account = self._aes_encode_param(
|
|
567
|
+
self.username, "/ppstrongs/redirect", timestamp=timestamp,
|
|
568
|
+
)
|
|
569
|
+
query_nonce = "".join(
|
|
570
|
+
[str(random.randint(0, 9)) for _ in range(8)]
|
|
571
|
+
)
|
|
572
|
+
header_nonce = str(random.randint(100000, 999999))
|
|
573
|
+
|
|
574
|
+
phone_code_bare = self.phone_code.lstrip("+")
|
|
575
|
+
params = {
|
|
576
|
+
"phoneType": "a",
|
|
577
|
+
"sourceApp": "8",
|
|
578
|
+
"appVer": "5.5.1",
|
|
579
|
+
"appVerCode": "551",
|
|
580
|
+
"localTime": str(timestamp),
|
|
581
|
+
"t": str(timestamp),
|
|
582
|
+
"lngType": "en",
|
|
583
|
+
"countryCode": self.country_code,
|
|
584
|
+
"userAccount": encrypted_account,
|
|
585
|
+
"phoneCode": phone_code_bare,
|
|
586
|
+
"partnerId": "8",
|
|
587
|
+
"nonce": query_nonce,
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
# sign = MD5("GET|/ppstrongs/redirect|{timestamp}|apis.meari.com.cn")
|
|
591
|
+
params["sign"] = hashlib.md5(
|
|
592
|
+
f"GET|/ppstrongs/redirect|{timestamp}|apis.meari.com.cn".encode()
|
|
593
|
+
).hexdigest()
|
|
594
|
+
|
|
595
|
+
# X-Ca-Sign: HMAC-SHA1 with CA_SECRET as key
|
|
596
|
+
# The app builds: "api=/ppstrongs/" + getRealUrl(path) + "|X-Ca-Key=...|..."
|
|
597
|
+
# getRealUrl("/ppstrongs/redirect") returns "/ppstrongs/redirect" unchanged,
|
|
598
|
+
# so the full string is "api=/ppstrongs//ppstrongs/redirect|..."
|
|
599
|
+
ca_sign_data = (
|
|
600
|
+
f"api=/ppstrongs//ppstrongs/redirect"
|
|
601
|
+
f"|X-Ca-Key={CA_KEY}"
|
|
602
|
+
f"|X-Ca-Timestamp={timestamp}"
|
|
603
|
+
f"|X-Ca-Nonce={header_nonce}"
|
|
604
|
+
)
|
|
605
|
+
ca_signature = base64.b64encode(
|
|
606
|
+
hmac.new(CA_SECRET.encode(), ca_sign_data.encode(), hashlib.sha1).digest()
|
|
607
|
+
).decode()
|
|
608
|
+
|
|
609
|
+
headers = {
|
|
610
|
+
**DEFAULT_HEADERS,
|
|
611
|
+
"X-Ca-Timestamp": str(timestamp),
|
|
612
|
+
"X-Ca-Sign": ca_signature,
|
|
613
|
+
"X-Ca-Key": CA_KEY,
|
|
614
|
+
"X-Ca-Nonce": header_nonce,
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
resp = self._session.get(
|
|
619
|
+
REDIRECT_URL, params=params, headers=headers, timeout=DEFAULT_TIMEOUT,
|
|
620
|
+
)
|
|
621
|
+
data = resp.json()
|
|
622
|
+
if data.get("resultCode") != "1001":
|
|
623
|
+
self._log(f"Redirect discovery returned {data.get('resultCode')}")
|
|
624
|
+
return
|
|
625
|
+
|
|
626
|
+
result = data.get("result", {})
|
|
627
|
+
api_server = result.get("apiServer") or result.get("gwUrl") or ""
|
|
628
|
+
if api_server:
|
|
629
|
+
self.BASE_URL = api_server.rstrip("/")
|
|
630
|
+
self._log(f"Discovered API server: {self.BASE_URL}")
|
|
631
|
+
|
|
632
|
+
pf_api = result.get("pfApi", {})
|
|
633
|
+
openapi_domain = pf_api.get("openapi", {}).get("domain", "")
|
|
634
|
+
if openapi_domain:
|
|
635
|
+
self.OPENAPI_BASE_URL = openapi_domain.rstrip("/")
|
|
636
|
+
self._log(f"Discovered OpenAPI server: {self.OPENAPI_BASE_URL}")
|
|
637
|
+
|
|
638
|
+
except Exception as exc:
|
|
639
|
+
self._log(f"Endpoint discovery failed (will use static endpoints): {exc}")
|
|
640
|
+
|
|
550
641
|
def authenticate(self) -> bool:
|
|
551
642
|
"""
|
|
552
643
|
Authenticate with CloudEdge API.
|
|
@@ -562,6 +653,11 @@ class CloudEdgeClient:
|
|
|
562
653
|
self.session_data = self._load_session_cache()
|
|
563
654
|
if self.session_data:
|
|
564
655
|
self._log("Using cached session")
|
|
656
|
+
# Restore discovered endpoints from cache if present
|
|
657
|
+
if self.session_data.get("apiServer"):
|
|
658
|
+
self.BASE_URL = self.session_data["apiServer"]
|
|
659
|
+
if self.session_data.get("openapiServer"):
|
|
660
|
+
self.OPENAPI_BASE_URL = self.session_data["openapiServer"]
|
|
565
661
|
# Fetch MQTT config if not already present in the cached session
|
|
566
662
|
if not self.session_data.get("mqtt"):
|
|
567
663
|
try:
|
|
@@ -570,7 +666,10 @@ class CloudEdgeClient:
|
|
|
570
666
|
except Exception as exc:
|
|
571
667
|
self._log(f"IoT config fetch failed (non-fatal): {exc}")
|
|
572
668
|
return True
|
|
573
|
-
|
|
669
|
+
|
|
670
|
+
# Discover the correct regional endpoints before login
|
|
671
|
+
self._discover_endpoints()
|
|
672
|
+
|
|
574
673
|
self._log("Performing CloudEdge login...")
|
|
575
674
|
|
|
576
675
|
# Encrypt credentials
|
|
@@ -660,6 +759,7 @@ class CloudEdgeClient:
|
|
|
660
759
|
"caKey": ca_key,
|
|
661
760
|
"loginTime": int(time.time()),
|
|
662
761
|
"apiServer": self.BASE_URL,
|
|
762
|
+
"openapiServer": self.OPENAPI_BASE_URL,
|
|
663
763
|
"iotPlatformKeys": iot_platform_keys,
|
|
664
764
|
}
|
|
665
765
|
|
|
@@ -5,8 +5,9 @@ Configuration constants for CloudEdge API
|
|
|
5
5
|
# Region identifiers
|
|
6
6
|
REGION_EU = "eu"
|
|
7
7
|
REGION_US = "us"
|
|
8
|
+
REGION_AP = "ap"
|
|
8
9
|
|
|
9
|
-
# Per-region API endpoints
|
|
10
|
+
# Per-region API endpoints (static fallback when redirect discovery fails)
|
|
10
11
|
REGION_ENDPOINTS = {
|
|
11
12
|
REGION_EU: {
|
|
12
13
|
"base_url": "https://apis-eu-frankfurt.cloudedge360.com",
|
|
@@ -16,8 +17,15 @@ REGION_ENDPOINTS = {
|
|
|
16
17
|
"base_url": "https://apis-us-west.cloudedge360.com",
|
|
17
18
|
"openapi_base_url": "https://openapi-uswe.mearicloud.com",
|
|
18
19
|
},
|
|
20
|
+
REGION_AP: {
|
|
21
|
+
"base_url": "https://apis-as-singapore.cloudedge360.com",
|
|
22
|
+
"openapi_base_url": "https://openapi-usce.mearicloud.com",
|
|
23
|
+
},
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
# Global redirect endpoint — used to discover the correct regional server
|
|
27
|
+
REDIRECT_URL = "https://apis.cloudedge360.com/ppstrongs/redirect"
|
|
28
|
+
|
|
21
29
|
# Default endpoints (EU) – kept for backward compatibility
|
|
22
30
|
BASE_URL = REGION_ENDPOINTS[REGION_EU]["base_url"]
|
|
23
31
|
OPENAPI_BASE_URL = REGION_ENDPOINTS[REGION_EU]["openapi_base_url"]
|
|
@@ -35,14 +43,39 @@ EU_COUNTRY_CODES = {
|
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
|
|
46
|
+
# Asia-Pacific / Oceania / Middle East / Africa country codes
|
|
47
|
+
# These are routed through the Singapore gateway
|
|
48
|
+
AP_COUNTRY_CODES = {
|
|
49
|
+
# East Asia
|
|
50
|
+
"JP", "KR", "TW", "HK", "MO",
|
|
51
|
+
# Southeast Asia
|
|
52
|
+
"SG", "MY", "TH", "VN", "PH", "ID", "MM", "KH", "LA", "BN", "TL",
|
|
53
|
+
# South Asia
|
|
54
|
+
"IN", "PK", "BD", "LK", "NP", "BT", "MV",
|
|
55
|
+
# Oceania
|
|
56
|
+
"AU", "NZ", "FJ", "PG", "WS", "TO", "VU", "SB", "KI", "MH", "FM",
|
|
57
|
+
"PW", "NR", "TV", "CK", "NU",
|
|
58
|
+
# Middle East
|
|
59
|
+
"AE", "SA", "QA", "KW", "BH", "OM", "JO", "LB", "IQ", "IR", "YE",
|
|
60
|
+
"SY", "PS", "IL",
|
|
61
|
+
# Africa
|
|
62
|
+
"ZA", "EG", "NG", "KE", "GH", "TZ", "UG", "ET", "MA", "DZ", "TN",
|
|
63
|
+
"SN", "CI", "CM", "AO", "MZ", "ZW", "BW", "NA", "MU", "MG", "RW",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
38
67
|
def region_for_country(country_code: str) -> str:
|
|
39
68
|
"""Return the region key for a given ISO 3166-1 alpha-2 country code."""
|
|
40
|
-
|
|
69
|
+
cc = country_code.upper()
|
|
70
|
+
if cc in EU_COUNTRY_CODES:
|
|
41
71
|
return REGION_EU
|
|
72
|
+
if cc in AP_COUNTRY_CODES:
|
|
73
|
+
return REGION_AP
|
|
42
74
|
return REGION_US
|
|
43
75
|
|
|
44
76
|
# API Keys (these are public keys from the mobile app)
|
|
45
77
|
CA_KEY = "bc29be30292a4309877807e101afbd51"
|
|
78
|
+
CA_SECRET = "35a69fd1-6527-4566-b190-921f9a651488"
|
|
46
79
|
|
|
47
80
|
# Default Headers
|
|
48
81
|
DEFAULT_HEADERS = {
|
|
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
|
|
File without changes
|