pypetkitapi 1.7.4__py3-none-any.whl → 1.7.5__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.
- pypetkitapi/client.py +6 -5
- pypetkitapi/const.py +2 -1
- pypetkitapi/containers.py +1 -1
- pypetkitapi/feeder_container.py +18 -3
- pypetkitapi/litter_container.py +3 -0
- pypetkitapi/medias.py +33 -36
- {pypetkitapi-1.7.4.dist-info → pypetkitapi-1.7.5.dist-info}/METADATA +2 -2
- pypetkitapi-1.7.5.dist-info/RECORD +15 -0
- pypetkitapi-1.7.4.dist-info/RECORD +0 -15
- {pypetkitapi-1.7.4.dist-info → pypetkitapi-1.7.5.dist-info}/LICENSE +0 -0
- {pypetkitapi-1.7.4.dist-info → pypetkitapi-1.7.5.dist-info}/WHEEL +0 -0
pypetkitapi/client.py
CHANGED
@@ -206,6 +206,7 @@ class PetKitClient:
|
|
206
206
|
if account.device_list:
|
207
207
|
_LOGGER.debug("Devices in account: %s", account.device_list)
|
208
208
|
device_list.extend(account.device_list)
|
209
|
+
_LOGGER.debug("Found %s devices", len(account.device_list))
|
209
210
|
|
210
211
|
for device in device_list:
|
211
212
|
device_type = device.device_type.lower()
|
@@ -259,6 +260,8 @@ class PetKitClient:
|
|
259
260
|
| FeederRecord
|
260
261
|
| LitterRecord
|
261
262
|
| WaterFountainRecord
|
263
|
+
| PetOutGraph
|
264
|
+
| LitterStats
|
262
265
|
],
|
263
266
|
) -> None:
|
264
267
|
"""Fetch the device data from the PetKit servers."""
|
@@ -345,16 +348,14 @@ class PetKitClient:
|
|
345
348
|
return 0
|
346
349
|
return end - start
|
347
350
|
|
348
|
-
async def populate_pet_stats(self, stats_data: Litter
|
351
|
+
async def populate_pet_stats(self, stats_data: Litter) -> None:
|
349
352
|
"""Collect data from litter data to populate pet stats."""
|
350
|
-
if stats_data is None:
|
351
|
-
return
|
352
353
|
|
353
354
|
pets_list = await self.get_pets_list()
|
354
355
|
for pet in pets_list:
|
355
|
-
if stats_data.device_records:
|
356
|
+
if stats_data.device_type == T4 and stats_data.device_records:
|
356
357
|
await self._process_t4(pet, stats_data.device_records)
|
357
|
-
elif stats_data.device_pet_graph_out:
|
358
|
+
elif stats_data.device_type == T6 and stats_data.device_pet_graph_out:
|
358
359
|
await self._process_t6(pet, stats_data.device_pet_graph_out)
|
359
360
|
|
360
361
|
async def _process_t4(self, pet, device_records):
|
pypetkitapi/const.py
CHANGED
@@ -31,7 +31,7 @@ CTW3 = "ctw3"
|
|
31
31
|
K2 = "k2"
|
32
32
|
|
33
33
|
DEVICES_LITTER_BOX = [T3, T4, T5, T6]
|
34
|
-
DEVICES_FEEDER = [FEEDER, FEEDER_MINI, D4, D4S, D4H, D4SH]
|
34
|
+
DEVICES_FEEDER = [FEEDER, FEEDER_MINI, D3, D4, D4S, D4H, D4SH]
|
35
35
|
DEVICES_WATER_FOUNTAIN = [W5, CTW3]
|
36
36
|
ALL_DEVICES = [*DEVICES_LITTER_BOX, *DEVICES_FEEDER, *DEVICES_WATER_FOUNTAIN]
|
37
37
|
|
@@ -131,3 +131,4 @@ class PetkitEndpoint(StrEnum):
|
|
131
131
|
MANUAL_FEED_MINI = "feedermini/save_dailyfeed"
|
132
132
|
MANUAL_FEED_FRESH_ELEMENT = "feeder/save_dailyfeed"
|
133
133
|
MANUAL_FEED_DUAL = "saveDailyFeed"
|
134
|
+
DAILY_FEED_AND_EAT = "dailyFeedAndEat"
|
pypetkitapi/containers.py
CHANGED
@@ -70,7 +70,7 @@ class Pet(BaseModel):
|
|
70
70
|
pet_id: int = Field(alias="petId")
|
71
71
|
pet_name: str | None = Field(None, alias="petName")
|
72
72
|
id: int | None = None # Fictive field (for HA compatibility) copied from id
|
73
|
-
sn:
|
73
|
+
sn: str # Fictive field (for HA compatibility) copied from id
|
74
74
|
name: str | None = None # Fictive field (for HA compatibility) copied from pet_name
|
75
75
|
device_type: str = "pet" # Fictive field (for HA compatibility) fixed
|
76
76
|
firmware: str | None = None # Fictive field (for HA compatibility) fixed
|
pypetkitapi/feeder_container.py
CHANGED
@@ -5,7 +5,7 @@ from typing import Any, ClassVar
|
|
5
5
|
|
6
6
|
from pydantic import BaseModel, Field
|
7
7
|
|
8
|
-
from pypetkitapi.const import DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
|
8
|
+
from pypetkitapi.const import D3, DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
|
9
9
|
from pypetkitapi.containers import CloudProduct, Device, FirmwareDetail, Wifi
|
10
10
|
|
11
11
|
|
@@ -89,6 +89,7 @@ class SettingsFeeder(BaseModel):
|
|
89
89
|
selected_sound: int | None = Field(None, alias="selectedSound")
|
90
90
|
smart_frame: int | None = Field(None, alias="smartFrame")
|
91
91
|
sound_enable: int | None = Field(None, alias="soundEnable")
|
92
|
+
surplus: int | None = None # D3
|
92
93
|
surplus_control: int | None = Field(None, alias="surplusControl")
|
93
94
|
surplus_standard: int | None = Field(None, alias="surplusStandard")
|
94
95
|
system_sound_enable: int | None = Field(None, alias="systemSoundEnable")
|
@@ -113,7 +114,7 @@ class FeedState(BaseModel):
|
|
113
114
|
eat_avg: int | None = Field(None, alias="eatAvg")
|
114
115
|
eat_count: int | None = Field(None, alias="eatCount")
|
115
116
|
eat_times: list[int] | None = Field(None, alias="eatTimes")
|
116
|
-
feed_times: dict | None = Field(None, alias="feedTimes")
|
117
|
+
feed_times: dict | list | None = Field(None, alias="feedTimes")
|
117
118
|
times: int | None = None
|
118
119
|
add_amount_total: int | None = Field(None, alias="addAmountTotal")
|
119
120
|
plan_amount_total: int | None = Field(None, alias="planAmountTotal")
|
@@ -135,17 +136,24 @@ class StateFeeder(BaseModel):
|
|
135
136
|
battery_power: int | None = Field(None, alias="batteryPower")
|
136
137
|
battery_status: int | None = Field(None, alias="batteryStatus")
|
137
138
|
bowl: int | None = None
|
139
|
+
block: int | None = None
|
140
|
+
broadcast: dict | None = None
|
138
141
|
camera_status: int | None = Field(None, alias="cameraStatus")
|
142
|
+
charge: int | None = None
|
139
143
|
desiccant_left_days: int | None = Field(None, alias="desiccantLeftDays")
|
140
144
|
desiccant_time: int | None = Field(None, alias="desiccantTime")
|
141
145
|
door: int | None = None
|
142
146
|
feed_state: FeedState | None = Field(None, alias="feedState")
|
143
147
|
feeding: int | None = None
|
148
|
+
error_code: str | None = Field(None, alias="errorCode")
|
149
|
+
error_detail: str | None = Field(None, alias="errorDetail")
|
150
|
+
error_level: int | None = Field(None, alias="errorLevel")
|
144
151
|
error_msg: str | None = Field(None, alias="errorMsg")
|
145
152
|
ota: int | None = None
|
146
153
|
overall: int | None = None
|
147
154
|
pim: int | None = None
|
148
155
|
runtime: int | None = None
|
156
|
+
weight: int | None = None
|
149
157
|
wifi: Wifi | None = None
|
150
158
|
eating: int | None = None
|
151
159
|
food: int | None = None
|
@@ -198,6 +206,7 @@ class RecordsItems(BaseModel):
|
|
198
206
|
eat_end_time: int | None = Field(None, alias="eatEndTime")
|
199
207
|
eat_start_time: int | None = Field(None, alias="eatStartTime")
|
200
208
|
eat_video: int | None = Field(None, alias="eatVideo")
|
209
|
+
eat_weight: int | None = Field(None, alias="eatWeight") # D3
|
201
210
|
empty: int | None = None
|
202
211
|
end_time: int | None = Field(None, alias="endTime")
|
203
212
|
enum_event_type: str | None = Field(None, alias="enumEventType")
|
@@ -210,10 +219,12 @@ class RecordsItems(BaseModel):
|
|
210
219
|
id: str | None = None
|
211
220
|
is_executed: int | None = Field(None, alias="isExecuted")
|
212
221
|
is_need_upload_video: int | None = Field(None, alias="isNeedUploadVideo")
|
222
|
+
left_weight: int | None = Field(None, alias="leftWeight") # D3
|
213
223
|
mark: int | None = None
|
214
224
|
media_api: str | None = Field(None, alias="mediaApi")
|
215
225
|
media_list: list[Any] | None = Field(None, alias="mediaList")
|
216
226
|
name: str | None = None
|
227
|
+
pet_id: str | None = Field(None, alias="petId")
|
217
228
|
preview: str | None = None
|
218
229
|
preview1: str | None = Field(None, alias="preview1")
|
219
230
|
preview2: str | None = Field(None, alias="preview2")
|
@@ -235,11 +246,13 @@ class RecordsType(BaseModel):
|
|
235
246
|
day: int | None = None
|
236
247
|
device_id: int | None = Field(None, alias="deviceId")
|
237
248
|
eat_count: int | None = Field(None, alias="eatCount")
|
249
|
+
eat_amount: int | None = Field(None, alias="eatAmount") # D3
|
238
250
|
items: list[RecordsItems] | None = None
|
239
251
|
plan_amount: int | None = Field(None, alias="planAmount")
|
240
252
|
real_amount: int | None = Field(None, alias="realAmount")
|
241
253
|
amount: int | None = None
|
242
254
|
times: int | None = None
|
255
|
+
user_id: str | None = Field(None, alias="userId") # D3
|
243
256
|
|
244
257
|
|
245
258
|
class FeederRecord(BaseModel):
|
@@ -254,8 +267,10 @@ class FeederRecord(BaseModel):
|
|
254
267
|
device_type: str | None = Field(None, alias="deviceType")
|
255
268
|
|
256
269
|
@classmethod
|
257
|
-
def get_endpoint(cls, device_type: str) -> str:
|
270
|
+
def get_endpoint(cls, device_type: str) -> str | None:
|
258
271
|
"""Get the endpoint URL for the given device type."""
|
272
|
+
if device_type == D3:
|
273
|
+
return PetkitEndpoint.DAILY_FEED_AND_EAT
|
259
274
|
return PetkitEndpoint.GET_DEVICE_RECORD
|
260
275
|
|
261
276
|
@classmethod
|
pypetkitapi/litter_container.py
CHANGED
@@ -109,6 +109,9 @@ class StateLitter(BaseModel):
|
|
109
109
|
box_full: bool | None = Field(None, alias="boxFull")
|
110
110
|
box_state: int | None = Field(None, alias="boxState")
|
111
111
|
deodorant_left_days: int | None = Field(None, alias="deodorantLeftDays")
|
112
|
+
error_code: str | None = Field(None, alias="errorCode")
|
113
|
+
error_detail: str | None = Field(None, alias="errorDetail")
|
114
|
+
error_level: int | None = Field(None, alias="errorLevel")
|
112
115
|
error_msg: str | None = Field(None, alias="errorMsg")
|
113
116
|
frequent_restroom: int | None = Field(None, alias="frequentRestroom")
|
114
117
|
liquid_empty: bool | None = Field(None, alias="liquidEmpty")
|
pypetkitapi/medias.py
CHANGED
@@ -41,28 +41,30 @@ async def extract_filename_from_url(url: str) -> str:
|
|
41
41
|
class MediaHandler:
|
42
42
|
"""Class to find media files from PetKit devices."""
|
43
43
|
|
44
|
-
def __init__(self,
|
44
|
+
def __init__(self, file_path: Path):
|
45
45
|
"""Initialize the class."""
|
46
|
-
self.device = device
|
47
46
|
self.media_download_decode = MediaDownloadDecode(file_path)
|
48
47
|
self.media_files: list[MediasFiles] = []
|
49
48
|
|
50
|
-
async def get_last_image(self) -> list[MediasFiles]:
|
49
|
+
async def get_last_image(self, device: Feeder) -> list[MediasFiles]:
|
51
50
|
"""Process device records and extract media info."""
|
52
51
|
record_types = ["eat", "feed", "move", "pet"]
|
53
52
|
self.media_files = []
|
54
53
|
|
55
|
-
if not
|
54
|
+
if not isinstance(device, Feeder):
|
55
|
+
_LOGGER.error("Device is not a Feeder")
|
56
|
+
return []
|
57
|
+
|
58
|
+
if not device.device_records:
|
56
59
|
_LOGGER.error("No device records found for feeder")
|
57
60
|
return []
|
58
61
|
|
59
62
|
for record_type in record_types:
|
60
|
-
records = getattr(
|
63
|
+
records = getattr(device.device_records, record_type, None)
|
61
64
|
if records:
|
62
65
|
self.media_files.extend(
|
63
66
|
await self._process_records(records, record_type)
|
64
67
|
)
|
65
|
-
|
66
68
|
return self.media_files
|
67
69
|
|
68
70
|
async def _process_records(
|
@@ -111,45 +113,39 @@ class MediaHandler:
|
|
111
113
|
class MediaDownloadDecode:
|
112
114
|
"""Class to download"""
|
113
115
|
|
114
|
-
def __init__(self, download_path:
|
116
|
+
def __init__(self, download_path: Path):
|
115
117
|
"""Initialize the class."""
|
116
118
|
self.download_path = download_path
|
117
119
|
|
118
120
|
async def get_file(self, url: str, aes_key: str) -> bool:
|
119
121
|
"""Download a file from a URL and decrypt it."""
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
122
|
+
# Check if the file already exists
|
123
|
+
filename = await extract_filename_from_url(url)
|
124
|
+
full_file_path = Path(self.download_path) / filename
|
125
|
+
if full_file_path.exists():
|
126
|
+
_LOGGER.debug("File already exist : %s don't need to download it", filename)
|
127
|
+
return True
|
128
|
+
|
129
|
+
# Download the file
|
130
|
+
async with aiohttp.ClientSession() as session, session.get(url) as response:
|
131
|
+
if response.status != 200:
|
132
|
+
_LOGGER.error(
|
133
|
+
"Failed to download %s, status code: %s", url, response.status
|
127
134
|
)
|
128
|
-
return
|
135
|
+
return False
|
129
136
|
|
130
|
-
|
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
|
+
content = await response.read()
|
137
138
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
)
|
139
|
+
encrypted_file_path = await self._save_file(content, f"{filename}.enc")
|
140
|
+
# Decrypt the image
|
141
|
+
decrypted_data = await self._decrypt_image_from_file(
|
142
|
+
encrypted_file_path, aes_key
|
143
|
+
)
|
144
144
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
return True
|
150
|
-
_LOGGER.error("Failed to decrypt %s", encrypted_file_path)
|
151
|
-
except Exception as e: # noqa: BLE001
|
152
|
-
_LOGGER.error("Error get media file from %s: %s", url, e)
|
145
|
+
if decrypted_data:
|
146
|
+
_LOGGER.debug("Decrypt was successful")
|
147
|
+
await self._save_file(decrypted_data, filename)
|
148
|
+
return True
|
153
149
|
return False
|
154
150
|
|
155
151
|
async def _save_file(self, content: bytes, filename: str) -> Path:
|
@@ -198,4 +194,5 @@ class MediaDownloadDecode:
|
|
198
194
|
except Exception as e: # noqa: BLE001
|
199
195
|
logging.error("Error decrypting image from file %s: %s", file_path, e)
|
200
196
|
return None
|
197
|
+
Path(file_path).unlink()
|
201
198
|
return decrypted_data
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pypetkitapi
|
3
|
-
Version: 1.7.
|
3
|
+
Version: 1.7.5
|
4
4
|
Summary: Python client for PetKit API
|
5
5
|
Home-page: https://github.com/Jezza34000/pypetkit
|
6
6
|
License: MIT
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
|
16
16
|
Requires-Dist: aiohttp (>=3.10.10,<4.0.0)
|
17
17
|
Requires-Dist: pycryptodome (>=3.19.1,<4.0.0)
|
18
|
-
Requires-Dist: pydantic (>=1.10.
|
18
|
+
Requires-Dist: pydantic (>=1.10.18,<2.0.0)
|
19
19
|
Description-Content-Type: text/markdown
|
20
20
|
|
21
21
|
# Petkit API Client
|
@@ -0,0 +1,15 @@
|
|
1
|
+
pypetkitapi/__init__.py,sha256=eVpyGMD3tkYtiHUkdKEeNSZhQlZ4woI2Y5oVoV7CwXM,61
|
2
|
+
pypetkitapi/client.py,sha256=aBfwh6O-egpaX7vhW1I2l3flnXL_XrwGraH1uOzmH-w,19888
|
3
|
+
pypetkitapi/command.py,sha256=gw3_J_oZHuuGLk66P8uRSqSrySjYa8ArpKaPHi2ybCw,7155
|
4
|
+
pypetkitapi/const.py,sha256=ZpFWBgzb3nvy0Z4oyM550cCunlIceWgXqqtwtJc3mFo,3479
|
5
|
+
pypetkitapi/containers.py,sha256=nhp50QwyoQRveTnEgWL7JFEY3Tl5m5wp9EqZvklW87Y,4338
|
6
|
+
pypetkitapi/exceptions.py,sha256=NWmpsI2ewC4HaIeu_uFwCeuPIHIJxZBzjoCP7aNwvhs,1139
|
7
|
+
pypetkitapi/feeder_container.py,sha256=y1A5WhObXCdbcWwtKgSPzTWokYchoUDlholAD-AMgGQ,14069
|
8
|
+
pypetkitapi/litter_container.py,sha256=wsM-v7ibx17lsD-o17RGz2wvcHCUu1iZ2RqAh3CN1Qc,18254
|
9
|
+
pypetkitapi/medias.py,sha256=8hrMdzFR9d0L0PQOVYdy-MReSF9GMp8Ft0IGGHtL1Ag,6904
|
10
|
+
pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
pypetkitapi/water_fountain_container.py,sha256=3F4GP5pXJqq6kxLMSK__GgFMuZ-rz1VDIIhaVd19Kl8,6781
|
12
|
+
pypetkitapi-1.7.5.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
|
13
|
+
pypetkitapi-1.7.5.dist-info/METADATA,sha256=bvYlZOdBLw9_nu53rnNrPSXvgYdSvQMjqAVeSFMm7VE,4852
|
14
|
+
pypetkitapi-1.7.5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
15
|
+
pypetkitapi-1.7.5.dist-info/RECORD,,
|
@@ -1,15 +0,0 @@
|
|
1
|
-
pypetkitapi/__init__.py,sha256=eVpyGMD3tkYtiHUkdKEeNSZhQlZ4woI2Y5oVoV7CwXM,61
|
2
|
-
pypetkitapi/client.py,sha256=h2HMfUSlfa5ppGn7ev3VVxlv0-T7FCqHGGDyP6DoNBw,19751
|
3
|
-
pypetkitapi/command.py,sha256=gw3_J_oZHuuGLk66P8uRSqSrySjYa8ArpKaPHi2ybCw,7155
|
4
|
-
pypetkitapi/const.py,sha256=9XNLhM9k0GwNmWPgGef5roULpsYVZ7hzxptGgNhjs74,3432
|
5
|
-
pypetkitapi/containers.py,sha256=8HA2LEzHga062LcC08c9JCowyDRmjjf8en4ImZ6tg_4,4352
|
6
|
-
pypetkitapi/exceptions.py,sha256=NWmpsI2ewC4HaIeu_uFwCeuPIHIJxZBzjoCP7aNwvhs,1139
|
7
|
-
pypetkitapi/feeder_container.py,sha256=28GXZ8Nbs08PnFZZI4ENBe3UJ63gsXT3rFa151KrPxo,13310
|
8
|
-
pypetkitapi/litter_container.py,sha256=fTPHa5VQvM6Ycm-XIeu3N1E6dTI4g1zEqC-o23sxdhE,18068
|
9
|
-
pypetkitapi/medias.py,sha256=1Nso1YZK_Bz4MrSuQ_zjawoyg5KqO_JoBSQaMmvh6Jo,7216
|
10
|
-
pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
pypetkitapi/water_fountain_container.py,sha256=3F4GP5pXJqq6kxLMSK__GgFMuZ-rz1VDIIhaVd19Kl8,6781
|
12
|
-
pypetkitapi-1.7.4.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
|
13
|
-
pypetkitapi-1.7.4.dist-info/METADATA,sha256=zOLLrY09rRjinO6hen7UAiBhi1diVDMz3ISu9ywTB04,4852
|
14
|
-
pypetkitapi-1.7.4.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
15
|
-
pypetkitapi-1.7.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|