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.
Files changed (34) hide show
  1. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/PKG-INFO +1 -1
  2. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/_version.py +3 -3
  3. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/client.py +117 -42
  4. pycloudedge-0.1.4.dev2/cloudedge/constants.py +68 -0
  5. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/PKG-INFO +1 -1
  6. pycloudedge-0.1.3/cloudedge/constants.py +0 -33
  7. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/.env.example +0 -0
  8. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/.gitignore +0 -0
  9. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/LICENSE +0 -0
  10. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/MANIFEST.in +0 -0
  11. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/README.md +0 -0
  12. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/__init__.py +0 -0
  13. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/cli.py +0 -0
  14. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/exceptions.py +0 -0
  15. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/iot_parameters.py +0 -0
  16. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/logging_config.py +0 -0
  17. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/utils.py +0 -0
  18. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/cloudedge/validators.py +0 -0
  19. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/examples/README.md +0 -0
  20. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/examples/basic_example.py +0 -0
  21. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/examples/device_control.py +0 -0
  22. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/examples/network_ping_status.py +0 -0
  23. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/SOURCES.txt +0 -0
  24. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/dependency_links.txt +0 -0
  25. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/entry_points.txt +0 -0
  26. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/requires.txt +0 -0
  27. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pycloudedge.egg-info/top_level.txt +0 -0
  28. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/pyproject.toml +0 -0
  29. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/requirements-dev.txt +0 -0
  30. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/requirements.txt +0 -0
  31. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/setup.cfg +0 -0
  32. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/setup.py +0 -0
  33. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/tests/test_basic.py +0 -0
  34. {pycloudedge-0.1.3 → pycloudedge-0.1.4.dev2}/tests/test_improvements.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycloudedge
3
- Version: 0.1.3
3
+ Version: 0.1.4.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.3'
32
- __version_tuple__ = version_tuple = (0, 1, 3)
31
+ __version__ = version = '0.1.4.dev2'
32
+ __version_tuple__ = version_tuple = (0, 1, 4, 'dev2')
33
33
 
34
- __commit_id__ = commit_id = 'gdbaa3a14a'
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 CA_KEY, DEFAULT_HEADERS, DEFAULT_TIMEOUT
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': device.get('deviceTypeName', 'Unknown'),
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
- for device in devices:
933
- device_dict = {
934
- 'device_id': device.get('deviceID'),
935
- 'serial_number': device.get('snNum'),
936
- 'name': device.get('deviceName', 'Unnamed'),
937
- 'type': device.get('deviceTypeName', 'Unknown'),
938
- 'type_id': device.get('devTypeID'),
939
- 'host_key': device.get('hostKey'),
940
- 'online': device.get('onLine') == 1 # Store original API status
941
- }
942
-
943
- # Get enhanced online status
944
- device_dict['online'] = self._get_enhanced_device_status(device_dict)
945
-
946
- standardized_devices.append(device_dict)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycloudedge
3
- Version: 0.1.3
3
+ Version: 0.1.4.dev2
4
4
  Summary: Python library for CloudEdge cameras
5
5
  Home-page: https://github.com/fradaloisio/pycloudedge
6
6
  Author: Francesco D'Aloisio
@@ -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