pypetkitapi 1.9.3__tar.gz → 1.10.1__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.
@@ -1,8 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pypetkitapi
3
- Version: 1.9.3
3
+ Version: 1.10.1
4
4
  Summary: Python client for PetKit API
5
- Home-page: https://github.com/Jezza34000/pypetkit
6
5
  License: MIT
7
6
  Author: Jezza34000
8
7
  Author-email: info@mail.com
@@ -14,8 +13,10 @@ Classifier: Programming Language :: Python :: 3.12
14
13
  Classifier: Programming Language :: Python :: 3.13
15
14
  Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
16
15
  Requires-Dist: aiohttp (>=3.10.10,<4.0.0)
16
+ Requires-Dist: m3u8 (>=6.0)
17
17
  Requires-Dist: pycryptodome (>=3.19.1,<4.0.0)
18
18
  Requires-Dist: pydantic (>=1.10.18,<3.0.0)
19
+ Project-URL: Homepage, https://github.com/Jezza34000/pypetkit
19
20
  Description-Content-Type: text/markdown
20
21
 
21
22
  # Petkit API Client
@@ -110,7 +111,9 @@ async def main():
110
111
  # simple hopper :
111
112
  await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
112
113
  # dual hopper :
113
- await client.send_api_request(123456789, FeederCommand.MANUAL_FEED_DUAL, {"amount1": 2})
114
+ await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount1": 2})
115
+ # or
116
+ await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount2": 2})
114
117
 
115
118
  ### Example 3 : Start the cleaning process
116
119
  ### Device_ID, Command, Payload
@@ -90,7 +90,9 @@ async def main():
90
90
  # simple hopper :
91
91
  await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
92
92
  # dual hopper :
93
- await client.send_api_request(123456789, FeederCommand.MANUAL_FEED_DUAL, {"amount1": 2})
93
+ await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount1": 2})
94
+ # or
95
+ await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount2": 2})
94
96
 
95
97
  ### Example 3 : Start the cleaning process
96
98
  ### Device_ID, Command, Payload
@@ -23,24 +23,35 @@ from .const import (
23
23
  DEVICES_WATER_FOUNTAIN,
24
24
  FEEDER,
25
25
  FEEDER_MINI,
26
+ FEEDER_WITH_CAMERA,
26
27
  K2,
27
28
  K3,
29
+ LITTER_NO_CAMERA,
30
+ LITTER_WITH_CAMERA,
28
31
  T3,
29
32
  T4,
30
33
  T5,
31
34
  T6,
32
35
  W5,
36
+ MediaType,
33
37
  RecordType,
34
38
  )
35
39
  from .containers import Pet
36
- from .exceptions import PetkitAuthenticationError, PypetkitError
40
+ from .exceptions import (
41
+ PetkitAuthenticationUnregisteredEmailError,
42
+ PetkitRegionalServerNotFoundError,
43
+ PetkitSessionError,
44
+ PetkitSessionExpiredError,
45
+ PetkitTimeoutError,
46
+ PypetkitError,
47
+ )
37
48
  from .feeder_container import Feeder, RecordsItems
38
49
  from .litter_container import Litter, LitterRecord, WorkState
39
- from .medias import MediaHandler, MediasFiles
50
+ from .media import DownloadDecryptMedia, MediaCloud, MediaFile, MediaManager
40
51
  from .purifier_container import Purifier
41
52
  from .water_fountain_container import WaterFountain
42
53
 
43
- __version__ = "1.9.3"
54
+ __version__ = "1.10.1"
44
55
 
