pycloudedge 0.1.4.dev3__tar.gz → 0.1.5.dev0__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.4.dev3 → pycloudedge-0.1.5.dev0}/PKG-INFO +4 -1
  2. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/__init__.py +4 -0
  3. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/_version.py +3 -3
  4. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/client.py +157 -2
  5. pycloudedge-0.1.5.dev0/cloudedge/mqtt.py +241 -0
  6. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/pycloudedge.egg-info/PKG-INFO +4 -1
  7. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/pycloudedge.egg-info/SOURCES.txt +1 -0
  8. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/pycloudedge.egg-info/requires.txt +4 -0
  9. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/pyproject.toml +4 -0
  10. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/.env.example +0 -0
  11. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/.gitignore +0 -0
  12. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/LICENSE +0 -0
  13. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/MANIFEST.in +0 -0
  14. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/README.md +0 -0
  15. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/cli.py +0 -0
  16. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/constants.py +0 -0
  17. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/exceptions.py +0 -0
  18. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/iot_parameters.py +0 -0
  19. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/logging_config.py +0 -0
  20. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/utils.py +0 -0
  21. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/cloudedge/validators.py +0 -0
  22. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/examples/README.md +0 -0
  23. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/examples/basic_example.py +0 -0
  24. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/examples/device_control.py +0 -0
  25. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/examples/network_ping_status.py +0 -0
  26. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/pycloudedge.egg-info/dependency_links.txt +0 -0
  27. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/pycloudedge.egg-info/entry_points.txt +0 -0
  28. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/pycloudedge.egg-info/top_level.txt +0 -0
  29. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/requirements-dev.txt +0 -0
  30. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/requirements.txt +0 -0
  31. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/setup.cfg +0 -0
  32. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/setup.py +0 -0
  33. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/tests/test_basic.py +0 -0
  34. {pycloudedge-0.1.4.dev3 → pycloudedge-0.1.5.dev0}/tests/test_improvements.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycloudedge
3
- Version: 0.1.4.dev3
3
+ Version: 0.1.5.dev0
4
4
  Summary: Python library for CloudEdge cameras
5
5
  Home-page: https://github.com/fradaloisio/pycloudedge
6
6
  Author: Francesco D'Aloisio
@@ -29,7 +29,10 @@ Description-Content-Type: text/markdown
29
29
  License-File: LICENSE
30
30
  Requires-Dist: requests>=2.25.0
31
31
  Requires-Dist: cryptography>=3.4.0
32
+ Requires-Dist: pycryptodome>=3.15.0
32
33
  Requires-Dist: python-dotenv>=0.19.0
34
+ Provides-Extra: mqtt
35
+ Requires-Dist: paho-mqtt>=2.0.0; extra == "mqtt"
33
36
  Provides-Extra: dev
34
37
  Requires-Dist: pytest>=6.0; extra == "dev"
35
38
  Requires-Dist: pytest-asyncio>=0.18.0; extra == "dev"
@@ -15,6 +15,7 @@ from .client import (
15
15
  DEVICE_STATUS_DORMANCY,
16
16
  DEVICE_STATUS_OFFLINE,
17
17
  )
