pypetkitapi 1.2.5__tar.gz → 1.4.0__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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pypetkitapi
3
- Version: 1.2.5
3
+ Version: 1.4.0
4
4
  Summary: Python client for PetKit API
5
5
  Home-page: https://github.com/Jezza34000/pypetkit
6
6
  License: MIT
@@ -14,6 +14,7 @@ from pypetkitapi.command import ACTIONS_MAP
14
14
  from pypetkitapi.const import (
15
15
  DEVICE_DATA,
16
16
  DEVICE_RECORDS,
17
+ DEVICE_STATS,
17
18
  DEVICES_FEEDER,
18
19
  DEVICES_LITTER_BOX,
19
20
  DEVICES_WATER_FOUNTAIN,
@@ -21,6 +22,8 @@ from pypetkitapi.const import (
21
22
  LOGIN_DATA,
22
23
  RES_KEY,
23
24
  SUCCESS_KEY,
25
+ T4,
26
+ T6,
24
27
  Header,
25
28
  PetkitDomain,
26
29
  PetkitEndpoint,
@@ -35,7 +38,7 @@ from pypetkitapi.exceptions import (
35
38
  PypetkitError,
36
39
  )
37
40
  from pypetkitapi.feeder_container import Feeder, FeederRecord
38
- from pypetkitapi.litter_container import Litter, LitterRecord
41
+ from pypetkitapi.litter_container import Litter, LitterRecord, LitterStats, PetOuGraph
39
42
  from pypetkitapi.water_fountain_container import WaterFountain, WaterFountainRecord
40
43
 
41
44
  _LOGGER = logging.getLogger(__name__)
@@ -201,37 +204,30 @@ class PetKitClient:
201
204
  for account in self.account_data:
202
205
  _LOGGER.debug("List devices data for account: %s", account)
203
206
  if account.device_list:
207
+ _LOGGER.debug("Devices in account: %s", account.device_list)
204
208
  device_list.extend(account.device_list)
205
209
 
206
- _LOGGER.debug("Fetch %s devices for this account", len(device_list))
207
-
208
- for device in device_list:
209
- device_type = device.device_type.lower()
210
- if device_type in DEVICES_FEEDER:
211
- main_tasks.append(
212
- self._fetch_device_data(account, device.device_id, Feeder)
213
- )
214
- record_tasks.append(
215
- self._fetch_device_data(account, device.device_id, FeederRecord)
216
- )
217
- elif device_type in DEVICES_LITTER_BOX:
218
- main_tasks.append(
219
- self._fetch_device_data(account, device.device_id, Litter)
220
- )
221
- record_tasks.append(
222
- self._fetch_device_data(account, device.device_id, LitterRecord)
223
- )
224
- elif device_type in DEVICES_WATER_FOUNTAIN:
225
- main_tasks.append(
226
- self._fetch_device_data(
227
- account, device.device_id, WaterFountain
228
- )
229
- )
230
- record_tasks.append(
231
- self._fetch_device_data(
232
- account, device.device_id, WaterFountainRecord
233
- )
234
- )
210
+ for device in device_list:
211
+ device_type = device.device_type.lower()
212
+ if device_type in DEVICES_FEEDER:
213
+ main_tasks.append(self._fetch_device_data(device, Feeder))
214
+ record_tasks.append(self._fetch_device_data(device, FeederRecord))
215
+ elif device_type in DEVICES_LITTER_BOX:
216
+ main_tasks.append(
217
+ self._fetch_device_data(device, Litter),
218
+ )
219
+ record_tasks.append(self._fetch_device_data(device, LitterRecord))
220
+
221
+ if device_type == T4:
222
+ record_tasks.append(self._fetch_device_data(device, LitterStats))
223
+ if device_type == T6:
224
+ record_tasks.append(self._fetch_device_data(device, PetOuGraph))
225
+
226
+ elif device_type in DEVICES_WATER_FOUNTAIN:
227
+ main_tasks.append(self._fetch_device_data(device, WaterFountain))
228
+ record_tasks.append(
229
+ self._fetch_device_data(device, WaterFountainRecord)
230
+ )
235
231
 
236
232
  # Execute main device tasks first
237
233
  await asyncio.gather(*main_tasks)
@@ -245,8 +241,7 @@ class PetKitClient:
245
241
 
246
242
  async def _fetch_device_data(
247
243
  self,
248
- account: AccountData,
249
- device_id: int,
244
+ device: Device,
250
245
  data_class: type[
251
246
  Feeder
252
247
  | Litter
@@ -258,26 +253,19 @@ class PetKitClient:
258
253
  ) -> None:
259
254
  """Fetch the device data from the PetKit servers."""
260
255
  await self.validate_session()
261
- device = None
262
-
263
- if account.device_list:
264
- device = next(
265
- (
266
- device
267
- for device in account.device_list
268
- if device.device_id == device_id
269
- ),
270
- None,
271
- )
272
- if device is None:
273
- _LOGGER.error("Device not found: id=%s", device_id)
274
- return
256
+
275
257
  device_type = device.device_type.lower()
276
258
 
277
- _LOGGER.debug("Reading device type : %s (id=%s)", device_type, device_id)
259
+ _LOGGER.debug("Reading device type : %s (id=%s)", device_type, device.device_id)
278
260
 
279
261
  endpoint = data_class.get_endpoint(device_type)
280
- query_param = data_class.query_param(account, device_type, device.device_id)
262
+
263
+ # Specific device ask for data from the device
264
+ device_cont = None
265
+ if self.petkit_entities.get(device.device_id, None):
266
+ device_cont = self.petkit_entities[device.device_id]
267
+
268
+ query_param = data_class.query_param(device, device_cont)
281
269
 
282
270
  response = await self.req.request(
283
271
  method=HTTPMethod.POST,
@@ -288,7 +276,7 @@ class PetKitClient:
288
276
 
289
277
  # Workaround for the litter box T6
290
278
  if isinstance(response, dict) and response.get("list", None):
291
- response = response.get("list")
279
+ response = response.get("list", [])
292
280
 
293
281
  # Check if the response is a list or a dict
294
282
  if isinstance(response, list):
@@ -307,11 +295,19 @@ class PetKitClient:
307
295
  device_data.device_type = device_type
308
296
 
309
297
  if data_class.data_type == DEVICE_DATA:
310
- self.petkit_entities[device_id] = device_data
298
+ self.petkit_entities[device.device_id] = device_data
311
299
  _LOGGER.debug("Device data fetched OK for %s", device_type)
312
300
  elif data_class.data_type == DEVICE_RECORDS:
313
- self.petkit_entities[device_id].device_records = device_data
301
+ self.petkit_entities[device.device_id].device_records = device_data
314
302
  _LOGGER.debug("Device records fetched OK for %s", device_type)
303
+ elif data_class.data_type == DEVICE_STATS:
304
+ if device_type == T4:
305
+ self.petkit_entities[device.device_id].device_stats = device_data
306
+ if device_type == T6:
307
+ self.petkit_entities[device.device_id].device_pet_graph_out = (
308
+ device_data
309
+ )
310
+ _LOGGER.debug("Device stats fetched OK for %s", device_type)
315
311
  else:
316
312
  _LOGGER.error("Unknown data type: %s", data_class.data_type)
317
313
 
@@ -375,8 +371,8 @@ class PetKitClient:
375
371
  data=params,
376
372
  headers=await self.get_session_id(),
377
373
  )
378
- if res in (SUCCESS_KEY, RES_KEY):
379
- # TODO : Manage to get the response from manual feeding
374
+
375
+ if res in [RES_KEY, SUCCESS_KEY]:
380
376
  _LOGGER.debug("Command executed successfully")
381
377
  return True
382
378
  _LOGGER.error("Command execution failed")
@@ -11,6 +11,7 @@ SUCCESS_KEY = "success"
11
11
 
12
12
  DEVICE_RECORDS = "deviceRecords"
13
13
  DEVICE_DATA = "deviceData"
14
+ DEVICE_STATS = "deviceStats"
14
15
  PET_DATA = "petData"
15
16
 
16
17
  # PetKit Models
@@ -114,6 +115,9 @@ class PetkitEndpoint(StrEnum):
114
115
 
115
116
  # Litter Box
116
117
  DEODORANT_RESET = "deodorantReset"
118
+ STATISTIC = "statistic"
119
+ STATISTIC_RELEASE = "statisticRelease"
120
+ GET_PET_OUT_GRAPH = "getPetOutGraph"
117
121
 
118
122
  # Feeders
119
123
  REPLENISHED_FOOD = "added"
@@ -6,7 +6,7 @@ from typing import Any, ClassVar
6
6
  from pydantic import BaseModel, Field
7
7
 
8
8
  from pypetkitapi.const import DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
9
- from pypetkitapi.containers import AccountData, CloudProduct, FirmwareDetail, Wifi
9
+ from pypetkitapi.containers import CloudProduct, Device, FirmwareDetail, Wifi
10
10
 
11
11
 
12
12
  class FeedItem(BaseModel):
@@ -157,6 +157,7 @@ class StateFeeder(BaseModel):
157
157
  class ManualFeed(BaseModel):
158
158
  """Dataclass for result data."""
159
159
 
160
+ amount: int | None = None
160
161
  amount1: int | None = None
161
162
  amount2: int | None = None
162
163
  id: str | None = None
@@ -260,15 +261,14 @@ class FeederRecord(BaseModel):
260
261
  @classmethod
261
262
  def query_param(
262
263
  cls,
263
- account: AccountData,
264
- device_type: str,
265
- device_id: int,
264
+ device: Device,
265
+ device_data: Any | None = None,
266
266
  request_date: str | None = None,
267
267
  ) -> dict:
268
268
  """Generate query parameters including request_date."""
269
269
  if request_date is None:
270
270
  request_date = datetime.now().strftime("%Y%m%d")
271
- return {"days": int(request_date), "deviceId": device_id}
271
+ return {"days": int(request_date), "deviceId": device.device_id}
272
272
 
273
273
 
274
274
  class Feeder(BaseModel):
@@ -310,7 +310,9 @@ class Feeder(BaseModel):
310
310
 
311
311
  @classmethod
312
312
  def query_param(
313
- cls, account: AccountData, device_type: str, device_id: int
313
+ cls,
314
+ device: Device,
315
+ device_data: Any | None = None,
314
316
  ) -> dict:
315
317
  """Generate query parameters including request_date."""
316
- return {"id": device_id}
318
+ return {"id": int(device.device_id)}
@@ -5,8 +5,15 @@ 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, T4, T6, PetkitEndpoint
9
- from pypetkitapi.containers import AccountData, CloudProduct, FirmwareDetail, Wifi
8
+ from pypetkitapi.const import (
9
+ DEVICE_DATA,
10
+ DEVICE_RECORDS,
11
+ DEVICE_STATS,
12
+ T4,
13
+ T6,
14
+ PetkitEndpoint,
15
+ )
16
+ from pypetkitapi.containers import CloudProduct, Device, FirmwareDetail, Wifi
10
17
 
11
18
 
12
19
  class SettingsLitter(BaseModel):
@@ -194,7 +201,9 @@ class LRSubContent(BaseModel):
194
201
 
195
202
 
196
203
  class LitterRecord(BaseModel):
197
- """Dataclass for feeder record data."""
204
+ """Dataclass for feeder record data.
205
+ Litter records
206
+ """
198
207
 
199
208
  data_type: ClassVar[str] = DEVICE_RECORDS
200
209
 
@@ -235,28 +244,140 @@ class LitterRecord(BaseModel):
235
244
  @classmethod
236
245
  def query_param(
237
246
  cls,
238
- account: AccountData,
239
- device_type: str,
240
- device_id: int,
247
+ device: Device,
248
+ device_data: Any | None = None,
241
249
  request_date: str | None = None,
242
250
  ) -> dict:
243
251
  """Generate query parameters including request_date."""
252
+ device_type = device.device_type.lower()
244
253
  if device_type == T4:
245
254
  if request_date is None:
246
255
  request_date = datetime.now().strftime("%Y%m%d")
247
- return {"date": int(request_date), "deviceId": device_id}
256
+ return {"date": int(request_date), "deviceId": device.device_id}
248
257
  if device_type == T6:
249
258
  return {
250
259
  "timestamp": int(datetime.now().timestamp()),
251
- "deviceId": device_id,
260
+ "deviceId": device.device_id,
252
261
  "type": 2,
253
262
  }
254
263
  raise ValueError(f"Invalid device type: {device_type}")
255
264
 
256
265
 
266
+ class StatisticInfo(BaseModel):
267
+ """Dataclass for statistic information.
268
+ Subclass of LitterStats.
269
+ """
270
+
271
+ pet_id: str | None = Field(None, alias="petId")
272
+ pet_name: str | None = Field(None, alias="petName")
273
+ pet_times: int | None = Field(None, alias="petTimes")
274
+ pet_total_time: int | None = Field(None, alias="petTotalTime")
275
+ pet_weight: int | None = Field(None, alias="petWeight")
276
+ statistic_date: str | None = Field(None, alias="statisticDate")
277
+ x_time: int | None = Field(None, alias="xTime")
278
+
279
+
280
+ class LitterStats(BaseModel):
281
+ """Dataclass for result data.
282
+ Supported devices = T4 only (T3 ?)
283
+ """
284
+
285
+ data_type: ClassVar[str] = DEVICE_STATS
286
+
287
+ avg_time: int | None = Field(None, alias="avgTime")
288
+ pet_ids: list[dict] | None = Field(None, alias="petIds")
289
+ statistic_info: list[StatisticInfo] | None = Field(None, alias="statisticInfo")
290
+ statistic_time: str | None = Field(None, alias="statisticTime")
291
+ times: int | None = None
292
+ total_time: int | None = Field(None, alias="totalTime")
293
+ device_type: str | None = None
294
+
295
+ @classmethod
296
+ def get_endpoint(cls, device_type: str) -> str:
297
+ """Get the endpoint URL for the given device type."""
298
+ return PetkitEndpoint.STATISTIC
299
+
300
+ @classmethod
301
+ def query_param(
302
+ cls,
303
+ device: Device,
304
+ device_data: Any | None = None,
305
+ start_date: str | None = None,
306
+ end_date: str | None = None,
307
+ ) -> dict:
308
+ """Generate query parameters including request_date."""
309
+
310
+ if start_date is None and end_date is None:
311
+ start_date = datetime.now().strftime("%Y%m%d")
312
+ end_date = datetime.now().strftime("%Y%m%d")
313
+
314
+ return {
315
+ "endDate": end_date,
316
+ "deviceId": device.device_id,
317
+ "type": 0,
318
+ "startDate": start_date,
319
+ }
320
+
321
+
322
+ class PetGraphContent(BaseModel):
323
+ """Dataclass for content data."""
324
+
325
+ auto_clear: int | None = Field(None, alias="autoClear")
326
+ img: str | None = None
327
+ interval: int | None = None
328
+ is_shit: int | None = Field(None, alias="isShit")
329
+ mark: int | None = None
330
+ media: int | None = None
331
+ pet_weight: int | None = Field(None, alias="petWeight")
332
+ shit_weight: int | None = Field(None, alias="shitWeight")
333
+ time_in: int | None = Field(None, alias="timeIn")
334
+ time_out: int | None = Field(None, alias="timeOut")
335
+
336
+
337
+ class PetOuGraph(BaseModel):
338
+ """Dataclass for event data."""
339
+
340
+ data_type: ClassVar[str] = DEVICE_STATS
341
+
342
+ aes_key: str | None = Field(None, alias="aesKey")
343
+ content: PetGraphContent | None = None
344
+ duration: int | None = None
345
+ event_id: str | None = Field(None, alias="eventId")
346
+ expire: int | None = None
347
+ is_need_upload_video: int | None = Field(None, alias="isNeedUploadVideo")
348
+ media_api: str | None = Field(None, alias="mediaApi")
349
+ pet_id: str | None = Field(None, alias="petId")
350
+ pet_name: str | None = Field(None, alias="petName")
351
+ preview: str | None = None
352
+ storage_space: int | None = Field(None, alias="storageSpace")
353
+ time: int | None = None
354
+ toilet_time: int | None = Field(None, alias="toiletTime")
355
+ device_type: str | None = None
356
+
357
+ @classmethod
358
+ def get_endpoint(cls, device_type: str) -> str:
359
+ """Get the endpoint URL for the given device type."""
360
+ return PetkitEndpoint.GET_PET_OUT_GRAPH
361
+
362
+ @classmethod
363
+ def query_param(
364
+ cls,
365
+ device: Device,
366
+ device_data: Any | None = None,
367
+ start_date: str | None = None,
368
+ end_date: str | None = None,
369
+ ) -> dict:
370
+ """Generate query parameters including request_date."""
371
+
372
+ return {
373
+ "timestamp": int(datetime.now().timestamp()),
374
+ "deviceId": device.device_id,
375
+ }
376
+
377
+
257
378
  class Litter(BaseModel):
258
379
  """Dataclass for Litter Data.
259
- Supported devices = T4, T6
380
+ Supported devices = T3, T4, T6
260
381
  """
261
382
 
262
383
  data_type: ClassVar[str] = DEVICE_DATA
@@ -293,8 +414,11 @@ class Litter(BaseModel):
293
414
  pet_out_records: list[list[int]] | None = Field(None, alias="petOutRecords")
294
415
  service_status: int | None = Field(None, alias="serviceStatus")
295
416
  total_time: int | None = Field(None, alias="totalTime")
417
+ with_k3: int | None = Field(None, alias="withK3")
296
418
  device_type: str | None = Field(None, alias="deviceType")
297
419
  device_records: list[LitterRecord] | None = None
420
+ device_stats: LitterStats | None = None
421
+ device_pet_graph_out: PetOuGraph | None = None
298
422
 
299
423
  @classmethod
300
424
  def get_endpoint(cls, device_type: str) -> str:
@@ -303,7 +427,9 @@ class Litter(BaseModel):
303
427
 
304
428
  @classmethod
305
429
  def query_param(
306
- cls, account: AccountData, device_type: str, device_id: int
430
+ cls,
431
+ device: Device,
432
+ device_data: Any | None = None,
307
433
  ) -> dict:
308
434
  """Generate query parameters including request_date."""
309
- return {"id": device_id}
435
+ return {"id": device.device_id}
@@ -6,7 +6,7 @@ from typing import Any, ClassVar
6
6
  from pydantic import BaseModel, Field
7
7
 
8
8
  from pypetkitapi.const import DEVICE_DATA, DEVICE_RECORDS, PetkitEndpoint
9
- from pypetkitapi.containers import AccountData
9
+ from pypetkitapi.containers import Device
10
10
 
11
11
 
12
12
  class Electricity(BaseModel):
@@ -103,21 +103,19 @@ class WaterFountainRecord(BaseModel):
103
103
  @classmethod
104
104
  def query_param(
105
105
  cls,
106
- account: AccountData,
107
- device_type: str,
108
- device_id: int,
106
+ device: Device,
107
+ device_data: Any | None = None,
109
108
  request_date: str | None = None,
110
109
  ) -> dict:
111
110
  """Generate query parameters including request_date."""
112
- if not account.user_list or not account.user_list[0]:
113
- raise ValueError("The account does not have a valid user_list.")
114
-
115
111
  if request_date is None:
116
112
  request_date = datetime.now().strftime("%Y%m%d")
113
+ if device_data is None or not hasattr(device_data, "user_id"):
114
+ raise ValueError("The device_data does not have a valid user_id.")
117
115
  return {
118
116
  "day": int(request_date),
119
- "deviceId": device_id,
120
- "userId": account.user_list[0].user_id,
117
+ "deviceId": device.device_id,
118
+ "userId": device_data.user_id,
121
119
  }
122
120
 
123
121
 
@@ -173,7 +171,9 @@ class WaterFountain(BaseModel):
173
171
 
174
172
  @classmethod
175
173
  def query_param(
176
- cls, account: AccountData, device_type: str, device_id: int
174
+ cls,
175
+ device: Device,
176
+ device_data: Any | None = None,
177
177
  ) -> dict:
178
178
  """Generate query parameters including request_date."""
179
- return {"id": device_id}
179
+ return {"id": device.device_id}
@@ -187,7 +187,7 @@ build-backend = "poetry.core.masonry.api"
187
187
 
188
188
  [tool.poetry]
189
189
  name = "pypetkitapi"
190
- version = "1.2.5"
190
+ version = "1.4.0"
191
191
  description = "Python client for PetKit API"
192
192
  authors = ["Jezza34000 <info@mail.com>"]
193
193
  readme = "README.md"
@@ -204,7 +204,7 @@ black = "^24.10.0"
204
204
  ruff = "^0.8.1"
205
205
 
206
206
  [tool.bumpver]
207
- current_version = "1.2.5"
207
+ current_version = "1.4.0"
208
208
  version_pattern = "MAJOR.MINOR.PATCH"
209
209
  commit_message = "bump version {old_version} -> {new_version}"
210
210
  tag_message = "{new_version}"
File without changes
File without changes