pypetkitapi 1.11.0__py3-none-any.whl → 1.11.3__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/__init__.py CHANGED
@@ -51,7 +51,7 @@ from .media import DownloadDecryptMedia, MediaCloud, MediaFile, MediaManager
51
51
  from .purifier_container import Purifier
52
52
  from .water_fountain_container import WaterFountain
53
53
 
54
- __version__ = "1.11.0"
54
+ __version__ = "1.11.3"
55
55
 
56
56
  __all__ = [
57
57
  "CTW3",
pypetkitapi/media.py CHANGED
@@ -18,13 +18,14 @@ import aiohttp
18
18
  from Crypto.Cipher import AES
19
19
  from Crypto.Util.Padding import unpad
20
20
 
21
- from pypetkitapi import Feeder, Litter, PetKitClient, RecordType
21
+ from pypetkitapi import Feeder, Litter, PetKitClient, RecordsItems, RecordType
22
22
  from pypetkitapi.const import (
23
23
  FEEDER_WITH_CAMERA,
24
24
  LITTER_WITH_CAMERA,
25
25
  MediaType,
26
26
  RecordTypeLST,
27
27
  )
28
+ from pypetkitapi.litter_container import LitterRecord
28
29
 
29
30
  _LOGGER = logging.getLogger(__name__)
30
31
 
@@ -269,21 +270,22 @@ class MediaManager:
269
270
  device_type = (
270
271
  device_obj.device_nfo.device_type if device_obj.device_nfo else None
271
272
  )
272
- cp_sub = (
273
- device_obj.cloud_product.subscribe if device_obj.cloud_product else None
274
- )
273
+ cp_sub = self.is_subscription_active(device_obj)
275
274
 
276
275
  if not feeder_id:
277
- _LOGGER.error("Missing feeder_id for record")
276
+ _LOGGER.warning("Missing feeder_id for record")
278
277
  return media_files
279
278
 
280
279
  if not record.items:
281
280
  return media_files
282
281
 
283
282
  for item in record.items:
283
+ if not isinstance(item, RecordsItems):
284
+ _LOGGER.debug("Record is empty")
285
+ continue
284
286
  timestamp = await self._get_timestamp(item)
285
287
  if timestamp is None:
286
- _LOGGER.error("Missing timestamp for record item")
288
+ _LOGGER.warning("Missing timestamp for record item")
287
289
  continue
288
290
  if not item.event_id:
289
291
  # Skip feed event in the future
@@ -292,10 +294,10 @@ class MediaManager:
292
294
  )
293
295
  continue
294
296
  if not user_id:
295
- _LOGGER.error("Missing user_id for record item")
297
+ _LOGGER.warning("Missing user_id for record item")
296
298
  continue
297
299
  if not item.aes_key:
298
- _LOGGER.error("Missing aes_key for record item")
300
+ _LOGGER.debug("Missing aes_key for record item")
299
301
  continue
300
302
 
301
303
  date_str = await self.get_date_from_ts(timestamp)
@@ -327,24 +329,27 @@ class MediaManager:
327
329
  litter_id = litter.device_nfo.device_id if litter.device_nfo else None
328
330
  device_type = litter.device_nfo.device_type if litter.device_nfo else None
329
331
  user_id = litter.user.id if litter.user else None
330
- cp_sub = litter.cloud_product.subscribe if litter.cloud_product else None
332
+ cp_sub = self.is_subscription_active(litter)
331
333
 
332
334
  if not litter_id:
333
- _LOGGER.error("Missing litter_id for record")
335
+ _LOGGER.warning("Missing litter_id for record")
334
336
  return media_files
335
337
 
336
338
  if not device_type:
337
- _LOGGER.error("Missing device_type for record")
339
+ _LOGGER.warning("Missing device_type for record")
338
340
  return media_files
339
341
 
340
342
  if not user_id:
341
- _LOGGER.error("Missing user_id for record")
343
+ _LOGGER.warning("Missing user_id for record")
342
344
  return media_files
343
345
 
344
346
  if not records:
345
347
  return media_files
346
348
 
347
349
  for record in records:
350
+ if not isinstance(record, LitterRecord):
351
+ _LOGGER.debug("Record is empty")
352
+ continue
348
353
  if not record.event_id:
349
354
  _LOGGER.debug("Missing event_id for record item")
350
355
  continue
@@ -376,6 +381,19 @@ class MediaManager:
376
381
  )
377
382
  return media_files
378
383
 
384
+ @staticmethod
385
+ def is_subscription_active(device: Feeder | Litter) -> bool:
386
+ """Check if the subscription is active based on the work_indate timestamp.
387
+ :param device: Device object
388
+ :return: True if the subscription is active, False otherwise
389
+ """
390
+ if device.cloud_product and device.cloud_product.work_indate:
391
+ return (
392
+ datetime.fromtimestamp(device.cloud_product.work_indate)
393
+ > datetime.now()
394
+ )
395
+ return False
396
+
379
397
  @staticmethod