18
+ from .mqtt import CloudEdgeMqttListener, ALARM_TYPE_NAMES, MOTION_ALARM_TYPES
18
19
  from .exceptions import (
19
20
  CloudEdgeError, AuthenticationError, DeviceNotFoundError,
20
21
  ConfigurationError, NetworkError, ValidationError, RateLimitError
@@ -32,6 +33,9 @@ __all__ = [
32
33
  'DEVICE_STATUS_ONLINE',
33
34
  'DEVICE_STATUS_DORMANCY',
34
35
  'DEVICE_STATUS_OFFLINE',
36
+ 'CloudEdgeMqttListener',
37
+ 'ALARM_TYPE_NAMES',
38
+ 'MOTION_ALARM_TYPES',
35
39
  'CloudEdgeError',
36
40
  'AuthenticationError',
37
41
  'DeviceNotFoundError',
@@ -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.4.dev3'
32
- __version_tuple__ = version_tuple = (0, 1, 4, 'dev3')
31
+ __version__ = version = '0.1.5.dev0'
32
+ __version_tuple__ = version_tuple = (0, 1, 5, 'dev0')
33
33
 
34
- __commit_id__ = commit_id = 'gc5b1ac2e4'
34
+ __commit_id__ = commit_id = 'ga05fef1a8'
@@ -653,9 +653,15 @@ class CloudEdgeClient:
653
653
  "caKey": ca_key,
654
654
  "loginTime": int(time.time()),
655
655
  "apiServer": self.BASE_URL,
656
- "iotPlatformKeys": iot_platform_keys
656
+ "iotPlatformKeys": iot_platform_keys,
657
657
  }
658
-
658
+
659
+ # Fetch MQTT / IoT platform config (host, port, mqtt signature)
660
+ try:
661
+ self._fetch_iot_config()
662
+ except Exception as exc:
663
+ self._log(f"IoT config fetch failed (non-fatal): {exc}")
664
+
659
665
  self._save_session_cache(self.session_data)
660
666
  return True
661
667
  else:
@@ -671,6 +677,155 @@ class CloudEdgeClient:
671
677
  except json.JSONDecodeError:
672
678
  raise AuthenticationError("Failed to parse login response")
673
679
 
680
+ @staticmethod
681
+ def _aes_cbc_decrypt(ciphertext_b64: str, key_str: str) -> str:
682
+ """AES-CBC decrypt (key == IV, PKCS7 padding).
683
+
684
+ Uses PyCryptodome when available (more lenient on padding edge cases
685
+ found in Meari responses), falls back to ``cryptography``.
686
+ """
687
+ key = key_str.encode("utf-8")
688
+ pad_len = 4 - len(ciphertext_b64) % 4 if len(ciphertext_b64) % 4 else 0
689
+ ct = base64.b64decode(ciphertext_b64 + "=" * pad_len)
690
+
691
+ try:
692
+ from Crypto.Cipher import AES
693
+ from Crypto.Util.Padding import unpad
694
+ cipher = AES.new(key, AES.MODE_CBC, key)
695
+ return unpad(cipher.decrypt(ct), AES.block_size).decode("utf-8")
696
+ except ImportError:
697
+ from cryptography.hazmat.primitives.ciphers import (
698
+ Cipher, algorithms, modes,
699
+ )
700
+ from cryptography.hazmat.primitives import padding
701
+ cipher = Cipher(algorithms.AES(key), modes.CBC(key))
702
+ decryptor = cipher.decryptor()
703
+ padded = decryptor.update(ct) + decryptor.finalize()
704
+ unpadder = padding.PKCS7(128).unpadder()
705
+ return (unpadder.update(padded) + unpadder.finalize()).decode("utf-8")
706
+
707
+ def _fetch_iot_config(self) -> None:
708
+ """Fetch MQTT and OpenAPI config from ``/v2/app/config/pf/init``.
709
+
710
+ Decrypts the platform signature to obtain the real ``access_id`` and
711
+ ``access_key`` required by the MQTT broker. Stores results in
712
+ ``self.session_data["mqtt"]``.
713
+
714
+ Called automatically after a successful :meth:`authenticate`.
715
+ """
716
+ if not self.session_data:
717
+ return
718
+
719
+ user_token = self.session_data["userToken"]
720
+ user_id = self.session_data["userID"]
721
+ ts = int(time.time() * 1000)
722
+
723
+ params = {
724
+ 'appVer': '5.5.1', 'appVerCode': '551', 'lngType': 'en',
725
+ 'phoneType': 'a', 'sdkVer': '1.0.0', 'sourceApp': '8',
726
+ 'countryCode': self.country_code,
727
+ 'phoneCode': self.phone_code,
728
+ 'iotType': '4',
729
+ 'signatureMethod': 'HMAC-SHA1', 'signatureVersion': '1.0',
730
+ 'signatureNonce': str(ts),
731
+ 't': str(ts), 'timestamp': str(ts),
732
+ 'userID': str(user_id),
733
+ }
734
+ sorted_keys = sorted(params.keys())
735
+ content = "&".join(f"{k}={params[k]}" for k in sorted_keys)
736
+ params['signature'] = base64.b64encode(
737
+ hmac.new(user_token.encode(), content.encode(), hashlib.sha1).digest()
738
+ ).decode()
739
+
740
+ xca_headers = self._generate_xca_headers(
741
+ f"api=/ppstrongs/v2/app/config/pf/init"
742
+ f"|X-Ca-Key={user_token}"
743
+ f"|X-Ca-Timestamp={str(ts)}"
744
+ f"|X-Ca-Nonce={str(ts % 100000000)}",
745
+ user_token,
746
+ )
747
+
748
+ try:
749
+ resp = self._session.get(
750
+ f"{self.BASE_URL}/v2/app/config/pf/init",
751
+ params=params,
752
+ headers={**DEFAULT_HEADERS, **xca_headers},
753
+ timeout=DEFAULT_TIMEOUT,
754
+ )
755
+ data = resp.json()
756
+ if data.get("resultCode") != "1001":
757
+ self._log(f"IoT config response: {data.get('resultCode')}")
758
+ return
759
+
760
+ pf = data.get("result", {}).get("pfApi", {})
761
+
762
+ # ── Decrypt platform signature to get real access_id/key ─────
763
+ #
764
+ # The MQTT broker authenticates with the *decrypted* access_id
765
+ # (not the raw "mearicloud" value from the login response).
766
+ # Key derivation: base64(userID + PARTNER_ID + TTID + expireTime)[:16]
767
+ PARTNER_ID = "8"
768
+ TTID = "a"
769
+
770
+ access_id = ""
771
+ access_key = ""
772
+ platform = pf.get("platform", {})
773
+ plat_sig = platform.get("signature", "")
774
+ expire_time = str(platform.get("expireTime", ""))
775
+
776
+ if plat_sig and expire_time:
777
+ try:
778
+ key_raw = f"{user_id}{PARTNER_ID}{TTID}{expire_time}"
779
+ key16 = base64.b64encode(key_raw.encode()).decode().rstrip("=")[:16]
780
+ decrypted = self._aes_cbc_decrypt(plat_sig, key16)
781
+ info_b64 = decrypted.split("-")[0]
782
+ pad = 4 - len(info_b64) % 4 if len(info_b64) % 4 else 0
783
+ info_json = base64.b64decode(info_b64 + "=" * pad).decode()
784
+ info = json.loads(info_json)
785
+ access_id = info.get("accessid", "")
786
+ access_key = info.get("accesskey", "")
787
+ self._log(f"Decrypted MQTT access_id: {access_id[:12]}...")
788
+ except Exception as exc:
789
+ self._log(f"Platform signature decryption failed: {exc}")
790
+
791
+ mqtt_cfg = pf.get("mqtt", {})
792
+ self.session_data["mqtt"] = {
793
+ "mqtt_host": mqtt_cfg.get("host", ""),
794
+ "mqtt_port": int(mqtt_cfg.get("port", 1883)),
795
+ "mqtt_signature": pf.get("mqttSignature", ""),
796
+ "mqtt_access_id": access_id,
797
+ "mqtt_access_key": access_key,
798
+ }
799
+ self._log(
800
+ f"MQTT config: {self.session_data['mqtt']['mqtt_host']}"
801
+ f":{self.session_data['mqtt']['mqtt_port']}"
802
+ )
803
+ except Exception as exc:
804
+ self._log(f"Failed to fetch IoT config: {exc}")
805
+
806
+ def get_mqtt_config(self) -> Optional[Dict[str, Any]]:
807
+ """Return MQTT connection parameters, or ``None`` if unavailable.
808
+
809
+ The returned dict contains:
810
+
811
+ * ``mqtt_host`` (str)
812
+ * ``mqtt_port`` (int)
813
+ * ``mqtt_signature`` (str) — password for the MQTT broker
814
+ * ``mqtt_access_id`` (str) — username for the MQTT broker
815
+ * ``user_id`` (int | str) — used in the topic path
816
+
817
+ Call :meth:`authenticate` first.
818
+ """
819
+ if not self.session_data:
820
+ return None
821
+ mqtt = self.session_data.get("mqtt")
822
+ if not mqtt or not mqtt.get("mqtt_host"):
823
+ return None
824
+ return {
825
+ **mqtt,
826
+ "user_id": self.session_data.get("userID"),
827
+ }
828
+
674
829
  def _generate_device_body(self, extra_params: Optional[Dict] = None) -> Dict:
675
830
  """Generate device body for API requests."""
676
831
  if not self.session_data:
@@ -0,0 +1,241 @@
1
+ """CloudEdge MQTT event listener.
2
+
3
+ Subscribes to the Meari/CloudEdge MQTT broker and dispatches push events
4
+ (motion detection, camera wake, tamper, etc.) in real-time.
5
+
6
+ Requires ``paho-mqtt`` (optional dependency)::
7
+
8
+ pip install paho-mqtt
9
+
10
+ Usage::
11
+
12
+ from cloudedge import CloudEdgeClient
13
+ from cloudedge.mqtt import CloudEdgeMqttListener
14
+
15
+ client = CloudEdgeClient(...)
16
+ client.authenticate()
17
+
18
+ def on_event(device_id, event_name, event_type, is_motion):
19
+ print(f"{device_id}: {event_name} motion={is_motion}")
20
+
21
+ listener = CloudEdgeMqttListener(client, on_event=on_event)
22
+ listener.start() # non-blocking (background thread)
23
+ ...
24
+ listener.stop()
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import logging
31
+ import ssl
32
+ from typing import Any, Callable, Dict, Optional
33
+
34
+ _LOGGER = logging.getLogger(__name__)
35
+
36
+ ALARM_TYPE_NAMES: Dict[int, str] = {
37
+ 1: "PIR",
38
+ 2: "Motion",
39
+ 3: "Visitor",
40
+ 6: "Noise",
41
+ 7: "Baby cry",
42
+ 8: "Face",
43
+ 9: "Call",
44
+ 10: "Tamper",
45
+ 11: "Human body",
46
+ 12: "Face detected",
47
+ 14: "Dog bark",
48
+ 17: "Cat",
49
+ 18: "Pet",
50
+ 19: "Package",
51
+ 20: "Person",
52
+ 21: "SD card removed",
53
+ 39: "Fire",
54
+ 41: "Cat meow",
55
+ }
56
+
57
+ MOTION_ALARM_TYPES: frozenset[int] = frozenset({1, 2, 11, 20})
58
+
59
+ OnEventCallback = Callable[[str, str, int, bool], None]
60
+ """Signature: (device_id, event_name, event_type_int, is_motion) -> None"""
61
+
62
+
63
+ class CloudEdgeMqttListener:
64
+ """Subscribe to the CloudEdge/Meari MQTT broker for push events.
65
+
66
+ Args:
67
+ client: An authenticated :class:`~cloudedge.client.CloudEdgeClient`.
68
+ on_event: Callback invoked for **every** event.
69
+ on_motion: Callback invoked only for motion-related events.
70
+ on_connect: Called when the MQTT connection is established.
71
+ on_disconnect: Called when the MQTT connection drops.
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ client: Any,
77
+ *,
78
+ on_event: Optional[OnEventCallback] = None,
79
+ on_motion: Optional[OnEventCallback] = None,
80
+ on_connect: Optional[Callable[[], None]] = None,
81
+ on_disconnect: Optional[Callable[[], None]] = None,
82
+ ) -> None:
83
+ mqtt_cfg = client.get_mqtt_config()
84
+ if not mqtt_cfg:
85
+ raise RuntimeError(
86
+ "MQTT config not available — call client.authenticate() first"
87
+ )
88
+
89
+ self._host = mqtt_cfg["mqtt_host"]
90
+ self._port = mqtt_cfg["mqtt_port"]
91
+ self._username = mqtt_cfg["mqtt_access_id"]
92
+ self._password = mqtt_cfg["mqtt_signature"]
93
+ self._user_id = str(mqtt_cfg["user_id"])
94
+ self._topic = (
95
+ f"$bsssvr/iot/{self._user_id}/{self._user_id}/event/update/accepted"
96
+ )
97
+
98
+ self.on_event = on_event
99
+ self.on_motion = on_motion
100
+ self._on_connect_cb = on_connect
101
+ self._on_disconnect_cb = on_disconnect
102
+
103
+ self._client: Any = None
104
+ self._connected = False
105
+
106
+ @property
107
+ def connected(self) -> bool:
108
+ """``True`` if the MQTT connection is active."""
109
+ return self._connected
110
+
111
+ @property
112
+ def topic(self) -> str:
113
+ """The MQTT topic being subscribed to."""
114
+ return self._topic
115
+
116
+ def start(self) -> bool:
117
+ """Start the MQTT background loop (non-blocking).
118
+
119
+ Returns ``True`` if the connection was initiated successfully.
120
+ Raises ``ImportError`` if ``paho-mqtt`` is not installed.
121
+ """
122
+ try:
123
+ import paho.mqtt.client as mqtt
124
+ except ImportError:
125
+ raise ImportError(
126
+ "paho-mqtt is required for MQTT support: pip install paho-mqtt"
127
+ )
128
+
129
+ def _on_connect(client, userdata, flags, rc, *args):
130
+ # paho-mqtt v2 passes rc as ReasonCode object, not int
131
+ rc_ok = (rc == 0) if isinstance(rc, int) else rc.is_failure is False
132
+ if rc_ok:
133
+ self._connected = True
134
+ client.subscribe(self._topic, qos=2)
135
+ _LOGGER.info("MQTT connected — topic: %s", self._topic)
136
+ if self._on_connect_cb:
137
+ self._on_connect_cb()
138
+ else:
139
+ _LOGGER.warning("MQTT connect failed: rc=%s", rc)
140
+
141
+ def _on_disconnect(client, userdata, *args):
142
+ self._connected = False
143
+ _LOGGER.debug("MQTT disconnected")
144
+ if self._on_disconnect_cb:
145
+ self._on_disconnect_cb()
146
+
147
+ def _on_message(client, userdata, msg):
148
+ try:
149
+ self._dispatch(msg.payload)
150
+ except Exception as exc:
151
+ _LOGGER.debug("MQTT message parse error: %s", exc)
152
+
153
+ try:
154
+ client = mqtt.Client(
155
+ callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
156
+ client_id=self._user_id,
157
+ clean_session=True,
158
+ protocol=mqtt.MQTTv311,
159
+ )
160
+ except (AttributeError, TypeError):
161
+ client = mqtt.Client(
162
+ client_id=self._user_id,
163
+ clean_session=True,
164
+ protocol=mqtt.MQTTv311,
165
+ )
166
+
167
+ client.username_pw_set(self._username, self._password)
168
+
169
+ ssl_ctx = ssl.create_default_context()
170
+ ssl_ctx.check_hostname = False
171
+ ssl_ctx.verify_mode = ssl.CERT_NONE
172
+ client.tls_set_context(ssl_ctx)
173
+
174
+ client.on_connect = _on_connect
175
+ client.on_disconnect = _on_disconnect
176
+ client.on_message = _on_message
177
+ client.reconnect_delay_set(min_delay=3, max_delay=60)
178
+
179
+ self._client = client
180
+
181
+ try:
182
+ client.connect_async(self._host, self._port, keepalive=300)
183
+ client.loop_start()
184
+ return True
185
+ except Exception as exc:
186
+ _LOGGER.error("MQTT connection failed: %s", exc)
187
+ self._connected = False
188
+ return False
189
+
190
+ def stop(self) -> None:
191
+ """Stop the MQTT background loop and disconnect."""
192
+ if self._client:
193
+ try:
194
+ self._client.loop_stop()
195
+ self._client.disconnect()
196
+ except Exception:
197
+ pass
198
+ self._client = None
199
+ self._connected = False
200
+
201
+ def _dispatch(self, payload: bytes) -> None:
202
+ """Parse an MQTT payload and invoke callbacks."""
203
+ try:
204
+ data: dict = json.loads(payload.decode("utf-8"))
205
+ except (json.JSONDecodeError, UnicodeDecodeError):
206
+ return
207
+
208
+ # Unwrap nested envelope: params → data → msg
209
+ for key in ("params", "data"):
210
+ if key in data and isinstance(data[key], dict):
211
+ data = data[key]
212
+ if "msg" in data and isinstance(data["msg"], dict):
213
+ data = data["msg"]
214
+
215
+ evt_raw = data.get("evt", data.get("eventType", ""))
216
+ device_id = str(data.get("deviceID", data.get("deviceId", "")))
217
+
218
+ try:
219
+ evt_int = int(evt_raw)
220
+ except (ValueError, TypeError):
221
+ evt_int = -1
222
+
223
+ evt_name = ALARM_TYPE_NAMES.get(evt_int, f"type={evt_raw}")
224
+ is_motion = evt_int in MOTION_ALARM_TYPES
225
+
226
+ _LOGGER.info(
227
+ "MQTT event: %s device=%s motion=%s",
228
+ evt_name, device_id, is_motion,
229
+ )
230
+
231
+ if self.on_event:
232
+ try:
233
+ self.on_event(device_id, evt_name, evt_int, is_motion)
234
+ except Exception as exc:
235
+ _LOGGER.debug("on_event callback error: %s", exc)
236
+
237
+ if is_motion and self.on_motion:
238
+ try:
239
+ self.on_motion(device_id, evt_name, evt_int, is_motion)
240
+ except Exception as exc:
241
+ _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.4.dev3
3
+ Version: 0.1.5.dev0
4
4
  Summary: Python library for CloudEdge cameras
