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.
Files changed (35) hide show
  1. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/.gitignore +1 -0
  2. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/PKG-INFO +1 -1
  3. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/_version.py +2 -2
  4. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/client.py +105 -5
  5. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/constants.py +35 -2
  6. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/PKG-INFO +1 -1
  7. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/.env.example +0 -0
  8. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/LICENSE +0 -0
  9. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/MANIFEST.in +0 -0
  10. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/README.md +0 -0
  11. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/__init__.py +0 -0
  12. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/cli.py +0 -0
  13. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/exceptions.py +0 -0
  14. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/image_decrypt.py +0 -0
  15. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/iot_parameters.py +0 -0
  16. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/logging_config.py +0 -0
  17. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/mqtt.py +0 -0
  18. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/utils.py +0 -0
  19. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/cloudedge/validators.py +0 -0
  20. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/examples/README.md +0 -0
  21. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/examples/basic_example.py +0 -0
  22. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/examples/device_control.py +0 -0
  23. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/examples/network_ping_status.py +0 -0
  24. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/SOURCES.txt +0 -0
  25. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/dependency_links.txt +0 -0
  26. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/entry_points.txt +0 -0
  27. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/requires.txt +0 -0
  28. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/top_level.txt +0 -0
  29. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/pyproject.toml +0 -0
  30. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/requirements-dev.txt +0 -0
  31. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/requirements.txt +0 -0
  32. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/setup.cfg +0 -0
  33. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/setup.py +0 -0
  34. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/tests/test_basic.py +0 -0
  35. {pycloudedge-0.1.6.dev1 → pycloudedge-0.1.6.dev2}/tests/test_improvements.py +0 -0
@@ -94,6 +94,7 @@ venv/
94
94
  ENV/
95
95
  env.bak/
96
96
  venv.bak/
97
+ .env_*
97
98
 
98
99
  # Spyder project settings
99
100
  .spyderproject
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycloudedge
3
- Version: 0.1.6.dev1
3
+ Version: 0.1.6.dev2
4
4
  Summary: Python library for CloudEdge cameras
5
5
  Home-page: https://github.com/fradaloisio/pycloudedge
6
6
  Author: Francesco D'Aloisio
@@ -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.dev1'
32
- __version_tuple__ = version_tuple = (0, 1, 6, 'dev1')
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 "us").
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 use the EU endpoints, everything else uses US.
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
- if country_code.upper() in EU_COUNTRY_CODES:
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 = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycloudedge
3
- Version: 0.1.6.dev1
3
+ Version: 0.1.6.dev2
4
4
  Summary: Python library for CloudEdge cameras
5
5
  Home-page: https://github.com/fradaloisio/pycloudedge
6
6
  Author: Francesco D'Aloisio