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 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 | None) -> None:
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: int | None = None # Fictive field (for HA compatibility) copied from id
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
@@ -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
@@ -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, device: Feeder, file_path: str):
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 self.device.device_records:
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(self.device.device_records, record_type, None)
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: str):
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
- try:
121
- # Check if the file already exists
122
- filename = await extract_filename_from_url(url)
123
- full_file_path = Path(self.download_path) / filename
124
- if full_file_path.exists():
125
- _LOGGER.debug(
126
- "File already exist : %s don't need to download it", filename
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 True
135
+ return False
129
136
 
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
+ content = await response.read()
137
138
 
138
- content = await response.read()
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
- )
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
- if decrypted_data:
146
- _LOGGER.debug("Decrypt was successful")
147
- await self._save_file(decrypted_data, filename)
148
- Path(encrypted_file_path).unlink()
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.4
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.19,<2.0.0)
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,,