5
5
  Home-page: https://github.com/fradaloisio/pycloudedge
6
6
  Author: Francesco D'Aloisio
@@ -29,7 +29,10 @@ Description-Content-Type: text/markdown
29
29
  License-File: LICENSE
30
30
  Requires-Dist: requests>=2.25.0
31
31
  Requires-Dist: cryptography>=3.4.0
32
+ Requires-Dist: pycryptodome>=3.15.0
32
33
  Requires-Dist: python-dotenv>=0.19.0
34
+ Provides-Extra: mqtt
35
+ Requires-Dist: paho-mqtt>=2.0.0; extra == "mqtt"
33
36
  Provides-Extra: dev
34
37
  Requires-Dist: pytest>=6.0; extra == "dev"
35
38
  Requires-Dist: pytest-asyncio>=0.18.0; extra == "dev"
@@ -15,6 +15,7 @@ cloudedge/constants.py
15
15
  cloudedge/exceptions.py
16
16
  cloudedge/iot_parameters.py
17
17
  cloudedge/logging_config.py
18
+ cloudedge/mqtt.py
18
19
  cloudedge/utils.py
19
20
  cloudedge/validators.py
20
21
  examples/README.md
@@ -1,5 +1,6 @@
1
1
  requests>=2.25.0
2
2
  cryptography>=3.4.0
3
+ pycryptodome>=3.15.0
3
4
  python-dotenv>=0.19.0
4
5
 
5
6
  [dev]
@@ -13,3 +14,6 @@ build>=0.7.0
13
14
 
14
15
  [examples]
15
16
  python-dotenv>=0.19.0
17
+
18
+ [mqtt]
19
+ paho-mqtt>=2.0.0
@@ -34,10 +34,14 @@ classifiers = [
34
34
  dependencies = [
35
35
  "requests>=2.25.0",
36
36
  "cryptography>=3.4.0",
37
+ "pycryptodome>=3.15.0",
37
38
  "python-dotenv>=0.19.0",
38
39
  ]
39
40
 
40
41
  [project.optional-dependencies]
42
+ mqtt = [
43
+ "paho-mqtt>=2.0.0",
44
+ ]
41
45
  dev = [
42
46
  "pytest>=6.0",
43
47
  "pytest-asyncio>=0.18.0",