380
398
  async def get_date_from_ts(timestamp: int | None) -> str:
381
399
  """Get date from timestamp.
@@ -388,7 +406,10 @@ class MediaManager:
388
406
 
389
407
  @staticmethod
390
408
  async def construct_video_url(
391
- device_type: str | None, media_url: str | None, user_id: int, cp_sub: int | None
409
+ device_type: str | None,
410
+ media_url: str | None,
411
+ user_id: int,
412
+ cp_sub: bool | None,
392
413
  ) -> str | None:
393
414
  """Construct the video URL.
394
415
  :param device_type: Device type
@@ -397,19 +418,19 @@ class MediaManager:
397
418
  :param cp_sub: Cpsub value
398
419
  :return: Constructed video URL
399
420
  """
400
- if not media_url or not user_id or cp_sub != 1:
421
+ if not media_url or not user_id or not cp_sub:
401
422
  return None
402
423
  params = parse_qs(urlparse(media_url).query)
403
424
  param_dict = {k: v[0] for k, v in params.items()}
404
425
  return f"/{device_type}/cloud/video?startTime={param_dict.get("startTime")}&deviceId={param_dict.get("deviceId")}&userId={user_id}&mark={param_dict.get("mark")}"
405
426
 
406
427
  @staticmethod
407
- async def _get_timestamp(item) -> int:
428
+ async def _get_timestamp(item) -> int | None:
408
429
  """Extract timestamp from a record item and raise an exception if it is None.
409
430
  :param item: Record item
410
431
  :return: Timestamp
