pypetkitapi 1.7.10__py3-none-any.whl → 1.9.0__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
@@ -1,15 +1,25 @@
1
1
  """Pypetkit: A Python library for interfacing with PetKit"""
2
2
 
3
3
  from .client import PetKitClient
4
- from .command import FeederCommand, LBAction, LBCommand, LitterCommand, PetCommand
4
+ from .command import (
5
+ DeviceAction,
6
+ DeviceCommand,
7
+ FeederCommand,
8
+ LBCommand,
9
+ LitterCommand,
10
+ PetCommand,
11
+ PurMode,
12
+ )
5
13
  from .const import (
6
14
  CTW3,
7
15
  D3,
8
16
  D4,
9
17
  D4H,
10
18
  D4S,
19
+ D4SH,
11
20
  DEVICES_FEEDER,
12
21
  DEVICES_LITTER_BOX,
22
+ DEVICES_PURIFIER,
13
23
  DEVICES_WATER_FOUNTAIN,
14
24
  FEEDER,
15
25
  FEEDER_MINI,
@@ -22,35 +32,55 @@ from .const import (
22
32
  W5,
23
33
  RecordType,
24
34
  )
35
+ from .containers import Pet
36
+ from .exceptions import PetkitAuthenticationError, PypetkitError
37
+ from .feeder_container import Feeder, RecordsItems
38
+ from .litter_container import Litter, LitterRecord, WorkState
25
39
  from .medias import MediaHandler, MediasFiles
40
+ from .purifier_container import Purifier
41
+ from .water_fountain_container import WaterFountain
26
42
 
27
- __version__ = "1.7.10"
43
+ __version__ = "1.9.0"
28
44
 
29
45
  __all__ = [
30
- "MediasFiles",
31
- "MediaHandler",
46
+ "CTW3",
32
47
  "D3",
33
48
  "D4",
34
49
  "D4H",
35
50
  "D4S",
51
+ "D4SH",
52
+ "DEVICES_FEEDER",
53
+ "DEVICES_LITTER_BOX",
54
+ "DEVICES_PURIFIER",
55
+ "DEVICES_WATER_FOUNTAIN",
56
+ "DeviceAction",
57
+ "DeviceCommand",
36
58
  "FEEDER",
37
59
  "FEEDER_MINI",
60
+ "Feeder",
61
+ "FeederCommand",
62
+ "K2",
63
+ "K3",
64
+ "LBCommand",
65
+ "Litter",
66
+ "LitterCommand",
67
+ "LitterRecord",
68
+ "MediaHandler",
69
+ "MediasFiles",
70
+ "Pet",
71
+ "PetCommand",
72
+ "PetKitClient",
73
+ "PetkitAuthenticationError",
74
+ "PurMode",
75
+ "Purifier",
76
+ "PypetkitError",
77
+ "RecordType",
78
+ "RecordsItems",
38
79
  "T3",
39
80
  "T4",
40
81
  "T5",
41
82
  "T6",
42
83
  "W5",
43
- "CTW3",
44
- "K2",
45
- "K3",
46
- "DEVICES_FEEDER",
47
- "DEVICES_LITTER_BOX",
48
- "DEVICES_WATER_FOUNTAIN",
49
- "RecordType",
50
- "PetKitClient",
51
- "FeederCommand",
52
- "LitterCommand",
53
- "PetCommand",
54
- "LBCommand",
55
- "LBAction",
84
+ "WaterFountain",
85
+ "WorkState",
56
86
  ]
pypetkitapi/client.py CHANGED
@@ -1,25 +1,32 @@
1
1
  """Pypetkit Client: A Python library for interfacing with PetKit"""
2
2
 
3
3
  import asyncio
4
+ import base64
4
5
  from datetime import datetime, timedelta
5
6
  from enum import StrEnum
6
7
  import hashlib
7
8
  from http import HTTPMethod
8
9
  import logging
10
+ import urllib.parse
9
11
 
10
12
  import aiohttp
11
13
  from aiohttp import ContentTypeError
12
14
 
13
- from pypetkitapi.command import ACTIONS_MAP
15
+ from pypetkitapi.command import ACTIONS_MAP, FOUNTAIN_COMMAND, FountainAction
14
16
  from pypetkitapi.const import (
17
+ BLE_CONNECT_ATTEMPT,
18
+ BLE_END_TRAME,
19
+ BLE_START_TRAME,
15
20
  DEVICE_DATA,
16
21
  DEVICE_RECORDS,
17
22
  DEVICE_STATS,
18
23
  DEVICES_FEEDER,
19
24
  DEVICES_LITTER_BOX,
25
+ DEVICES_PURIFIER,
20
26
  DEVICES_WATER_FOUNTAIN,
21
27
  ERR_KEY,
22
28
  LOGIN_DATA,
29
+ PET,
23
30
  RES_KEY,
24
31
  T4,
25
32
  T6,
@@ -27,7 +34,14 @@ from pypetkitapi.const import (
27
34
  PetkitDomain,
28
35
  PetkitEndpoint,
29
36
  )
30
- from pypetkitapi.containers import AccountData, Device, Pet, RegionInfo, SessionInfo
37
+ from pypetkitapi.containers import (
38
+ AccountData,
39
+ BleRelay,
40
+ Device,
41
+ Pet,
42
+ RegionInfo,
43
+ SessionInfo,
44
+ )
31
45
  from pypetkitapi.exceptions import (
32
46
  PetkitAuthenticationError,
33
47
  PetkitAuthenticationUnregisteredEmailError,
@@ -40,6 +54,7 @@ from pypetkitapi.exceptions import (
40
54
  )
41
55
  from pypetkitapi.feeder_container import Feeder, FeederRecord
42
56
  from pypetkitapi.litter_container import Litter, LitterRecord, LitterStats, PetOutGraph
57
+ from pypetkitapi.purifier_container import Purifier
43
58
  from pypetkitapi.water_fountain_container import WaterFountain, WaterFountainRecord
44
59
 
45
60
  _LOGGER = logging.getLogger(__name__)
@@ -50,7 +65,7 @@ class PetKitClient:
50
65
 
51
66
  _session: SessionInfo | None = None
52
67
  account_data: list[AccountData] = []
53
- petkit_entities: dict[int, Feeder | Litter | WaterFountain | Pet] = {}
68
+ petkit_entities: dict[int, Feeder | Litter | WaterFountain | Purifier | Pet] = {}
54
69
 
55
70
  def __init__(
56
71
  self,
@@ -75,6 +90,7 @@ class PetKitClient:
75
90
  async def _get_base_url(self) -> None:
76
91
  """Get the list of API servers, filter by region, and return the matching server."""
77
92
  _LOGGER.debug("Getting API server list")
93
+ self.req.base_url = PetkitDomain.PASSPORT_PETKIT
78
94
 
79
95
  if self.region.lower() == "china":
80
96
  self.req.base_url = PetkitDomain.CHINA_SRV
@@ -126,8 +142,9 @@ class PetKitClient:
126
142
  data["validCode"] = valid_code
127
143
  else:
128
144
  _LOGGER.debug("Login method: using password")
129
- pwd = hashlib.md5(self.password.encode()).hexdigest() # noqa: S324
130
- data["password"] = pwd # noqa: S324
145
+ data["password"] = hashlib.md5(
146
+ self.password.encode()
147
+ ).hexdigest() # noqa: S324
131
148
 
132
149
  # Send the login request
133
150
  response = await self.req.request(
@@ -149,6 +166,7 @@ class PetKitClient:
149
166
  )
150
167
  session_data = response["session"]
151
168
  self._session = SessionInfo(**session_data)
169
+ self._session.refreshed_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")
152
170
 
153
171
  async def validate_session(self) -> None:
154
172
  """Check if the session is still valid and refresh or re-login if necessary."""
@@ -195,6 +213,16 @@ class PetKitClient:
195
213
  if account.pet_list:
196
214
  for pet in account.pet_list:
197
215
  self.petkit_entities[pet.pet_id] = pet
216
+ pet.device_nfo = Device(
217
+ deviceType=PET,
218
+ deviceId=pet.pet_id,
219
+ createdAt=pet.created_at,
220
+ deviceName=pet.pet_name,
221
+ groupId=0,
222
+ type=0,
223
+ typeCode=0,
224
+ uniqueId=pet.sn,
225
+ )
198
226
 
199
227
  async def get_devices_data(self) -> None:
200
228
  """Get the devices data from the PetKit servers."""
@@ -216,10 +244,12 @@ class PetKitClient:
216
244
  _LOGGER.debug("Found %s devices", len(account.device_list))
217
245
 
218
246
  for device in device_list:
219
- device_type = device.device_type.lower()
247
+ device_type = device.device_type
248
+
220
249
  if device_type in DEVICES_FEEDER:
221
250
  main_tasks.append(self._fetch_device_data(device, Feeder))
222
251
  record_tasks.append(self._fetch_device_data(device, FeederRecord))
252
+
223
253
  elif device_type in DEVICES_LITTER_BOX:
224
254
  main_tasks.append(
225
255
  self._fetch_device_data(device, Litter),
@@ -237,6 +267,9 @@ class PetKitClient:
237
267
  self._fetch_device_data(device, WaterFountainRecord)
238
268
  )
239
269
 
270
+ elif device_type in DEVICES_PURIFIER:
271
+ main_tasks.append(self._fetch_device_data(device, Purifier))
272
+
240
273
  # Execute main device tasks first
241
274
  await asyncio.gather(*main_tasks)
242
275
 
@@ -247,7 +280,7 @@ class PetKitClient:
247
280
  stats_tasks = [
248
281
  self.populate_pet_stats(self.petkit_entities[device.device_id])
249
282
  for device in device_list
250
- if device.device_type.lower() in DEVICES_LITTER_BOX
283
+ if device.device_type in DEVICES_LITTER_BOX
251
284
  ]
252
285
 
253
286
  # Execute stats tasks
@@ -264,6 +297,7 @@ class PetKitClient:
264
297
  Feeder
265
298
  | Litter
266
299
  | WaterFountain
300
+ | Purifier
267
301
  | FeederRecord
268
302
  | LitterRecord
269
303
  | WaterFountainRecord
@@ -272,7 +306,7 @@ class PetKitClient:
272
306
  ],
273
307
  ) -> None:
274
308
  """Fetch the device data from the PetKit servers."""
275
- device_type = device.device_type.lower()
309
+ device_type = device.device_type
276
310
 
277
311
  _LOGGER.debug("Reading device type : %s (id=%s)", device_type, device.device_id)
278
312
 
@@ -309,15 +343,9 @@ class PetKitClient:
309
343
  _LOGGER.error("Unexpected response type: %s", type(response))
310
344
  return
311
345
 
312
- # Add the device type into dataclass
313
- if isinstance(device_data, list):
314
- for item in device_data:
315
- item.device_type = device_type
316
- else:
317
- device_data.device_type = device_type
318
-
319
346
  if data_class.data_type == DEVICE_DATA:
320
347
  self.petkit_entities[device.device_id] = device_data
348
+ self.petkit_entities[device.device_id].device_nfo = device
321
349
  _LOGGER.debug("Device data fetched OK for %s", device_type)
322
350
  elif data_class.data_type == DEVICE_RECORDS:
323
351
  self.petkit_entities[device.device_id].device_records = device_data
@@ -358,9 +386,12 @@ class PetKitClient:
358
386
 
359
387
  pets_list = await self.get_pets_list()
360
388
  for pet in pets_list:
361
- if stats_data.device_type == T4 and stats_data.device_records:
389
+ if stats_data.device_nfo.device_type == T4 and stats_data.device_records:
362
390
  await self._process_t4(pet, stats_data.device_records)
363
- elif stats_data.device_type == T6 and stats_data.device_pet_graph_out:
391
+ elif (
392
+ stats_data.device_nfo.device_type == T6
393
+ and stats_data.device_pet_graph_out
394
+ ):
364
395
  await self._process_t6(pet, stats_data.device_pet_graph_out)
365
396
 
366
397
  async def _process_t4(self, pet, device_records):
@@ -394,6 +425,171 @@ class PetKitClient:
394
425
  pet.last_duration_usage = self.get_safe_value(graph.toilet_time)
395
426
  pet.last_device_used = "Purobot Ultra"
396
427
 
428
+ async def _get_fountain_instance(self, fountain_id: int) -> WaterFountain:
429
+ # Retrieve the water fountain object
430
+ water_fountain = self.petkit_entities.get(fountain_id)
431
+ if not water_fountain:
432
+ _LOGGER.error("Water fountain with ID %s not found.", fountain_id)
433
+ raise ValueError(f"Water fountain with ID {fountain_id} not found.")
434
+ return water_fountain
435
+
436
+ async def check_relay_availability(self, fountain_id: int) -> bool:
437
+ """Check if BLE relay is available for the account."""
438
+ fountain = None
439
+ for account in self.account_data:
440
+ fountain = next(
441
+ (
442
+ device
443
+ for device in account.device_list
444
+ if device.device_id == fountain_id
445
+ ),
446
+ None,
447
+ )
448
+ if fountain:
449
+ break
450
+
451
+ if not fountain:
452
+ raise ValueError(
453
+ f"Fountain with device_id {fountain_id} not found for the current account"
454
+ )
455
+
456
+ group_id = fountain.group_id
457
+
458
+ response = await self.req.request(
459
+ method=HTTPMethod.POST,
460
+ url=f"{PetkitEndpoint.BLE_AS_RELAY}",
461
+ params={"groupId": group_id},
462
+ headers=await self.get_session_id(),
463
+ )
464
+ ble_relays = [BleRelay(**relay) for relay in response]
465
+
466
+ if len(ble_relays) == 0:
467
+ _LOGGER.warning("No BLE relay devices found.")
468
+ return False
469
+ return True
470
+
471
+ async def open_ble_connection(self, fountain_id: int) -> bool:
472
+ """Open a BLE connection to a PetKit device."""
473
+ _LOGGER.info("Opening BLE connection to fountain %s", fountain_id)
474
+ water_fountain = await self._get_fountain_instance(fountain_id)
475
+
476
+ if await self.check_relay_availability(fountain_id) is False:
477
+ _LOGGER.error("BLE relay not available.")
478
+ return False
479
+
480
+ if water_fountain.is_connected is True:
481
+ _LOGGER.error("BLE connection already established.")
482
+ return True
483
+
484
+ response = await self.req.request(
485
+ method=HTTPMethod.POST,
486
+ url=PetkitEndpoint.BLE_CONNECT,
487
+ data={
488
+ "bleId": fountain_id,
489
+ "type": 24,
490
+ "mac": water_fountain.mac,
491
+ },
492
+ headers=await self.get_session_id(),
493
+ )
494
+ if response != {"state": 1}:
495
+ _LOGGER.error("Failed to establish BLE connection.")
496
+ water_fountain.is_connected = False
497
+ return False
498
+
499
+ for attempt in range(BLE_CONNECT_ATTEMPT):
500
+ _LOGGER.warning("BLE connection attempt n%s", attempt)
501
+ response = await self.req.request(
502
+ method=HTTPMethod.POST,
503
+ url=PetkitEndpoint.BLE_POLL,
504
+ data={
505
+ "bleId": fountain_id,
506
+ "type": 24,
507
+ "mac": water_fountain.mac,
508
+ },
509
+ headers=await self.get_session_id(),
510
+ )
511
+ if response == 1:
512
+ _LOGGER.info("BLE connection established successfully.")
513
+ water_fountain.is_connected = True
514
+ water_fountain.last_ble_poll = datetime.now().strftime(
515
+ "%Y-%m-%dT%H:%M:%S.%f"
516
+ )
517
+ return True
518
+ await asyncio.sleep(4)
519
+ _LOGGER.error("Failed to establish BLE connection after multiple attempts.")
520
+ water_fountain.is_connected = False
521
+ return False
522
+
523
+ async def close_ble_connection(self, fountain_id: int) -> None:
524
+ """Close the BLE connection to a PetKit device."""
525
+ _LOGGER.info("Closing BLE connection to fountain %s", fountain_id)
526
+ water_fountain = await self._get_fountain_instance(fountain_id)
527
+
528
+ await self.req.request(
529
+ method=HTTPMethod.POST,
530
+ url=PetkitEndpoint.BLE_CANCEL,
531
+ data={
532
+ "bleId": fountain_id,
533
+ "type": 24,
534
+ "mac": water_fountain.mac,
535
+ },
536
+ headers=await self.get_session_id(),
537
+ )
538
+ _LOGGER.info("BLE connection closed successfully.")
539
+
540
+ async def get_ble_cmd_data(
541
+ self, fountain_command: list, counter: int
542
+ ) -> tuple[int, str]:
543
+ """Prepare BLE data by adding start and end trame to the command and extracting the first number."""
544
+ cmd_code = fountain_command[0]
545
+ modified_command = fountain_command[:2] + [counter] + fountain_command[2:]
546
+ ble_data = [*BLE_START_TRAME, *modified_command, *BLE_END_TRAME]
547
+ encoded_data = await self._encode_ble_data(ble_data)
548
+ return cmd_code, encoded_data
549
+
550
+ @staticmethod
551
+ async def _encode_ble_data(byte_list: list) -> str:
552
+ """Encode a list of bytes to a URL encoded base64 string."""
553
+ byte_array = bytearray(byte_list)
554
+ b64_encoded = base64.b64encode(byte_array)
555
+ return urllib.parse.quote(b64_encoded)
556
+
557
+ async def send_ble_command(self, fountain_id: int, command: FountainAction) -> bool:
558
+ """BLE command to a PetKit device."""
559
+ _LOGGER.info("Sending BLE command to fountain %s", fountain_id)
560
+ water_fountain = await self._get_fountain_instance(fountain_id)
561
+
562
+ if water_fountain.is_connected is False:
563
+ _LOGGER.error("BLE connection not established.")
564
+ return False
565
+
566
+ command = FOUNTAIN_COMMAND.get[command, None]
567
+ if command is None:
568
+ _LOGGER.error("Command not found.")
569
+ return False
570
+
571
+ cmd_code, cmd_data = await self.get_ble_cmd_data(
572
+ command, water_fountain.ble_counter
573
+ )
574
+
575
+ response = await self.req.request(
576
+ method=HTTPMethod.POST,
577
+ url=PetkitEndpoint.BLE_CONTROL_DEVICE,
578
+ data={
579
+ "bleId": water_fountain.id,
580
+ "cmd": cmd_code,
581
+ "data": cmd_data,
582
+ "mac": water_fountain.mac,
583
+ "type": 24,
584
+ },
585
+ headers=await self.get_session_id(),
586
+ )
587
+ if response != 1:
588
+ _LOGGER.error("Failed to send BLE command.")
589
+ return False
590
+ _LOGGER.info("BLE command sent successfully.")
591
+ return True
592
+
397
593
  async def send_api_request(
398
594
  self,
399
595
  device_id: int,
@@ -403,21 +599,21 @@ class PetKitClient:
403
599
  """Control the device using the PetKit API."""
404
600
  await self.validate_session()
405
601
 
406
- device = self.petkit_entities.get(device_id)
602
+ device = self.petkit_entities.get(device_id, None)
407
603
  if not device:
408
604
  raise PypetkitError(f"Device with ID {device_id} not found.")
409
605
 
410
606
  _LOGGER.debug(
411
607
  "Control API device=%s id=%s action=%s param=%s",
412
- device.device_type,
608
+ device.device_nfo.device_type,
413
609
  device_id,
414
610
  action,
415
611
  setting,
416
612
  )
417
613
 
418
614
  # Check if the device type is supported
419
- if device.device_type:
420
- device_type = device.device_type.lower()
615
+ if device.device_nfo.device_type:
616
+ device_type = device.device_nfo.device_type
421
617
  else:
422
618
  raise PypetkitError(
423
619
  "Device type is not available, and is mandatory for sending commands."
@@ -442,7 +638,7 @@ class PetKitClient:
442
638
  else:
443
639
  endpoint = action_info.endpoint
444
640
  _LOGGER.debug("Endpoint field")
445
- url = f"{device.device_type.lower()}/{endpoint}"
641
+ url = f"{device.device_nfo.device_type}/{endpoint}"
446
642
 
447
643
  # Get the parameters
448
644
  if setting is not None:
pypetkitapi/command.py CHANGED
@@ -16,6 +16,8 @@ from pypetkitapi.const import (
16
16
  DEVICES_FEEDER,
17
17
  FEEDER,
18
18
  FEEDER_MINI,
19
+ K2,
20
+ K3,
19
21
  T3,
20
22
  T4,
21
23
  T5,
@@ -27,11 +29,19 @@ from pypetkitapi.const import (
27
29
  class DeviceCommand(StrEnum):
28
30
  """Device Command"""
29
31
 
32
+ POWER = "power_device"
33
+ CONTROL_DEVICE = "control_device"
30
34
  UPDATE_SETTING = "update_setting"
31
35
 
32
36
 
37
+ class FountainCommand(StrEnum):
38
+ """Device Command"""
39
+
40
+ CONTROL_DEVICE = "control_device"
41
+
42
+
33
43
  class FeederCommand(StrEnum):
34
- """Feeder Command"""
44
+ """Specific Feeder Command"""
35
45
 
36
46
  CALL_PET = "call_pet"
37
47
  CALIBRATION = "food_reset"
@@ -45,25 +55,17 @@ class FeederCommand(StrEnum):
45
55
 
46
56
 
47
57
  class LitterCommand(StrEnum):
48
- """LitterCommand"""
58
+ """Specific LitterCommand"""
49
59
 
50
- POWER = "power"
51
- CONTROL_DEVICE = "control_device"
52
60
  RESET_DEODORIZER = "reset_deodorizer"
53
61
 
54
62
 
55
63
  class PetCommand(StrEnum):
56
- """PetCommand"""
64
+ """Specific PetCommand"""
57
65
 
58
66
  PET_UPDATE_SETTING = "pet_update_setting"
59
67
 
60
68
 
61
- class FountainCommand(StrEnum):
62
- """Fountain Command"""
63
-
64
- CONTROL_DEVICE = "control_device"
65
-
66
-
67
69
  class LBCommand(IntEnum):
68
70
  """LitterBoxCommand"""
69
71
 
@@ -79,29 +81,43 @@ class LBCommand(IntEnum):
79
81
  MAINTENANCE = 9
80
82
 
81
83
 
82
- class LBAction(StrEnum):
83
- """LitterBoxCommandKey"""
84
+ class PurMode(IntEnum):
85
+ """Purifier working mode"""
86
+
87
+ AUTO_MODE = 0
88
+ SILENT_MODE = 1
89
+ STANDARD_MODE = 2
90
+ STRONG_MODE = 3
91
+
92
+
93
+ class DeviceAction(StrEnum):
94
+ """Device action for LitterBox and Purifier"""
84
95
 
96
+ # LitterBox only
85
97
  CONTINUE = "continue_action"
86
98
  END = "end_action"
87
- POWER = "power_action"
88
99
  START = "start_action"
89
100
  STOP = "stop_action"
101
+ # Purifier only
102
+ MODE = "mode_action"
103
+ # All devices
104
+ POWER = "power_action"
90
105
 
91
106
 
92
107
  class FountainAction(StrEnum):
93
108
  """Fountain Action"""
94
109
 
110
+ MODE_NORMAL = "Normal"
111
+ MODE_SMART = "Smart"
112
+ MODE_STANDARD = "Standard"
113
+ MODE_INTERMITTENT = "Intermittent"
95
114
  PAUSE = "Pause"
96
- NORMAL_TO_PAUSE = "Normal To Pause"
97
- SMART_TO_PAUSE = "Smart To Pause"
98
- NORMAL = "Normal"
99
- SMART = "Smart"
115
+ CONTINUE = "Continue"
116
+ POWER_OFF = "Power Off"
117
+ POWER_ON = "Power On"
100
118
  RESET_FILTER = "Reset Filter"
101
119
  DO_NOT_DISTURB = "Do Not Disturb"
102
120
  DO_NOT_DISTURB_OFF = "Do Not Disturb Off"
103
- FIRST_BLE_CMND = "First BLE Command"
104
- SECOND_BLE_CMND = "Second BLE Command"
105
121
  LIGHT_LOW = "Light Low"
106
122
  LIGHT_MEDIUM = "Light Medium"
107
123
  LIGHT_HIGH = "Light High"
@@ -109,20 +125,12 @@ class FountainAction(StrEnum):
109
125
  LIGHT_OFF = "Light Off"
110
126
 
111
127
 
112
- FOUNTAIN_COMMAND_TO_CODE = {
113
- FountainAction.DO_NOT_DISTURB: "221",
114
- FountainAction.DO_NOT_DISTURB_OFF: "221",
115
- FountainAction.LIGHT_ON: "221",
116
- FountainAction.LIGHT_OFF: "221",
117
- FountainAction.LIGHT_LOW: "221",
118
- FountainAction.LIGHT_MEDIUM: "221",
119
- FountainAction.LIGHT_HIGH: "221",
120
- FountainAction.PAUSE: "220",
121
- FountainAction.RESET_FILTER: "222",
122
- FountainAction.NORMAL: "220",
123
- FountainAction.NORMAL_TO_PAUSE: "220",
124
- FountainAction.SMART: "220",
125
- FountainAction.SMART_TO_PAUSE: "220",
128
+ FOUNTAIN_COMMAND = {
129
+ FountainAction.PAUSE: [220, 1, 3, 0, 1, 0, 2],
130
+ FountainAction.CONTINUE: [220, 1, 3, 0, 1, 1, 2],
131
+ FountainAction.RESET_FILTER: [222, 1, 0, 0],
132
+ FountainAction.POWER_OFF: [220, 1, 3, 0, 0, 1, 1],
133
+ FountainAction.POWER_ON: [220, 1, 3, 0, 1, 1, 1],
126
134
  }
127
135
 
128
136
 
@@ -137,18 +145,18 @@ class CmdData:
137
145
 
138
146
  def get_endpoint_manual_feed(device):
139
147
  """Get the endpoint for the device"""
140
- if device.device_type == FEEDER_MINI:
148
+ if device.device_nfo.device_type == FEEDER_MINI:
141
149
  return PetkitEndpoint.MANUAL_FEED_MINI
142
- if device.device_type == FEEDER:
150
+ if device.device_nfo.device_type == FEEDER:
143
151
  return PetkitEndpoint.MANUAL_FEED_FRESH_ELEMENT
144
152
  return PetkitEndpoint.MANUAL_FEED_DUAL
145
153
 
146
154
 
147
155
  def get_endpoint_reset_desiccant(device):
148
156
  """Get the endpoint for the device"""
149
- if device.device_type == FEEDER_MINI:
157
+ if device.device_nfo.device_type == FEEDER_MINI:
150
158
  return PetkitEndpoint.MINI_DESICCANT_RESET
151
- if device.device_type == FEEDER:
159
+ if device.device_nfo.device_type == FEEDER:
152
160
  return PetkitEndpoint.FRESH_ELEMENT_DESICCANT_RESET
153
161
  return PetkitEndpoint.DESICCANT_RESET
154
162
 
@@ -162,6 +170,15 @@ ACTIONS_MAP = {
162
170
  },
163
171
  supported_device=ALL_DEVICES,
164
172
  ),
173
+ DeviceCommand.CONTROL_DEVICE: CmdData(
174
+ endpoint=PetkitEndpoint.CONTROL_DEVICE,
175
+ params=lambda device, command: {
176
+ "id": device.id,
177
+ "kv": json.dumps(command),
178
+ "type": list(command.keys())[0].split("_")[0],
179
+ },
180
+ supported_device=[K2, K3, T3, T4, T5, T6],
181
+ ),
165
182
  FeederCommand.REMOVE_DAILY_FEED: CmdData(
166
183
  endpoint=PetkitEndpoint.REMOVE_DAILY_FEED,
167
184
  params=lambda device, setting: {
@@ -204,7 +221,7 @@ ACTIONS_MAP = {
204
221
  FeederCommand.CANCEL_MANUAL_FEED: CmdData(
205
222
  endpoint=lambda device: (
206
223
  PetkitEndpoint.FRESH_ELEMENT_CANCEL_FEED
207
- if device.device_type == FEEDER
224
+ if device.device_nfo.device_type == FEEDER
208
225
  else PetkitEndpoint.CANCEL_FEED
209
226
  ),
210
227
  params=lambda device: {
@@ -212,7 +229,7 @@ ACTIONS_MAP = {
212
229
  "deviceId": device.id,
213
230
  **(
214
231
  {"id": device.manual_feed_id}
215
- if device.device_type.lower() in [D4H, D4S, D4SH]
232
+ if device.device_nfo.device_type in [D4H, D4S, D4SH]
216
233
  else {}
217
234
  ),
218
235
  },
@@ -255,15 +272,6 @@ ACTIONS_MAP = {
255
272
  },
256
273
  supported_device=[D3],
257
274
  ),
258
- LitterCommand.CONTROL_DEVICE: CmdData(
259
- endpoint=PetkitEndpoint.CONTROL_DEVICE,
260
- params=lambda device, command: {
261
- "id": device.id,
262
- "kv": json.dumps(command),
263
- "type": list(command.keys())[0].split("_")[0],
264
- },
265
- supported_device=[T3, T4, T5, T6],
266
- ),
267
275
  PetCommand.PET_UPDATE_SETTING: CmdData(
268
276
  endpoint=PetkitEndpoint.CONTROL_DEVICE,
269
277
  params=lambda pet, setting: {
@@ -272,15 +280,4 @@ ACTIONS_MAP = {
272
280
  },
273
281
  supported_device=ALL_DEVICES,
274
282
  ),
275
- # FountainCommand.CONTROL_DEVICE: CmdData(
276
- # endpoint=PetkitEndpoint.CONTROL_DEVICE,
277
- # params=lambda device, setting: {
278
- # "bleId": device.id,
279
- # "cmd": cmnd_code,
280
- # "data": ble_data,
281
- # "mac": device.mac,
282
- # "type": water_fountain.ble_relay,
283
- # },
284
- # supported_device=[CTW3],
285
- # ),
286
283
  }
pypetkitapi/const.py CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  from enum import StrEnum
4
4
 
5
- MIN_FEED_AMOUNT = 0
6
- MAX_FEED_AMOUNT = 10
7
-
8
5
  RES_KEY = "result"
9
6
  ERR_KEY = "error"
10
7
  SUCCESS_KEY = "success"
@@ -14,6 +11,11 @@ DEVICE_DATA = "deviceData"
14
11
  DEVICE_STATS = "deviceStats"
15
12
  PET_DATA = "petData"
16
13
 
14
+ # Bluetooth
15
+ BLE_CONNECT_ATTEMPT = 4
16
+ BLE_START_TRAME = [250, 252, 253]
17
+ BLE_END_TRAME = [251]
18
+
17
19
  # PetKit Models
18
20
  FEEDER = "feeder"
19
21
  FEEDER_MINI = "feedermini"
@@ -30,11 +32,18 @@ W5 = "w5"
30
32
  CTW3 = "ctw3"
31
33
  K2 = "k2"
32
34
  K3 = "k3"
35
+ PET = "pet"
33
36
 
34
37
  DEVICES_LITTER_BOX = [T3, T4, T5, T6]
35
38
  DEVICES_FEEDER = [FEEDER, FEEDER_MINI, D3, D4, D4S, D4H, D4SH]
36
39
  DEVICES_WATER_FOUNTAIN = [W5, CTW3]
37
- ALL_DEVICES = [*DEVICES_LITTER_BOX, *DEVICES_FEEDER, *DEVICES_WATER_FOUNTAIN]
40
+ DEVICES_PURIFIER = [K2]
41
+ ALL_DEVICES = [
42
+ *DEVICES_LITTER_BOX,
43
+ *DEVICES_FEEDER,
44
+ *DEVICES_WATER_FOUNTAIN,
45
+ *DEVICES_PURIFIER,
46
+ ]
38
47
 
39
48
 
40
49
  class PetkitDomain(StrEnum):
@@ -116,11 +125,12 @@ class PetkitEndpoint(StrEnum):
116
125
  GET_DEVICE_RECORD_RELEASE = "getDeviceRecordRelease"
117
126
  UPDATE_SETTING = "updateSettings"
118
127
 
119
- # Bluetooth relay
128
+ # Bluetooth
120
129
  BLE_AS_RELAY = "ble/ownSupportBleDevices"
121
130
  BLE_CONNECT = "ble/connect"
122
131
  BLE_POLL = "ble/poll"
123
132
  BLE_CANCEL = "ble/cancel"
133
+ BLE_CONTROL_DEVICE = "ble/controlDevice"
124
134
 
125
135
  # Fountain & Litter Box
126
136
  CONTROL_DEVICE = "controlDevice"
pypetkitapi/containers.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Dataclasses container for petkit API."""
2
2
 
3
- from pydantic import BaseModel, Field
3
+ from pydantic import BaseModel, Field, field_validator
4
4
 
5
5
 
6
6
  class RegionInfo(BaseModel):
@@ -43,6 +43,7 @@ class SessionInfo(BaseModel):
43
43
  expires_in: int = Field(alias="expiresIn")
44
44
  region: str | None = None
45
45
  created_at: str = Field(alias="createdAt")
46
+ refreshed_at: str | None = None
46
47
 
47
48
 
48
49
  class Device(BaseModel):
@@ -56,24 +57,31 @@ class Device(BaseModel):
56
57
  device_type: str = Field(alias="deviceType")
57
58
  group_id: int = Field(alias="groupId")
58
59
  type: int
59
- type_code: int = Field(alias="typeCode")
60
+ type_code: int = Field(0, alias="typeCode")
60
61
  unique_id: str = Field(alias="uniqueId")
61
62
 
63
+ @field_validator("device_name", "device_type", "unique_id", mode="before")
64
+ def convert_to_lower(cls, value): # noqa: N805
65
+ """Convert device_name, device_type and unique_id to lowercase."""
66
+ if value is not None and isinstance(value, str):
67
+ return value.lower()
68
+ return value
69
+
62
70
 
63
71
  class Pet(BaseModel):
64
72
  """Dataclass for pet data.
65
73
  Subclass of AccountData.
66
74
  """
67
75
 
68
- avatar: str | None = None
76
+ avatar: str
69
77
  created_at: int = Field(alias="createdAt")
70
78
  pet_id: int = Field(alias="petId")
71
- pet_name: str | None = Field(None, alias="petName")
79
+ pet_name: str = Field(alias="petName")
72
80
  id: int | None = None # Fictive field copied from id (for HA compatibility)
73
81
  sn: str | None = None # Fictive field copied from id (for HA compatibility)
74
82
  name: str | None = None # Fictive field copied from pet_name (for HA compatibility)
75
- device_type: str = "pet" # Fictive fixed field (for HA compatibility)
76
83
  firmware: str | None = None # Fictive fixed field (for HA compatibility)
84
+ device_nfo: Device | None = None # Device is now optional
77
85
 
78
86
  # Litter stats
79
87
  last_litter_usage: int = 0
@@ -85,9 +93,7 @@ class Pet(BaseModel):
85
93
  """Initialize the Pet dataclass.
86
94
  This method is used to fill the fictive fields after the standard initialization.
87
95
  """
88
- # Appeler l'initialisation standard de Pydantic
89
96
  super().__init__(**data)
90
- # Remplir les champs fictifs après l'initialisation
91
97
  self.id = self.id or self.pet_id
92
98
  self.sn = self.sn or str(self.id)
93
99
  self.name = self.name or self.pet_name
@@ -267,6 +267,7 @@ class FeederRecord(BaseModel):
267
267
  move: list[RecordsType] | None = None
268
268
  pet: list[RecordsType] | None = None
269
269
  device_type: str | None = Field(None, alias="deviceType")
270
+ type_code: int = Field(0, alias="typeCode")
270
271
 
271
272
  @classmethod
272
273
  def get_endpoint(cls, device_type: str) -> str | None:
@@ -290,7 +291,7 @@ class FeederRecord(BaseModel):
290
291
  if request_date is None:
291
292
  request_date = datetime.now().strftime("%Y%m%d")
292
293
 
293
- if device.device_type.lower() == D4:
294
+ if device.device_type == D4:
294
295
  return {"date": request_date, "type": 0, "deviceId": device.device_id}
295
296
  return {"days": request_date, "deviceId": device.device_id}
296
297
 
@@ -305,7 +306,6 @@ class Feeder(BaseModel):
305
306
  cloud_product: CloudProduct | None = Field(None, alias="cloudProduct")
306
307
  created_at: str | None = Field(None, alias="createdAt")
307
308
  device_records: FeederRecord | None = None
308
- device_type: str | None = Field(None, alias="deviceType")
309
309
  firmware: float
310
310
  firmware_details: list[FirmwareDetail] | None = Field(None, alias="firmwareDetails")
311
311
  hardware: int
@@ -326,6 +326,7 @@ class Feeder(BaseModel):
326
326
  sn: str
327
327
  state: StateFeeder | None = None
328
328
  timezone: float | None = None
329
+ device_nfo: Device | None = None
329
330
 
330
331
  @classmethod
331
332
  def get_endpoint(cls, device_type: str) -> str:
@@ -235,7 +235,6 @@ class LitterRecord(BaseModel):
235
235
  toilet_detection: int | None = Field(None, alias="toiletDetection")
236
236
  upload: int | None = None
237
237
  user_id: str | None = Field(None, alias="userId")
238
- device_type: str | None = Field(None, alias="deviceType")
239
238
 
240
239
  @classmethod
241
240
  def get_endpoint(cls, device_type: str) -> str:
@@ -254,7 +253,7 @@ class LitterRecord(BaseModel):
254
253
  request_date: str | None = None,
255
254
  ) -> dict:
256
255
  """Generate query parameters including request_date."""
257
- device_type = device.device_type.lower()
256
+ device_type = device.device_type
258
257
  if device_type == T4:
259
258
  if request_date is None:
260
259
  request_date = datetime.now().strftime("%Y%m%d")
@@ -295,7 +294,6 @@ class LitterStats(BaseModel):
295
294
  statistic_time: str | None = Field(None, alias="statisticTime")
296
295
  times: int | None = None
297
296
  total_time: int | None = Field(None, alias="totalTime")
298
- device_type: str | None = None
299
297
 
300
298
  @classmethod
301
299
  def get_endpoint(cls, device_type: str) -> str:
@@ -359,7 +357,6 @@ class PetOutGraph(BaseModel):
359
357
  storage_space: int | None = Field(None, alias="storageSpace")
360
358
  time: int | None = None
361
359
  toilet_time: int | None = Field(None, alias="toiletTime")
362
- device_type: str | None = None
363
360
 
364
361
  @classmethod
365
362
  def get_endpoint(cls, device_type: str) -> str:
@@ -382,6 +379,31 @@ class PetOutGraph(BaseModel):
382
379
  }
383
380
 
384
381
 
382
+ class K3Device(BaseModel):
383
+ """Dataclass for K3 device data."""
384
+
385
+ battery: int | None = None
386
+ created_at: datetime | None = Field(None, alias="createdAt")
387
+ firmware: int | None = None
388
+ hardware: int | None = None
389
+ id: int | None = None
390
+ lighting: int | None = None
391
+ liquid: int | None = None
392
+ liquid_lack: int | None = Field(None, alias="liquidLack")
393
+ mac: str | None = None
394
+ name: str | None = None
395
+ refreshing: int | None = None
396
+ relate_t4: int | None = Field(None, alias="relateT4")
397
+ relation: dict | None = None
398
+ secret: str | None = None
399
+ settings: dict | None = None
400
+ sn: str | None = None
401
+ timezone: float | None = None
402
+ update_at: datetime | None = Field(None, alias="updateAt")
403
+ user_id: str | None = Field(None, alias="userId")
404
+ voltage: int | None = None
405
+
406
+
385
407
  class Litter(BaseModel):
386
408
  """Dataclass for Litter Data.
387
409
  Supported devices = T3, T4, T6
@@ -396,6 +418,7 @@ class Litter(BaseModel):
396
418
  firmware_details: list[FirmwareDetail] = Field(alias="firmwareDetails")
397
419
  hardware: int
398
420
  id: int
421
+ k3_device: K3Device | None = Field(None, alias="k3Device")
399
422
  is_pet_out_tips: int | None = Field(None, alias="isPetOutTips")
400
423
  locale: str | None = None
401
424
  mac: str | None = None
@@ -422,10 +445,10 @@ class Litter(BaseModel):
422
445
  service_status: int | None = Field(None, alias="serviceStatus")
423
446
  total_time: int | None = Field(None, alias="totalTime")
424
447
  with_k3: int | None = Field(None, alias="withK3")
425
- device_type: str | None = Field(None, alias="deviceType")
426
448
  device_records: list[LitterRecord] | None = None
427
449
  device_stats: LitterStats | None = None
428
450
  device_pet_graph_out: list[PetOutGraph] | None = None
451
+ device_nfo: Device | None = None
429
452
 
430
453
  @classmethod
431
454
  def get_endpoint(cls, device_type: str) -> str:
@@ -0,0 +1,77 @@
1
+ """Dataclasses for feeder data."""
2
+
3
+ from typing import Any, ClassVar
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from pypetkitapi.const import DEVICE_DATA, PetkitEndpoint
8
+ from pypetkitapi.containers import Device, FirmwareDetail, Wifi
9
+
10
+
11
+ class Settings(BaseModel):
12
+ """Dataclass for settings data."""
13
+
14
+ auto_work: int | None = Field(None, alias="autoWork")
15
+ lack_notify: int | None = Field(None, alias="lackNotify")
16
+ light_mode: int | None = Field(None, alias="lightMode")
17
+ light_range: list[int] | None = Field(None, alias="lightRange")
18
+ log_notify: int | None = Field(None, alias="logNotify")
19
+ manual_lock: int | None = Field(None, alias="manualLock")
20
+ sound: int | None = None
21
+ temp_unit: int | None = Field(None, alias="tempUnit")
22
+
23
+
24
+ class State(BaseModel):
25
+ """Dataclass for state data."""
26
+
27
+ humidity: int | None = None
28
+ left_day: int | None = Field(None, alias="leftDay")
29
+ liquid: int | None = None
30
+ mode: int | None = None
31
+ ota: int | None = None
32
+ overall: int | None = None
33
+ pim: int | None = None
34
+ power: int | None = None
35
+ refresh: float | None = None
36
+ temp: int | None = None
37
+ wifi: Wifi | None = None
38
+
39
+
40
+ class Purifier(BaseModel):
41
+ """Dataclass for feeder data."""
42
+
43
+ data_type: ClassVar[str] = DEVICE_DATA
44
+
45
+ bt_mac: str | None = Field(None, alias="btMac")
46
+ created_at: str | None = Field(None, alias="createdAt")
47
+ firmware: str | None = None
48
+ firmware_details: list[FirmwareDetail] | None = Field(None, alias="firmwareDetails")
49
+ hardware: int | None = None
50
+ id: int | None = None
51
+ locale: str | None = None
52
+ mac: str | None = None
53
+ name: str | None = None
54
+ relation: dict[str, str]
55
+ secret: str | None = None
56
+ settings: Settings | None = None
57
+ share_open: int | None = Field(None, alias="shareOpen")
58
+ signup_at: str | None = Field(None, alias="signupAt")
59
+ sn: str | None = None
60
+ state: State | None = None
61
+ timezone: float | None = None
62
+ work_time: list[tuple[int, int]] | None = Field(None, alias="workTime")
63
+ device_nfo: Device | None = None
64
+
65
+ @classmethod
66
+ def get_endpoint(cls, device_type: str) -> str:
67
+ """Get the endpoint URL for the given device type."""
68
+ return PetkitEndpoint.DEVICE_DETAIL
69
+
70
+ @classmethod
71
+ def query_param(
72
+ cls,
73
+ device: Device,
74
+ device_data: Any | None = None,
75
+ ) -> dict:
76
+ """Generate query parameters including request_date."""
77
+ return {"id": int(device.device_id)}
@@ -93,7 +93,6 @@ class WaterFountainRecord(BaseModel):
93
93
  day_time: int | None = Field(None, alias="dayTime")
94
94
  stay_time: int | None = Field(None, alias="stayTime")
95
95
  work_time: int | None = Field(None, alias="workTime")
96
- device_type: str | None = Field(None, alias="deviceType")
97
96
 
98
97
  @classmethod
99
98
  def get_endpoint(cls, device_type: str) -> str | None:
@@ -163,8 +162,11 @@ class WaterFountain(BaseModel):
163
162
  update_at: str | None = Field(None, alias="updateAt")
164
163
  user_id: str | None = Field(None, alias="userId")
165
164
  water_pump_run_time: int | None = Field(None, alias="waterPumpRunTime")
166
- device_type: str | None = Field(None, alias="deviceType")
167
165
  device_records: list[WaterFountainRecord] | None = None
166
+ device_nfo: Device | None = None
167
+ is_connected: bool = False
168
+ ble_counter: int = 0
169
+ last_ble_poll: str | None = None
168
170
 
169
171
  @classmethod
170
172
  def get_endpoint(cls, device_type: str) -> str:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: pypetkitapi
3
- Version: 1.7.10
3
+ Version: 1.9.0
4
4
  Summary: Python client for PetKit API
5
5
  Home-page: https://github.com/Jezza34000/pypetkit
6
6
  License: MIT
@@ -66,6 +66,8 @@ pip install pypetkitapi
66
66
  ## Usage Example:
67
67
 
68
68
  ```python
69
+ import asyncio
70
+ import logging
69
71
  import aiohttp
70
72
  from pypetkitapi.client import PetKitClient
71
73
  from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, LBAction, LitterCommand
@@ -99,7 +101,10 @@ async def main():
99
101
 
100
102
  ### Example 2 : Feed the pet
101
103
  ### Device_ID, Command, Payload
104
+ # simple hopper :
102
105
  await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
106
+ # dual hopper :
107
+ await client.send_api_request(123456789, FeederCommand.MANUAL_FEED_DUAL, {"amount1": 2})
103
108
 
104
109
  ### Example 3 : Start the cleaning process
105
110
  ### Device_ID, Command, Payload
@@ -110,6 +115,10 @@ if __name__ == "__main__":
110
115
  asyncio.run(main())
111
116
  ```
112
117
 
118
+ ## More example usage
119
+
120
+ Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
121
+
113
122
  ## Contributing
114
123
 
115
124
  Contributions are welcome! Please open an issue or submit a pull request.
@@ -0,0 +1,16 @@
1
+ pypetkitapi/__init__.py,sha256=FSbNIvn4PuEEWbVnJ2rQEQkyOZgCYiXVAgrWqokbnVg,1562
2
+ pypetkitapi/client.py,sha256=J7d8lgrCyR1fJPEB9zUTu_z7HVDp_ALgD_qihu9KAYo,27630
3
+ pypetkitapi/command.py,sha256=G7AEtUcaK-lcRliNf4oUxPkvDO_GNBkJ-ZUcOo7DGHM,7697
4
+ pypetkitapi/const.py,sha256=pkTJ0l-8mQix9aoJNC2UYfyUdG7ie826xnv7EkOZtPw,4208
5
+ pypetkitapi/containers.py,sha256=oJR22ZruMr-0IRgiucdnj_nutOH59MKvmaFTwLJNiJI,4635
6
+ pypetkitapi/exceptions.py,sha256=fuTLT6Iw2_kA7eOyNJPf59vQkgfByhAnTThY4lC0Rt0,1283
7
+ pypetkitapi/feeder_container.py,sha256=q9lsUvBMxVt-vs8gE3Gg7jVyiIz_eh-3Vq-vMhDIzGA,14472
8
+ pypetkitapi/litter_container.py,sha256=kVbPddHV7wjaeBbRIsigHn_-G42qLZyFUXJg5U5PcQ0,19061
9
+ pypetkitapi/medias.py,sha256=IuWkC7usw0Hbx173X8TGv24jOp4nqv6bIUosZBpXMGg,6945
10
+ pypetkitapi/purifier_container.py,sha256=ssyIxhNben5XJ4KlQTXTrtULg2ji6DqHqjzOq08d1-I,2491
11
+ pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pypetkitapi/water_fountain_container.py,sha256=5J0b-fDZYcFLNX2El7fifv8H6JMhBCt-ttxSow1ozRQ,6787
13
+ pypetkitapi-1.9.0.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
14
+ pypetkitapi-1.9.0.dist-info/METADATA,sha256=ga5bwlaFeRelb8zt8nUMGpSt6X8Uh3mvnbbJwjYteEU,5167
15
+ pypetkitapi-1.9.0.dist-info/WHEEL,sha256=RaoafKOydTQ7I_I3JTrPCg6kUmTgtm4BornzOqyEfJ8,88
16
+ pypetkitapi-1.9.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.0.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,15 +0,0 @@
1
- pypetkitapi/__init__.py,sha256=r1w3zjlBQJOVxP43DyaVDvtsGOGm5nKYTBqhS8KqQkA,908
2
- pypetkitapi/client.py,sha256=VsISt7-njR0eFlloJenBjMXqeHjnrXXJr-7wamNA3fk,20473
3
- pypetkitapi/command.py,sha256=q_9B-_z74BicMKNTR46PpELhmqW1cUV8q-95D6riJy4,7959
4
- pypetkitapi/const.py,sha256=RzGVarP4lfF7eYjpp5XOPgDcen8zJYBucZSn0Lcj3rw,4044
5
- pypetkitapi/containers.py,sha256=8A_ShIqIt4PNL7c7_OAipVq3OFb5wCPiyGAp4UgenX4,4417
6
- pypetkitapi/exceptions.py,sha256=fuTLT6Iw2_kA7eOyNJPf59vQkgfByhAnTThY4lC0Rt0,1283
7
- pypetkitapi/feeder_container.py,sha256=97vcOe6WaX-pi2d2-VPfn3WMYvPffhv4DEpGPMA_cQo,14457
8
- pypetkitapi/litter_container.py,sha256=lcoWt3SRSrAaH11DXybSNT3n5KAMamMkOW-CRv2jYJI,18332
9
- pypetkitapi/medias.py,sha256=IuWkC7usw0Hbx173X8TGv24jOp4nqv6bIUosZBpXMGg,6945
10
- pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- pypetkitapi/water_fountain_container.py,sha256=3F4GP5pXJqq6kxLMSK__GgFMuZ-rz1VDIIhaVd19Kl8,6781
12
- pypetkitapi-1.7.10.dist-info/LICENSE,sha256=4FWnKolNLc1e3w6cVlT61YxfPh0DQNeQLN1CepKKSBg,1067
13
- pypetkitapi-1.7.10.dist-info/METADATA,sha256=xNrfF_madK5tDmGzl-O1F-eHhHwBe5yMaB5ljE7O9no,4853
14
- pypetkitapi-1.7.10.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
15
- pypetkitapi-1.7.10.dist-info/RECORD,,