pycloudedge 0.1.6.dev0__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.dev0 → pycloudedge-0.1.6.dev2}/.gitignore +1 -0
  2. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/PKG-INFO +1 -1
  3. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/__init__.py +5 -1
  4. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/_version.py +3 -3
  5. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/client.py +105 -5
  6. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/constants.py +35 -2
  7. pycloudedge-0.1.6.dev2/cloudedge/image_decrypt.py +96 -0
  8. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/mqtt.py +53 -8
  9. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/PKG-INFO +1 -1
  10. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/SOURCES.txt +1 -0
  11. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/.env.example +0 -0
  12. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/LICENSE +0 -0
  13. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/MANIFEST.in +0 -0
  14. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/README.md +0 -0
  15. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/cli.py +0 -0
  16. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/exceptions.py +0 -0
  17. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/iot_parameters.py +0 -0
  18. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/logging_config.py +0 -0
  19. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/utils.py +0 -0
  20. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/cloudedge/validators.py +0 -0
  21. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/examples/README.md +0 -0
  22. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/examples/basic_example.py +0 -0
  23. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/examples/device_control.py +0 -0
  24. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/examples/network_ping_status.py +0 -0
  25. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/dependency_links.txt +0 -0
  26. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/entry_points.txt +0 -0
  27. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/requires.txt +0 -0
  28. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/pycloudedge.egg-info/top_level.txt +0 -0
  29. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/pyproject.toml +0 -0
  30. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/requirements-dev.txt +0 -0
  31. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/requirements.txt +0 -0
  32. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/setup.cfg +0 -0
  33. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/setup.py +0 -0
  34. {pycloudedge-0.1.6.dev0 → pycloudedge-0.1.6.dev2}/tests/test_basic.py +0 -0
  35. {pycloudedge-0.1.6.dev0 → 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.dev0
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
@@ -16,6 +16,7 @@ from .client import (
16
16
  DEVICE_STATUS_OFFLINE,
17
17
  )
18
18
  from .mqtt import CloudEdgeMqttListener, ALARM_TYPE_NAMES, MOTION_ALARM_TYPES
19
+ from .image_decrypt import decrypt_jpgx3, decrypt_jpgx3_from_url, verify_licence_for_url
19
20
  from .exceptions import (
20
21
  CloudEdgeError, AuthenticationError, DeviceNotFoundError,
21
22
  ConfigurationError, NetworkError, ValidationError, RateLimitError
@@ -45,5 +46,8 @@ __all__ = [
45
46
  'RateLimitError',
46
47
  'IOT_PARAMETERS',
47
48
  'get_parameter_name',
48
- 'format_parameter_value'
49
+ 'format_parameter_value',
50
+ 'decrypt_jpgx3',
51
+ 'decrypt_jpgx3_from_url',
52
+ 'verify_licence_for_url',
49
53
  ]
@@ -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.dev0'
32
- __version_tuple__ = version_tuple = (0, 1, 6, 'dev0')
31
+ __version__ = version = '0.1.6.dev2'
32
+ __version_tuple__ = version_tuple = (0, 1, 6, 'dev2')
33
33
 
34
- __commit_id__ = commit_id = 'g782ac8af8'
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 = {
@@ -0,0 +1,96 @@
1
+ """Decrypt Meari/CloudEdge ``.jpgx3`` alarm images.
2
+
3
+ The CloudEdge MQTT push events include a URL to an encrypted JPEG snapshot
4
+ captured at the moment of the alarm. The file uses a simple XOR cipher
5
+ derived from the device's serial number (licence ID).
6
+
7
+ Algorithm (reverse-engineered from ``libmrplayer.so``):
8
+
9
+ 1. Build the seed string ``"{licence_id}|{len(licence_id)}|meari.stream"``.
10
+ 2. Compute ``MD5(seed)`` and represent it as a **32-char lowercase hex string**.
11
+ 3. XOR the first ``min(len(data), 1024)`` bytes of the encrypted file with
12
+ the hex string, cycling every 32 bytes. Bytes beyond 1024 are unchanged.
13
+
14
+ The companion ``meari.key`` variant (same pattern but with ``"meari.key"``
15
+ instead of ``"meari.stream"``) produces the hash embedded in the filename,
16
+ which can be used to verify that the correct licence ID was used.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import logging
23
+ from typing import Optional
24
+
25
+ _LOGGER = logging.getLogger(__name__)
26
+
27
+ _XOR_LIMIT = 1024
28
+
29
+
30
+ def _meari_md5(licence_id: str, suffix: str) -> str:
31
+ """Return ``MD5("{licence_id}|{len}|{suffix}")`` as a 32-char hex string."""
32
+ seed = f"{licence_id}|{len(licence_id)}|{suffix}"
33
+ return hashlib.md5(seed.encode("utf-8")).hexdigest()
34
+
35
+
36
+ def verify_licence_for_url(url: str, licence_id: str) -> bool:
37
+ """Check whether *licence_id* matches the hash embedded in *url*."""
38
+ expected = _meari_md5(licence_id, "meari.key")
39
+ return expected in url
40
+
41
+
42
+ def decrypt_jpgx3(data: bytes, licence_id: str) -> bytes:
43
+ """Decrypt a ``.jpgx3`` encrypted JPEG image.
44
+
45
+ Args:
46
+ data: Raw (encrypted) bytes downloaded from the alarm URL.
47
+ licence_id: The device serial number / licence ID
48
+ (e.g. ``"ppsl6b9baee1884c4a9b"``).
49
+
50
+ Returns:
51
+ Decrypted JPEG bytes.
52
+ """
53
+ key_hex = _meari_md5(licence_id, "meari.stream").encode("ascii") # 32 bytes
54
+ xor_len = min(len(data), _XOR_LIMIT)
55
+
56
+ out = bytearray(data)
57
+ for i in range(xor_len):
58
+ out[i] ^= key_hex[i % 32]
59
+
60
+ return bytes(out)
61
+
62
+
63
+ def decrypt_jpgx3_from_url(
64
+ url: str,
65
+ licence_id: str,
66
+ *,
67
+ timeout: int = 15,
68
+ session=None,
69
+ ) -> Optional[bytes]:
70
+ """Download and decrypt a ``.jpgx3`` alarm image.
71
+
72
+ Args:
73
+ url: The full ``.jpgx3`` URL from the MQTT alarm payload.
74
+ licence_id: Device serial number / licence ID.
75
+ timeout: HTTP request timeout in seconds.
76
+ session: Optional ``requests.Session`` to reuse.
77
+
78
+ Returns:
79
+ Decrypted JPEG bytes, or ``None`` on failure.
80
+ """
81
+ import requests
82
+
83
+ try:
84
+ r = (session or requests).get(url, timeout=timeout)
85
+ r.raise_for_status()
86
+ if len(r.content) < 100:
87
+ _LOGGER.debug("Alarm image too small (%d bytes)", len(r.content))
88
+ return None
89
+ except Exception as exc:
90
+ _LOGGER.debug("Failed to download alarm image: %s", exc)
91
+ return None
92
+
93
+ if ".jpgx3" in url or ".jpgx2" in url:
94
+ return decrypt_jpgx3(r.content, licence_id)
95
+
96
+ return r.content
@@ -59,6 +59,13 @@ MOTION_ALARM_TYPES: frozenset[int] = frozenset({1, 2, 11, 20})
59
59
  OnEventCallback = Callable[[str, str, int, bool], None]
60
60
  """Signature: (device_id, event_name, event_type_int, is_motion) -> None"""
61
61
 
62
+ OnEventCallbackEx = Callable[[str, str, int, bool, dict], None]
63
+ """Extended signature: (device_id, event_name, event_type_int, is_motion, extra) -> None
64
+
65
+ ``extra`` contains optional fields from the payload such as
66
+ ``url``, ``alert``, ``deviceName``, ``licenseID``, ``msgDate``.
67
+ """
68
+
62
69
 
63
70
  class CloudEdgeMqttListener:
64
71
  """Subscribe to the CloudEdge/Meari MQTT broker for push events.
@@ -205,15 +212,37 @@ class CloudEdgeMqttListener:
205
212
  except (json.JSONDecodeError, UnicodeDecodeError):
206
213
  return
207
214
 
208
- # Unwrap nested envelope: params data → msg
209
- for key in ("params", "data"):
215
+ _LOGGER.info("MQTT raw payload: %s", json.dumps(data, default=str)[:2000])
216
+
217
+ original = data
218
+
219
+ # Unwrap nested envelope — walk into nested dicts looking for the event
220
+ # Real payload: { "event": "alarm", "params": { "result": { "evt": "1", "deviceID": "...", ... } } }
221
+ for key in ("params", "result", "data", "state", "reported"):
210
222
  if key in data and isinstance(data[key], dict):
211
223
  data = data[key]
212
224
  if "msg" in data and isinstance(data["msg"], dict):
213
225
  data = data["msg"]
214
226
 
215
- evt_raw = data.get("evt", data.get("eventType", ""))
216
- device_id = str(data.get("deviceID", data.get("deviceId", "")))
227
+ for key in ("alarm", "alarmInfo"):
228
+ if key in data and isinstance(data[key], dict):
229
+ data = data[key]
230
+
231
+ evt_raw = (
232
+ data.get("evt")
233
+ or data.get("eventType")
234
+ or data.get("alarmType")
235
+ or data.get("type")
236
+ or ""
237
+ )
238
+ device_id = str(
239
+ data.get("deviceID")
240
+ or data.get("deviceId")
241
+ or data.get("devID")
242
+ or data.get("did")
243
+ or original.get("state", {}).get("reported", {}).get("deviceID", "")
244
+ or ""
245
+ )
217
246
 
218
247
  try:
219
248
  evt_int = int(evt_raw)
@@ -223,19 +252,35 @@ class CloudEdgeMqttListener:
223
252
  evt_name = ALARM_TYPE_NAMES.get(evt_int, f"type={evt_raw}")
224
253
  is_motion = evt_int in MOTION_ALARM_TYPES
225
254
 
255
+ extra: dict = {}
256
+ for field in ("url", "alert", "deviceName", "licenseID", "msgDate", "title"):
257
+ val = data.get(field)
258
+ if val:
259
+ extra[field] = str(val)
260
+
226
261
  _LOGGER.info(
227
- "MQTT event: %s device=%s motion=%s",
228
- evt_name, device_id, is_motion,
262
+ "MQTT event: %s device=%s motion=%s url=%s",
263
+ evt_name, device_id, is_motion, bool(extra.get("url")),
229
264
  )
230
265
 
231
266
  if self.on_event:
232
267
  try:
233
- self.on_event(device_id, evt_name, evt_int, is_motion)
268
+ self.on_event(device_id, evt_name, evt_int, is_motion, extra)
269
+ except TypeError:
270
+ try:
271
+ self.on_event(device_id, evt_name, evt_int, is_motion)
272
+ except Exception as exc:
273
+ _LOGGER.debug("on_event callback error: %s", exc)
234
274
  except Exception as exc:
235
275
  _LOGGER.debug("on_event callback error: %s", exc)
236
276
 
237
277
  if is_motion and self.on_motion:
238
278
  try:
239
- self.on_motion(device_id, evt_name, evt_int, is_motion)
279
+ self.on_motion(device_id, evt_name, evt_int, is_motion, extra)
280
+ except TypeError:
281
+ try:
282
+ self.on_motion(device_id, evt_name, evt_int, is_motion)
283
+ except Exception as exc:
284
+ _LOGGER.debug("on_motion callback error: %s", exc)
240
285
  except Exception as exc:
241
286
  _LOGGER.debug("on_motion callback error: %s", exc)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycloudedge
3
- Version: 0.1.6.dev0
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
@@ -13,6 +13,7 @@ cloudedge/cli.py
13
13
  cloudedge/client.py
14
14
  cloudedge/constants.py
15
15
  cloudedge/exceptions.py
16
+ cloudedge/image_decrypt.py
16
17
  cloudedge/iot_parameters.py
17
18
  cloudedge/logging_config.py
18
19
  cloudedge/mqtt.py