411
432
  """
412
- timestamp = (
433
+ return (
413
434
  item.timestamp
414
435
  or item.completed_at
415
436
  or item.eat_start_time
@@ -419,9 +440,6 @@ class MediaManager:
419
440
  or item.time
420
441
  or None
421
442
  )
422
- if timestamp is None:
423
- raise ValueError("Can't find timestamp in record item")
424
- return timestamp
425
443
 
426
444
 
427
445
  class DownloadDecryptMedia:
@@ -515,7 +533,7 @@ class DownloadDecryptMedia:
515
533
  ]
516
534
 
517
535
  if not segment_files:
518
- _LOGGER.error("No segment files found")
536
+ _LOGGER.warning("No segment files found")
519
537
  elif len(segment_files) == 1:
520
538
  _LOGGER.debug("Single file segment, no need to concatenate")
521
539
  elif len(segment_files) > 1:
@@ -535,7 +553,7 @@ class DownloadDecryptMedia:
535
553
 
536
554
  media_api = video_data.get("mediaApi", None)
537
555
  if not media_api:
538
- _LOGGER.error("Missing mediaApi in video data")
556
+ _LOGGER.debug("Missing mediaApi in video data")
539
557
  raise ValueError("Missing mediaApi in video data")
540
558
  return await self.client.extract_segments_m3u8(str(media_api))
541
559
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pypetkitapi
3
- Version: 1.11.0
3
+ Version: 1.11.3
4
4
  Summary: Python client for PetKit API
5
5
  License: MIT
6
6
  Author: Jezza34000
@@ -77,7 +77,7 @@ import asyncio
77
77
  import logging
78
78
  import aiohttp
79
79
  from pypetkitapi.client import PetKitClient
80
- from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, LBAction, LitterCommand
80
+ from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, DeviceAction, LitterCommand
81
81
 
82
82
  logging.basicConfig(level=logging.DEBUG)
83
83
 
@@ -86,8 +86,8 @@ async def main():
86
86
  client = PetKitClient(
87
87
  username="username", # Your PetKit account username or id
88
88
  password="password", # Your PetKit account password
89
- region="FR", # Your region or country code (e.g. FR, US, etc.)
90
- timezone="Europe/Paris", # Your timezone
89
+ region="FR", # Your region or country code (e.g. FR, US,CN etc.)
90
+ timezone="Europe/Paris", # Your timezone(e.g. "Asia/Shanghai")
91
91
  session=session,
92
92
  )
93
93
 
@@ -98,26 +98,28 @@ async def main():
98
98
  for key, value in client.petkit_entities.items():
99
99
  print(f"{key}: {type(value).__name__} - {value.name}")
100
100
 
101
+ # Select a device
102
+ device_id = key
101
103
  # Read devices or pet information
102
- print(client.petkit_entities[123456789])
104
+ print(client.petkit_entities[device_id])
103
105
 
104
106
  # Send command to the devices
105
107
  ### Example 1 : Turn on the indicator light
106
108
  ### Device_ID, Command, Payload
107
- await client.send_api_request(123456789, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
109
+ await client.send_api_request(device_id, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
108
110
 
109
111
  ### Example 2 : Feed the pet
110
112
  ### Device_ID, Command, Payload
111
113
  # simple hopper :
112
- await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
114
+ await client.send_api_request(device_id, FeederCommand.MANUAL_FEED, {"amount": 1})
113
115
  # dual hopper :
114
- await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount1": 2})
116
+ await client.send_api_request(device_id, FeederCommand.MANUAL_FEED, {"amount1": 2})
115
117
  # or
116
- await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount2": 2})
118
+ await client.send_api_request(device_id, FeederCommand.MANUAL_FEED, {"amount2": 2})
117
119
 
118
120
  ### Example 3 : Start the cleaning process
119
121
  ### Device_ID, Command, Payload
120
- await client.send_api_request(123456789, LitterCommand.CONTROL_DEVICE, {LBAction.START: LBCommand.CLEANING})
122
+ await client.send_api_request(device_id, LitterCommand.CONTROL_DEVICE, {DeviceAction.START: LBCommand.CLEANING})
121
123
 
122
124
 
123
125
  if __name__ == "__main__":
@@ -1,4 +1,4 @@
1
- pypetkitapi/__init__.py,sha256=shPCRXZDzB8-Pofzju8FSV6FKX8m8F_vPfW9woBu_DQ,2107
1
+ pypetkitapi/__init__.py,sha256=Ta6BJBUyI7-C8kUXHJ1nym7-oXdmKWxXn1OfCssy9U8,2107
2
2
  pypetkitapi/bluetooth.py,sha256=eu6c2h6YHBafAhhSSy4As6tn29i5WbOH9tZzRlMm44U,7843
3
3
  pypetkitapi/client.py,sha256=wzZQUHg_ee6lmdAjli6zS7qw_sgPER9iLfcTZe4VTH4,27190
4
4
  pypetkitapi/command.py,sha256=cMCUutZCQo9Ddvjl_FYR5UjU_CqFz1iyetMznYwjpzM,7500
@@ -7,13 +7,13 @@ pypetkitapi/containers.py,sha256=F_uyDBD0a5QD4s_ArjYiKTAAg1XHYBvmV_lEnO9RQ-U,478
7
7
  pypetkitapi/exceptions.py,sha256=4BXUyYXLfZjNxdnOGJPjyE9ASIl7JmQphjws87jvHtE,1631
8
8
  pypetkitapi/feeder_container.py,sha256=PhidWd5WpsZqtdKZy60PzE67YXgQfApjm8CqvMCHK3U,14743
9
9
  pypetkitapi/litter_container.py,sha256=KWvHNAOJ6hDSeJ_55tqtzY9GxHtd9gAntPkbnVbdb-I,19275
10
- pypetkitapi/media.py,sha256=BW6WHhGGn7hxdZvN27Rcg6vDu4NXB2Q_nTa8arCvacg,25687
10
+ pypetkitapi/media.py,sha256=aGl4nw8q8UK5J2pDEH-lP-Jhy4RWFAqWfyZaLeX3wKM,26356
11
11
  pypetkitapi/purifier_container.py,sha256=ssyIxhNben5XJ4KlQTXTrtULg2ji6DqHqjzOq08d1-I,2491
12
12
  pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  pypetkitapi/schedule_container.py,sha256=OjLAY6FY-g14JNJJnYMNFV5ZtdkjUzNBit1VUiiZKnQ,2053
14
14
  pypetkitapi/utils.py,sha256=z7325kcJQUburnF28HSXrJMvY_gY9007K73Zwxp-4DQ,743
15
15
  pypetkitapi/water_fountain_container.py,sha256=5J0b-fDZYcFLNX2El7fifv8H6JMhBCt-ttxSow1ozRQ,6787
16
- pypetkitapi-1.11.0.dist-info/LICENSE,sha256=u5jNkZEn6YMrtN4Kr5rU3TcBJ5-eAt0qMx4JDsbsnzM,1074
17
- pypetkitapi-1.11.0.dist-info/METADATA,sha256=Xhjy59tO-gmZZodLmf6LDbu0L57IOqFgHL6CdDGfZu4,6256
18
- pypetkitapi-1.11.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
19
- pypetkitapi-1.11.0.dist-info/RECORD,,
16
+ pypetkitapi-1.11.3.dist-info/LICENSE,sha256=u5jNkZEn6YMrtN4Kr5rU3TcBJ5-eAt0qMx4JDsbsnzM,1074
17
+ pypetkitapi-1.11.3.dist-info/METADATA,sha256=Mqjd4OtD4X539bRYn15IxcRzz2nOMYNEj0jQRu_WnyU,6338
18
+ pypetkitapi-1.11.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
19
+ pypetkitapi-1.11.3.dist-info/RECORD,,