pypetkitapi 1.9.3__py3-none-any.whl → 1.10.1__py3-none-any.whl

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.
@@ -0,0 +1,67 @@
1
+ """Dataclasses for Schedule."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, ClassVar
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from pypetkitapi.const import DEVICE_DATA, PetkitEndpoint
9
+ from pypetkitapi.containers import Device
10
+
11
+
12
+ class Owner(BaseModel):
13
+ """Dataclass for Owner Data."""
14
+
15
+ device_count: int | None = Field(0, alias="deviceCount")
16
+ id: str | None = None
17
+ pet_count: int | None = Field(0, alias="petCount")
18
+ user_count: int | None = Field(0, alias="userCount")
19
+
20
+
21
+ class Type(BaseModel):
22
+ """Dataclass for Type Data."""
23
+
24
+ enable: int | None = None
25
+ id: str | None = None
26
+ img: str | None = None
27
+ is_custom: int | None = Field(0, alias="isCustom")
28
+ name: str | None = None
29
+ priority: int | None = None
30
+ repeat_option: str | None = Field(alias="repeatOption")
31
+ rpt: str | None = None
32
+ schedule_appoint: str | None = Field(alias="scheduleAppoint")
33
+ with_device_type: str | None = Field(alias="withDeviceType")
34
+ with_pet: int | None = Field(0, alias="withPet")
35
+
36
+
37
+ class Schedule(BaseModel):
38
+ """Dataclass for Schedule Data."""
39
+
40
+ data_type: ClassVar[str] = DEVICE_DATA
41
+
42
+ alarm_before: int | None = Field(0, alias="alarmBefore")
43
+ created_at: datetime | None = Field(None, alias="createdAt")
44
+ device_id: str | None = Field(None, alias="deviceId")
45
+ device_type: str | None = Field(None, alias="deviceType")
46
+ id: str | None = None
47
+ name: str | None = None
48
+ owner: Owner | None = None
49
+ repeat: str | None = None
50
+ status: int | None = None
51
+ time: datetime | None = None
52
+ type: Type | None = None
53
+ user_custom_id: int | None = Field(0, alias="userCustomId")
54
+
55
+ @classmethod
56
+ def get_endpoint(cls, device_type: str) -> str:
57
+ """Get the endpoint URL for the given device type."""
58
+ return PetkitEndpoint.SCHEDULE
59
+
60
+ @classmethod
61
+ def query_param(
62
+ cls,
63
+ device: Device,
64
+ device_data: Any | None = None,
65
+ ) -> dict:
66
+ """Generate query parameters including request_date."""
67
+ return {"limit": 20}
@@ -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
@@ -0,0 +1,19 @@
1
+ pypetkitapi/__init__.py,sha256=8Y70aqpRJgFqcA64OlYIxp0f6gLGpsXWuSpi69TXXN8,2107
2
+ pypetkitapi/bluetooth.py,sha256=u_xGp701WnrroTOt_KuIVUCZ3kRQ7BJeoMR8b9RpJ54,7176
3
+ pypetkitapi/client.py,sha256=6HdTx4Bj8zwHzSzvQz1acdRzGLCX8nETsTeIv-BVc9M,26921
4
+ pypetkitapi/command.py,sha256=cMCUutZCQo9Ddvjl_FYR5UjU_CqFz1iyetMznYwjpzM,7500
5
+ pypetkitapi/const.py,sha256=US5QihmBYvlm8hIHX0PORPUnMmDW3nmLzwLWTepkkGg,4609
6
+ pypetkitapi/containers.py,sha256=F_uyDBD0a5QD4s_ArjYiKTAAg1XHYBvmV_lEnO9RQ-U,4786
7
+ pypetkitapi/exceptions.py,sha256=4BXUyYXLfZjNxdnOGJPjyE9ASIl7JmQphjws87jvHtE,1631
8
+ pypetkitapi/feeder_container.py,sha256=PhidWd5WpsZqtdKZy60PzE67YXgQfApjm8CqvMCHK3U,14743
9
+ pypetkitapi/litter_container.py,sha256=0HrBjxylkMzxDyLWoReMaLAGRBh1YIJsifGhXzB54hA,19275
10
+ pypetkitapi/media.py,sha256=31fjIznqfIRpmdObJPYNSwSa95fYmRQnqxEs9Wq5gcM,22116
11
+ pypetkitapi/purifier_container.py,sha256=ssyIxhNben5XJ4KlQTXTrtULg2ji6DqHqjzOq08d1-I,2491
12
+ pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ pypetkitapi/schedule_container.py,sha256=OjLAY6FY-g14JNJJnYMNFV5ZtdkjUzNBit1VUiiZKnQ,2053
14
+ pypetkitapi/utils.py,sha256=z7325kcJQUburnF28HSXrJMvY_gY9007K73Zwxp-4DQ,743
15
+ pypetkitapi/water_fountain_container.py,sha256=5J0b-fDZYcFLNX2El7fifv8H6JMhBCt-ttxSow1ozRQ,6787
16
+ pypetkitapi-1.10.1.dist-info/LICENSE,sha256=u5jNkZEn6YMrtN4Kr5rU3TcBJ5-eAt0qMx4JDsbsnzM,1074
17
+ pypetkitapi-1.10.1.dist-info/METADATA,sha256=kzPqa6ZgyoZaVsgYL5aroM0h12mmBzXVz0ZxqWwSKjs,6256
18
+ pypetkitapi-1.10.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
19
+ pypetkitapi-1.10.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.0.0
2
+ Generator: poetry-core 2.0.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
pypetkitapi/medias.py DELETED
@@ -1,199 +0,0 @@
1
- """Module to handle media files from PetKit devices."""
2
-
3
- from dataclasses import dataclass
4
- import logging
5
- from pathlib import Path
6
- import re
7
- from typing import Any
8
-
9
- from aiofiles import open as aio_open
10
- import aiohttp
11
- from Crypto.Cipher import AES
12
- from Crypto.Util.Padding import unpad
13
-
14
- from pypetkitapi.feeder_container import Feeder, RecordsType
15
-
16
- _LOGGER = logging.getLogger(__name__)
17
-
18
-
19
- @dataclass
20
- class MediasFiles:
21
- """Dataclass for media files.
22
- Subclass of many other device dataclasses.
23
- """
24
-
25
- filename: str
26
- record_type: str
27
- url: str
28
- aes_key: str
29
- timestamp: str | None = None
30
-
31
-
32
- async def extract_filename_from_url(url: str) -> str:
33
- """Extract the filename from the URL and format it as requested."""
34
- match = re.search(r"https?://[^/]+(/[^?]+)", url)
35
- if match:
36
- path = match.group(1)
37
- formatted_filename = path.replace("/", "_").lstrip("_").lower()
38
- return f"{formatted_filename}.jpg"
39
- raise ValueError(f"Failed to extract filename from URL: {url}")
40
-
41
-
42
- class MediaHandler:
43
- """Class to find media files from PetKit devices."""
44
-
45
- def __init__(self, file_path: Path):
46
- """Initialize the class."""
47
- self.media_download_decode = MediaDownloadDecode(file_path)
48
- self.media_files: list[MediasFiles] = []
49
-
50
- async def get_last_image(self, device: Feeder) -> list[MediasFiles]:
51
- """Process device records and extract media info."""
52
- record_types = ["eat", "feed", "move", "pet"]
53
- self.media_files = []
54
-
55
- if not isinstance(device, Feeder):
56
- _LOGGER.error("Device is not a Feeder")
57
- return []
58
-
59
- if not device.device_records:
60
- _LOGGER.error("No device records found for feeder")
61
- return []
62
-
63
- for record_type in record_types:
64
- records = getattr(device.device_records, record_type, None)
65
- if records:
66
- self.media_files.extend(
67
- await self._process_records(records, record_type)
68
- )
69
- return self.media_files
70
-
71
- async def _process_records(
72
- self, records: RecordsType, record_type: str
73
- ) -> list[MediasFiles]:
74
- """Process individual records and return media info."""
75
- media_files = []
76
-
77
- async def process_item(record_items):
78
- last_item = next(
79
- (
80
- item
81
- for item in reversed(record_items)
82
- if item.preview and item.aes_key
83
- ),
84
- None,
85
- )
86
- if last_item:
87
- filename = await extract_filename_from_url(last_item.preview)
88
- await self.media_download_decode.get_file(
89
- last_item.preview, last_item.aes_key
90
- )
91
- timestamp = (
92
- last_item.eat_start_time
93
- or last_item.completed_at
94
- or last_item.timestamp
95
- or None
96
- )
97
- media_files.append(
98
- MediasFiles(
99
- record_type=record_type,
100
- filename=filename,
101
- url=last_item.preview,
102
- aes_key=last_item.aes_key,
103
- timestamp=timestamp,
104
- )
105
- )
106
-
107
- for record in records:
108
- if hasattr(record, "items"):
109
- await process_item(record.items) # type: ignore[attr-defined]
110
-
111
- return media_files
112
-
113
-
114
- class MediaDownloadDecode:
115
- """Class to download"""
116
-
117
- def __init__(self, download_path: Path):
118
- """Initialize the class."""
119
- self.download_path = download_path
120
-
121
- async def get_file(self, url: str, aes_key: str) -> bool:
122
- """Download a file from a URL and decrypt it."""
123
- # Check if the file already exists
124
- filename = await extract_filename_from_url(url)
125
- full_file_path = Path(self.download_path) / filename
126
- if full_file_path.exists():
127
- _LOGGER.debug("File already exist : %s don't need to download it", filename)
128
- return True
129
-
130
- # Download the file
131
- async with aiohttp.ClientSession() as session, session.get(url) as response:
132
- if response.status != 200:
133
- _LOGGER.error(
134
- "Failed to download %s, status code: %s", url, response.status
135
- )
136
- return False
137
-
138
- content = await response.read()
139
-
140
- encrypted_file_path = await self._save_file(content, f"{filename}.enc")
141
- # Decrypt the image
142
- decrypted_data = await self._decrypt_image_from_file(
143
- encrypted_file_path, aes_key
144
- )
145
-
146
- if decrypted_data:
147
- _LOGGER.debug("Decrypt was successful")
148
- await self._save_file(decrypted_data, filename)
149
- return True
150
- return False
151
-
152
- async def _save_file(self, content: bytes, filename: str) -> Path:
153
- """Save content to a file asynchronously and return the file path."""
154
- file_path = Path(self.download_path) / filename
155
- try:
156
- # Ensure the directory exists
157
- file_path.parent.mkdir(parents=True, exist_ok=True)
158
-
159
- async with aio_open(file_path, "wb") as file:
160
- await file.write(content)
161
- _LOGGER.debug("Save file OK : %s", file_path)
162
- except PermissionError as e:
163
- _LOGGER.error("Save file, permission denied %s: %s", file_path, e)
164
- except FileNotFoundError as e:
165
- _LOGGER.error("Save file, file/folder not found %s: %s", file_path, e)
166
- except OSError as e:
167
- _LOGGER.error("Save file, error saving file %s: %s", file_path, e)
168
- except Exception as e: # noqa: BLE001
169
- _LOGGER.error(
170
- "Save file, unexpected error saving file %s: %s", file_path, e
171
- )
172
- return file_path
173
-
174
- @staticmethod
175
- async def _decrypt_image_from_file(file_path: Path, aes_key: str) -> bytes | None:
176
- """Decrypt an image from a file using AES encryption.
177
- :param file_path: Path to the encrypted image file.
178
- :param aes_key: AES key used for decryption.
179
- :return: Decrypted image data.
180
- """
181
- try:
182
- if aes_key.endswith("\n"):
183
- aes_key = aes_key[:-1]
184
- key_bytes: bytes = aes_key.encode("utf-8")
185
- iv: bytes = b"\x61" * 16
186
- cipher: Any = AES.new(key_bytes, AES.MODE_CBC, iv)
187
-
188
- async with aio_open(file_path, "rb") as encrypted_file:
189
- encrypted_data: bytes = await encrypted_file.read()
190
-
191
- decrypted_data: bytes = unpad(
192
- cipher.decrypt(encrypted_data), AES.block_size # type: ignore[attr-defined]
193
- )
194
- except Exception as e: # noqa: BLE001
195
- logging.error("Error decrypting image from file %s: %s", file_path, e)
196
- return None
197
- if Path(file_path).exists():
198
- Path(file_path).unlink()
199
- return decrypted_data
@@ -1,17 +0,0 @@
1
- pypetkitapi/__init__.py,sha256=dQiuXe1aOwEGmW27csDtKRVc4vhXrqvsW-0ElcbrTRY,1562
2
- pypetkitapi/client.py,sha256=oi1GhGIcvWMP5J9ueN2Y1xDX-Wm91b7LfjTVSe_plk4,30357
3
- pypetkitapi/command.py,sha256=G7AEtUcaK-lcRliNf4oUxPkvDO_GNBkJ-ZUcOo7DGHM,7697
4
- pypetkitapi/const.py,sha256=xDsF6sdxqQPy0B0Qhpe0Nn5xrkDjfo_omL4XL_oXFDE,4050
5
- pypetkitapi/containers.py,sha256=oJR22ZruMr-0IRgiucdnj_nutOH59MKvmaFTwLJNiJI,4635
6
- pypetkitapi/exceptions.py,sha256=fuTLT6Iw2_kA7eOyNJPf59vQkgfByhAnTThY4lC0Rt0,1283
7
- pypetkitapi/feeder_container.py,sha256=ZGJhgqP-gjTFB2q91XoyZQ_G1S5cAY37JoqqHbzoanU,14640
8
- pypetkitapi/litter_container.py,sha256=-z2BtdtRg8RyLJzJYY3AIACs9GGZ0C64hVhW4do6yQo,19172
9
- pypetkitapi/medias.py,sha256=ZFdiPj24crYYFwKBUqlxKhfKGrW2uXoXzDl2vWukZ-A,7036
10
- pypetkitapi/purifier_container.py,sha256=ssyIxhNben5XJ4KlQTXTrtULg2ji6DqHqjzOq08d1-I,2491
11
- pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- pypetkitapi/utils.py,sha256=z7325kcJQUburnF28HSXrJMvY_gY9007K73Zwxp-4DQ,743
13
- pypetkitapi/water_fountain_container.py,sha256=5J0b-fDZYcFLNX2El7fifv8H6JMhBCt-ttxSow1ozRQ,6787
14
- pypetkitapi-1.9.3.dist-info/LICENSE,sha256=u5jNkZEn6YMrtN4Kr5rU3TcBJ5-eAt0qMx4JDsbsnzM,1074
15
- pypetkitapi-1.9.3.dist-info/METADATA,sha256=p0SF9NMmun43lj81I0Sbc24o7JwFhuQgHoOK4tTquC8,6115
16
- pypetkitapi-1.9.3.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
17
- pypetkitapi-1.9.3.dist-info/RECORD,,