45
56
  __all__ = [
46
57
  "CTW3",
@@ -53,6 +64,9 @@ __all__ = [
53
64
  "DEVICES_LITTER_BOX",
54
65
  "DEVICES_PURIFIER",
55
66
  "DEVICES_WATER_FOUNTAIN",
67
+ "FEEDER_WITH_CAMERA",
68
+ "LITTER_WITH_CAMERA",
69
+ "LITTER_NO_CAMERA",
56
70
  "DeviceAction",
57
71
  "DeviceCommand",
58
72
  "FEEDER",
@@ -65,12 +79,19 @@ __all__ = [
65
79
  "Litter",
66
80
  "LitterCommand",
67
81
  "LitterRecord",
68
- "MediaHandler",
69
- "MediasFiles",
82
+ "MediaManager",
83
+ "DownloadDecryptMedia",
84
+ "MediaCloud",
85
+ "MediaFile",
86
+ "MediaType",
70
87
  "Pet",
71
88
  "PetCommand",
72
89
  "PetKitClient",
73
- "PetkitAuthenticationError",
90
+ "PetkitAuthenticationUnregisteredEmailError",
91
+ "PetkitRegionalServerNotFoundError",
92
+ "PetkitSessionError",
93
+ "PetkitSessionExpiredError",
94
+ "PetkitTimeoutError",
74
95
  "PurMode",
75
96
  "Purifier",
76
97
  "PypetkitError",
@@ -0,0 +1,174 @@
1
+ """Module for handling Bluetooth communication with PetKit devices."""
2
+
3
+ import asyncio
4
+ import base64
5
+ from datetime import datetime
6
+ from http import HTTPMethod
7
+ import logging
8
+ from typing import TYPE_CHECKING
9
+ import urllib.parse
10
+
11
+ from pypetkitapi.command import FOUNTAIN_COMMAND, FountainAction
12
+ from pypetkitapi.const import (
13
+ BLE_CONNECT_ATTEMPT,
14
+ BLE_END_TRAME,
15
+ BLE_START_TRAME,
16
+ PetkitEndpoint,
17
+ )
18
+ from pypetkitapi.containers import BleRelay
19
+
20
+ if TYPE_CHECKING:
21
+ from pypetkitapi import PetKitClient, WaterFountain
22
+
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ class BluetoothManager:
27
+ """Class for handling Bluetooth communication with PetKit devices."""
28
+
29
+ def __init__(self, client: "PetKitClient"):
30
+ """Initialize the BluetoothManager class."""
31
+ self.client = client
32
+
33
+ async def _get_fountain_instance(self, fountain_id: int) -> "WaterFountain":
34
+ """Get the WaterFountain instance for the given fountain_id."""
35
+ from pypetkitapi.water_fountain_container import WaterFountain
36
+
37
+ water_fountain = self.client.petkit_entities.get(fountain_id)
38
+ if not isinstance(water_fountain, WaterFountain):
39
+ _LOGGER.error("Water fountain with ID %s not found.", fountain_id)
40
+ raise TypeError(f"Water fountain with ID {fountain_id} not found.")
41
+ return water_fountain
42
+
43
+ async def check_relay_availability(self, fountain_id: int) -> bool:
44
+ """Check if BLE relay is available for the given fountain_id."""
45
+ fountain = None
46
+ for account in self.client.account_data:
47
+ if account.device_list:
48
+ fountain = next(
49
+ (
50
+ device
51
+ for device in account.device_list
52
+ if device.device_id == fountain_id
53
+ ),
54
+ None,
55
+ )
56
+ if fountain:
57
+ break
58
+ if not fountain:
59
+ raise ValueError(
60
+ f"Fountain with device_id {fountain_id} not found for the current account"
61
+ )
62
+ group_id = fountain.group_id
63
+ response = await self.client.req.request(
64
+ method=HTTPMethod.POST,
65
+ url=f"{PetkitEndpoint.BLE_AS_RELAY}",
66
+ params={"groupId": group_id},
67
+ headers=await self.client.get_session_id(),
68
+ )
69
+ ble_relays = [BleRelay(**relay) for relay in response]
70
+ if len(ble_relays) == 0:
71
+ _LOGGER.warning("No BLE relay devices found.")
72
+ return False
73
+ return True
74
+
75
+ async def open_ble_connection(self, fountain_id: int) -> bool:
76
+ """Open a BLE connection to the given fountain_id."""
77
+ _LOGGER.info("Opening BLE connection to fountain %s", fountain_id)
78
+ water_fountain = await self._get_fountain_instance(fountain_id)
79
+ if await self.check_relay_availability(fountain_id) is False:
80
+ _LOGGER.error("BLE relay not available.")
81
+ return False
82
+ if water_fountain.is_connected is True:
83
+ _LOGGER.error("BLE connection already established.")
84
+ return True
85
+ response = await self.client.req.request(
86
+ method=HTTPMethod.POST,
87
+ url=PetkitEndpoint.BLE_CONNECT,
88
+ data={"bleId": fountain_id, "type": 24, "mac": water_fountain.mac},
89
+ headers=await self.client.get_session_id(),
90
+ )
91
+ if response != {"state": 1}:
92
+ _LOGGER.error("Failed to establish BLE connection.")
93
+ water_fountain.is_connected = False
94
+ return False
95
+ for attempt in range(BLE_CONNECT_ATTEMPT):
96
+ _LOGGER.warning("BLE connection attempt n%s", attempt)
97
+ response = await self.client.req.request(
98
+ method=HTTPMethod.POST,
99
+ url=PetkitEndpoint.BLE_POLL,
100
+ data={"bleId": fountain_id, "type": 24, "mac": water_fountain.mac},
101
+ headers=await self.client.get_session_id(),
102
+ )
103
+ if response == 1:
104
+ _LOGGER.info("BLE connection established successfully.")
105
+ water_fountain.is_connected = True
106
+ water_fountain.last_ble_poll = datetime.now().strftime(
107
+ "%Y-%m-%dT%H:%M:%S.%f"
108
+ )
109
+ return True
110
+ await asyncio.sleep(4)
111
+ _LOGGER.error("Failed to establish BLE connection after multiple attempts.")
112
+ water_fountain.is_connected = False
113
+ return False
114
+
115
+ async def close_ble_connection(self, fountain_id: int) -> None:
116
+ """Close the BLE connection to the given fountain_id."""
117
+ _LOGGER.info("Closing BLE connection to fountain %s", fountain_id)
118
+ water_fountain = await self._get_fountain_instance(fountain_id)
119
+ await self.client.req.request(
120
+ method=HTTPMethod.POST,
121
+ url=PetkitEndpoint.BLE_CANCEL,
122
+ data={"bleId": fountain_id, "type": 24, "mac": water_fountain.mac},
123
+ headers=await self.client.get_session_id(),
124
+ )
125
+ _LOGGER.info("BLE connection closed successfully.")
126
+
127
+ async def get_ble_cmd_data(
128
+ self, fountain_command: list, counter: int
129
+ ) -> tuple[int, str]:
130
+ """Get the BLE command data for the given fountain_command."""
131
+ cmd_code = fountain_command[0]
132
+ modified_command = fountain_command[:2] + [counter] + fountain_command[2:]
133
+ ble_data = [*BLE_START_TRAME, *modified_command, *BLE_END_TRAME]
134
+ encoded_data = await self._encode_ble_data(ble_data)
135
+ return cmd_code, encoded_data
136
+
137
+ @staticmethod
138
+ async def _encode_ble_data(byte_list: list) -> str:
139
+ """Encode the given byte_list to a base64 encoded string."""
140
+ byte_array = bytearray(byte_list)
141
+ b64_encoded = base64.b64encode(byte_array)
142
+ return urllib.parse.quote(b64_encoded)
143
+
144
+ async def send_ble_command(self, fountain_id: int, command: FountainAction) -> bool:
145
+ """Send the given BLE command to the fountain_id."""
146
+ _LOGGER.info("Sending BLE command to fountain %s", fountain_id)
147
+ water_fountain = await self._get_fountain_instance(fountain_id)
148
+ if water_fountain.is_connected is False:
149
+ _LOGGER.error("BLE connection not established.")
150
+ return False
151
+ command_data = FOUNTAIN_COMMAND.get(command)
152
+ if command_data is None:
153
+ _LOGGER.error("Command not found.")
154
+ return False
155
+ cmd_code, cmd_data = await self.get_ble_cmd_data(
156
+ list(command_data), water_fountain.ble_counter
157
+ )
158
+ response = await self.client.req.request(
159
+ method=HTTPMethod.POST,
160
+ url=PetkitEndpoint.BLE_CONTROL_DEVICE,
161
+ data={
162
+ "bleId": water_fountain.id,
163
+ "cmd": cmd_code,
164
+ "data": cmd_data,
165
+ "mac": water_fountain.mac,
166
+ "type": 24,
167
+ },
168
+ headers=await self.client.get_session_id(),
169
+ )
170
+ if response != 1:
171
+ _LOGGER.error("Failed to send BLE command.")
172
+ return False
173
+ _LOGGER.info("BLE command sent successfully.")
174
+ return True